Introduce
In this tutorial, you and you will practice creating a simple Todo List application with NuxtJS and Vuex. Basically, our application will have functions such as: adding, modifying, deleting a job, statistics and filtering out the unfinished and completed jobs. The entire source code I put on git you can go to see and easily follow here .
Todo App will have the interface as shown below:
Setting
To quickly have a NuxtJS project we will use the create-nuxt-app
command. Make sure your computer has node
installed. To check if the computer has installed the node
we do a test in the terminal.
1 2 3 | node -v v10.23.2 |
On my device, it is currently version v10.23.2
. If your computer does not report the node version, you can go full to install the node . Next, we will proceed to create a NuxtJS project.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | npx create-nuxt-app todoapp create-nuxt-app v3.5.2 ✨ Generating Nuxt.js project in todoapp // Đoạn này là nó hỏi bạn đặt tên cho project bạn chỉ cần nhấn Enter ? Project name: (todoapp) // Đoạn này nó hỏi bạn xem code ngôn ngữ nào. Ở đây mình chọn JS nên chỉ cần nhấn Enter ? Programming language: (Use arrow keys) ❯ JavaScript TypeScript // Đoạn hỏi xem bạn muốn quản lý package ntn. Ở đây mình chọn npm (các bạn có thể chọn tùy ý) ? Package manager: (Use arrow keys) ❯ Yarn Npm // Ở đây nó thông kê các UI framework chon bạn chọn để dùng trong dự án. Ở đây mình sẽ chọn None. ? UI framework: (Use arrow keys) ❯ None Ant Design Vue BalmUI Bootstrap Vue Buefy Chakra UI Element Framevuerk iView Tachyons Tailwind CSS Vuesax Vuetify.js Oruga // Module trong NuxtJS bạn dùng ở đây mình dùng Axios -> Enter ? Nuxt.js modules: (Press <space> to select, <a> to toggle all, <i> to invert se lection) ❯◯ Axios - Promise based HTTP client ◯ Progressive Web App (PWA) ◯ Content - Git-based headless CMS // Một số tools để format ở đây mình chọn ESLint và Prettier -> Enter ? Linting tools: ◉ ESLint ❯◉ Prettier ◯ Lint staged files ◯ StyleLint ◯ Commitlint // Testing framework mình chọn None ? Testing framework: (Use arrow keys) ❯ None Jest AVA WebdriverIO Nightwatch // Rendering mode mình chọn Univesal (SSR / SSG) ? Rendering mode: (Use arrow keys) ❯ Universal (SSR / SSG) Single Page App // Deployment target mình chọn Server (Node.js hoisting) -> Enter ? Deployment target: (Use arrow keys) ❯ Server (Node.js hosting) Static (Static/JAMStack hosting) // Chỗ này mình chọn theo recommended -> Enter ? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert selection) ❯◯ jsconfig.json (Recommended for VS Code if you're not using typescript) ◯ Semantic Pull Requests ◯ Dependabot (For auto-updating dependencies, GitHub only) // Về phần CI mình chọn None -> Enter ? Continuous integration: (Use arrow keys) ❯ None GitHub Actions (GitHub only) // VCS mình chọn Git ? Version control system: (Use arrow keys) ❯ Git None |
After waiting for the run to finish you run npm run dev
to run the server we will be.
Route
In the project’s pages directory, we create an additional file todos.vue
. File it will look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <template> <div class="container"> <h1>todos</h1> </div> </template> <script> export default { } </script> <style scoped> // Về phần CSS của giao diện các bạn có thể lấy toàn bộ code trên github repo của mình </style> |
Once created in the pages directory. You access the address http://localhost:<your-port>/todos
. We will have a similar interface below:
Display
In the template we will have the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | <template> <div class="container"> <h1>todos</h1> <div class="main-content"> <input class="todo-input" placeholder="What needs to be done?" @keyup.enter="addTodo" /> <ul> <li v-for="(todo, index) in filterTodos" :key="index" @click.self="toggle(todo)" > <span :class="{ done: todo.isComplete, hide: isEdit === todo.id }" @click.self="toggle(todo)" >{{ todo.content }}</span > <input v-if="isEdit === todo.id" v-model="content" class="input-edit" type="text" @keydown.enter.stop="editTodo(todo)" /> <div> <span class="edit-text" @click.stop="clickEdit(todo)">Edit</span> <span class="delete-text" @click.stop="deleteTodo(todo)" >Delete</span > </div> </li> </ul> <div class="filter"> <span class="all-border" @click="clickAll">All({{ total }})</span> <span class="progress-border" @click="clickProgress" >Progess({{ countProgress }})</span > <span class="done-border" @click="clickDone" >Done({{ countDone }})</span > </div> </div> </div> </template> |
We will have an interface similar to the one below.
API
Here for a little bit I will setup a simple API with mockAPI
. We access: https://www.mockapi.io/
to create an API with simple data as follows.
It should basically look like this:
So the interface and database are finished. Now let’s start coding the main functions !!!.
main fuction
Get todo data
For the data manipulation, I will use Vuex. In the store
directory you create a file todos.js
with the following state. We create an actions
to call API and mutations
to manipulate the state
. I will use the axios
module to operate.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | store/todos.js export const state = () => ({ todoList: [], }) export const getters = { all(state) { return state.todoList }, progress(state) { return state.todoList.filter(function (item) { return !item.isComplete }) }, done(state) { return state.todoList.filter(function (item) { return item.isComplete }) }, } export const mutations = { store(state, data) { state.todoList = data }, } export const actions = { getTodoList(vuexContext) { return this.$axios .$get('https://****************.mockapi.io/todos') .then((res) => { vuexContext.commit('store', res) }) }, } |
The store
function takes a default parameter, the first one is state
and the second parameter is the data
returned from the API. The getTodoList
function has the first default parameter, vuexContext
. In this function we use axios
to call the API we created in the previous section to get data. After success we use the commit
method to call the store
function in mutations
-> Put data in the state. In gettters
we have 3 functions for data filtering purpose. The all
function gets all data from state
, progress
and done
we use the array’s filter
function to filter out but todo
according to the object’s isCompelete
field.
Next comes the file todos.vue
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | <template> <div class="container"> <h1>todos</h1> <div class="main-content"> <input class="todo-input" placeholder="What needs to be done?"/> <ul> <li v-for="(todo, index) in filterTodos" :key="index" > <span :class="{ done: todo.isComplete, hide: isEdit === todo.id }" > {{ todo.content }} </span> <div> <span class="edit-text">Edit</span> <span class="delete-text">Delete</span> </div> </li> </ul> <div class="filter"> <span class="all-border">All(1)</span> <span class="progress-border">Progress(0)</span> <span class="done-border">Done(0)</span> </div> </div> </div> </template> <script> export default { data() { return { todoList: [], filter: 'all', isEdit: -1, content: '', } }, computed: { filterTodos() { return this.$store.getters[`todos/${this.filter}`] }, }, } </script> |
We will create a computed
filterTodos
to get the data to render and a data
filter
so that when we filter the data by 3 states all, progress, done
. The filterTodos
function we call the filterTodos
getters
. Here I call the getters
function according to the filter
variable in the data
. The rest of the fields in my data
will slowly explain below. In the li
tag, I loop through the computed filterTodos
to render the data in the view. Here I have to check the isCompelete
field to check if todo
is done then I will add line-through
to it.
After the code we will have a view that is almost complete. Now we just need to add events to it.
Add a new todo item
To do this function we need to add some functions as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // store/todos.js // Hàm này được thêm vào mutations add(state, content) { state.todoList.push(content) } // Hàm này được thêm vào actions addTodo(vuexContext, content) { return this.$axios .$post('https://6035ea036496b9001749f8ee.mockapi.io/todos', { content, isComplete: false, }) .then(function (res) { vuexContext.commit('add', res) }) .catch(function (err) { alert(err) }) } |
In the todos.js
file we add the add(state, content)
to mutations
to push the new element to the todoList
in the state
. The addTodo(vuexContext, content)
function addTodo(vuexContext, content)
I use axios
to send a post request to the API after success, we will use a commit
to call the add
function in mutations
to add a new element to the array -> computed
changes -> re-render.
1 2 3 4 5 6 7 8 9 10 | // pages/todos.vue // Hàm này được thêm vào methods addTodo(e) { if (e.target.value.length) { this.$store.dispatch('todos/addTodo', e.target.value) e.target.value = '' } } |
In todos.vue
we attach an event when the user presses enter
will trigger the addTodo(e)
function in methods
. This function will dispatch
to the addTodo
function in the actions
of Vuex.
Modify an todo item
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // pages/todos.vue // Hàm này được thêm vào methods clickEdit(todo) { this.isEdit = todo.id this.content = todo.content } editTodo(todo) { if (this.content.length) { this.$store.dispatch('todos/editTodo', { todo, content: this.content }) this.isEdit = -1 this.content = '' } }, |
We will attach the click
event to the span edit
tab in the template. When the user click
the clickEdit(todo)
function is activated. This function will change the isEdit
and the content
of the data
in the component. Why must to do that? I do this because when I click edit
, I have to know which todo
is being edit
and must show an input box with the value of todo
‘s content and hide the span
tag that is displaying the todo
‘s content being edited. The editTodo(todo)
will dispatch
to actions
in Vuex and after editing is complete, we will change isEdit, content
back to the same.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // store/todos.js // Hàm này được thêm vào actions editTodo(vuexContext, data) { return this.$axios .$put( `https://6035ea036496b9001749f8ee.mockapi.io/todos/${data.todo.id}`, { content: data.content, } ) .then(function (res) { vuexContext.commit('edit', res) }) .catch(function (err) { alert(err) }) } // Hàm này được thêm vào mutations edit(state, todo) { const index = state.todoList.findIndex( (todoItem) => todoItem.id === todo.id ) state.todoList[index].content = todo.content }, |
The editTodo
function in actions
similar to the addTodo
function above and triggers the edit
function in mutations
.
Updating the status of an todo item
1 2 3 4 5 6 7 | // pages/todos.vue // Hàm này được thêm vào methods toggle(todo) { this.$store.dispatch('todos/toggleTodo', todo) }, |
We will attach the click.self
event to the li
tag of each todoItem
. When the user clicks on a todo
, it changes the state to complete or incomplete. Once completed, the todo
will be dashed. Function toggle
also dispatch to function toggleTodo
in store
Exploration of Vuex
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // store/todos.js // Hàm này được thêm vào actions toggleTodo(vuexContext, todo) { return this.$axios .$put(`https://6035ea036496b9001749f8ee.mockapi.io/todos/${todo.id}`, { isComplete: !todo.isComplete, }) .then(function (res) { vuexContext.commit('toggle', res) }) .catch(function (err) { alert(err) }) }, // Hàm này được thêm vào mutations remove(state, todo) { const index = state.todoList.findIndex( (todoItem) => todoItem.id === todo.id ) state.todoList.splice(index, 1) }, |
The toggleTodo
function in actions
similar to the editTodo
function above and triggers the toggle
function in mutations
. Specifically, we will change the data on the API and state
of Vuex
.
Delete a todo item
1 2 3 4 5 6 7 | // pages/todos.vue // Hàm này được thêm vào methods deleteTodo(todo) { this.$store.dispatch('todos/deleteTodo', todo) }, |
We will attach the click.stop
event to the span delete
tag of each todoItem
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // store/todos.js // Hàm này được thêm vào actions deleteTodo(vuexContext, todo) { return this.$axios .$delete(`https://6035ea036496b9001749f8ee.mockapi.io/todos/${todo.id}`) .then(function (res) { vuexContext.commit('remove', res) }) .catch(function (err) { alert(err) }) }, // Hàm này được thêm vào mutations remove(state, todo) { const index = state.todoList.findIndex( (todoItem) => todoItem.id === todo.id ) state.todoList.splice(index, 1) }, |
The deleteTodo
function in actions
triggers the remove
function in mutations
. The use is similar to the above functions.
Filter
So we have finished the basic functions such as: Add, edit, delete already. Now we will proceed to create filters to see the completed, unfinished, or all works.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | // pages/todos.vue data() { return { filter: 'all', isEdit: -1, content: '', } }, computed: { filterTodos() { return this.$store.getters[`todos/${this.filter}`] }, total() { return this.$store.state.todos.todoList.length }, countProgress() { return this.$store.state.todos.todoList.filter(function (item) { return !item.isComplete }).length }, countDone() { return this.total - this.countProgress }, }, // Các hàm sau được thêm vào methods clickAll() { this.filter = 'all' }, clickProgress() { this.filter = 'progress' }, clickDone() { this.filter = 'done' }, |
In the data
I add a filter
to see how we are filtering the data, the default will be all
. The methods
will correspond to the events when the user presses all , progress and done . In the computed
we will calculate the number of todoList
for visualization and the todoList
we render by filter. In the filterTodos
function we will call the filterTodos
getter
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // store/todos.js export const getters = { all(state) { return state.todoList }, progress(state) { return state.todoList.filter(function (item) { return !item.isComplete }) }, done(state) { return state.todoList.filter(function (item) { return item.isComplete }) }, } |
In the file store/todos.js
. We will get getters
corresponding to the filters.
After completing the filter, we will demo a little bit of the app:
summary
So we’ve completed a simple todo app with NuxtJS and Vuex. This simple todo App just helps us to get acquainted and practice with Vuex, a little bit about NuxtJS. Hope you will have more knowledge and apply effectively to your project. See you in other sharing articles.