Like most JS frameworks Vue supports state. And like the others Vue has component & global state.
Let's build a Todo list to learn how to CRUD using local & global state in Vue.
Start by defining the TodosContainer.vue
component.
Start with defining your state vars.
The newTodo
var will hold the new todo we're adding & todos
our list of todos.
1<script setup>
2const todos = ref([])
3const newTodo = ref('')
4</script>
5
6<template>
7 <div>
8 <h1>Todos</h1>
9 </div>
10</template>
Next add an input to update newTodo
.
Update newTodo
when the input changes by binding it to the input using v-model
1<script setup>
2const todos = ref([])
3const newTodo = ref('')
4</script>
5
6<template>
7 <div>
8 <h1>Todos</h1>
9 <input
10 autofocus
11 v-model="newTodo"
12 class="text-black px-1"
13 />
14 </div>
15</template>
Next define a handler, addTodo
, that implements the logic of adding a todo
to our todos
list.
1<script setup>
2const todos = ref([])
3const newTodo = ref('')
4
5function addTodo() {
6 const todo = {
7 done: false,
8 name: newTodo.value,
9 id: todos.value.length + 1,
10 }
11 todos.value.push(todo)
12 newTodo.value = ''
13}
14</script>
15
16<template>
17 <div>
18 <h1>Todos</h1>
19 <input
20 autofocus
21 v-model="newTodo"
22 class="text-black px-1"
23 />
24 </div>
25</template>
And lastly trigger addTodo
when the user hits the enter key by binding addTodo
to the enter key up event.
1<script setup>
2const todos = ref([])
3const newTodo = ref('')
4
5function addTodo() {
6 const todo = {
7 done: false,
8 name: newTodo.value,
9 id: todos.value.length + 1,
10 }
11 todos.value.push(todo)
12 newTodo.value = ''
13}
14</script>
15
16<template>
17 <div>
18 <h1>Todos</h1>
19 <input
20 autofocus
21 v-model="newTodo"
22 class="text-black px-1"
23 + @keyup.enter="addTodo"
24 />
25 </div>
26</template>
You'll now see todos
update when you enter a new todo and press enter in your console.
Next use a v-for
to render the todos
.
1<script setup>
2const todos = ref([])
3const newTodo = ref('')
4
5function addTodo() {
6 const todo = {
7 done: false,
8 name: newTodo.value,
9 id: todos.value.length + 1,
10 }
11 todos.value.push(todo)
12 newTodo.value = ''
13}
14</script>
15
16<template>
17 <div>
18 <h1>Todos</h1>
19 <input
20 autofocus
21 v-model="newTodo"
22 class="text-black px-1"
23 @keyup.enter="addTodo"
24 />
25 <ul>
26 <li
27 :key="todo.id"
28 v-for="todo of todos"
29 class="flex flex-row justify-between"
30 >
31 <span v-text="todo.name" />
32 </li>
33 </ul>
34 </div>
35</template>
Next implement the ability to add a todo by defining the handler & a state var newTodo
to hold the name of the todo
Now let's add the ability to update a todo.
Define a function to toggle the status of a todo, toggleStatus
.
1<script setup>
2const todos = ref([])
3const newTodo = ref('')
4
5function addTodo() {
6 const todo = {
7 done: false,
8 name: newTodo.value,
9 id: todos.value.length + 1,
10 }
11 todos.value.push(todo)
12 newTodo.value = ''
13}
14function toggleStatus(id) {
15 const idx = todos.value.findIndex((t) => t.id === id)
16 const todo = todos.value[idx]
17 todo.done = !todo.done
18 todos.value[idx] = todo
19}
20</script>
21
22<template>
23 <div>
24 <h1>Todos</h1>
25 <input
26 autofocus
27 v-model="newTodo"
28 class="text-black px-1"
29 @keyup.enter="addTodo"
30 />
31 <ul>
32 <li
33 :key="todo.id"
34 v-for="todo of todos"
35 class="flex flex-row justify-between"
36 >
37 <span v-text="todo.name" />
38 </li>
39 </ul>
40 </div>
41</template>
Bind toggleStatus
to the @click
of each todo item.
Also make sure to pass the id of the todo to toggleStatus
as well.
1<script setup>
2const todos = ref([])
3const newTodo = ref('')
4
5function addTodo() {
6 const todo = {
7 done: false,
8 name: newTodo.value,
9 id: todos.value.length + 1,
10 }
11 todos.value.push(todo)
12 newTodo.value = ''
13}
14function toggleStatus(id) {
15 const idx = todos.value.findIndex((t) => t.id === id)
16 const todo = todos.value[idx]
17 todo.done = !todo.done
18 todos.value[idx] = todo
19}
20</script>
21
22<template>
23 <div>
24 <h1>Todos</h1>
25 <input
26 autofocus
27 v-model="newTodo"
28 class="text-black px-1"
29 @keyup.enter="addTodo"
30 />
31 <ul>
32 <li
33 :key="todo.id"
34 v-for="todo of todos"
35 + @click="toggleStatus(todo.id)"
36 class="flex flex-row justify-between"
37 >
38 <span v-text="todo.name" />
39 </li>
40 </ul>
41 </div>
42</template>
Lastly programmatically add the class .done
to each todo item.
Also define a class .done
in the style tag which will give the todo a line-through if it's status is done.
1<script setup>
2const todos = ref([])
3const newTodo = ref('')
4
5function addTodo() {
6 const todo = {
7 done: false,
8 name: newTodo.value,
9 id: todos.value.length + 1,
10 }
11 todos.value.push(todo)
12 newTodo.value = ''
13}
14function toggleStatus(id) {
15 const idx = todos.value.findIndex((t) => t.id === id)
16 const todo = todos.value[idx]
17 todo.done = !todo.done
18 todos.value[idx] = todo
19}
20</script>
21
22<template>
23 <div>
24 <h1>Todos</h1>
25 <input
26 autofocus
27 v-model="newTodo"
28 class="text-black px-1"
29 @keyup.enter="addTodo"
30 />
31 <ul>
32 <li
33 :key="todo.id"
34 v-for="todo of todos"
35 :class="{ done: todo.done }"
36 @click="toggleStatus(todo.id)"
37 class="flex flex-row justify-between"
38 >
39 <span v-text="todo.name" />
40 </li>
41 </ul>
42 </div>
43</template>
44
45<style>
46.done {
47 color: indianred;
48 text-decoration: line-through;
49}
50</style>
The last thing we need to do is add the ability to remove a todo item.
Define removeTodo
which finds a todo in the list via id and removes it from the list.
1<script setup>
2const todos = ref([])
3const newTodo = ref('')
4
5function addTodo() {
6 const todo = {
7 done: false,
8 name: newTodo.value,
9 id: todos.value.length + 1,
10 }
11 todos.value.push(todo)
12 newTodo.value = ''
13}
14function toggleStatus(id) {
15 const idx = todos.value.findIndex((t) => t.id === id)
16 const todo = todos.value[idx]
17 todo.done = !todo.done
18 todos.value[idx] = todo
19}
20function removeTodo(id) {
21 const idx = todos.value.findIndex((t) => t.id === id)
22 todos.value.splice(idx, 1)
23}
24</script>
25
26<template>
27 <div>
28 <h1>Todos</h1>
29 <input
30 autofocus
31 v-model="newTodo"
32 class="text-black px-1"
33 @keyup.enter="addTodo"
34 />
35 <ul>
36 <li
37 :key="todo.id"
38 v-for="todo of todos"
39 :class="{ done: todo.done }"
40 @click="toggleStatus(todo.id)"
41 class="flex flex-row justify-between"
42 >
43 <span v-text="todo.name" />
44 </li>
45 </ul>
46 </div>
47</template>
48
49<style>
50.done {
51 color: indianred;
52 text-decoration: line-through;
53}
54</style>
Bind removeTodo
to an @click
event of an HTML element of your choice.
I imported & used an icon here.
Once @click
of the todo item is triggered you'll see the todo removed from the list.
1<script setup>
2import XIcon from '~/assets/images/icons/XIcon.vue'
3const todos = ref([])
4const newTodo = ref('')
5
6function addTodo() {
7 const todo = {
8 done: false,
9 name: newTodo.value,
10 id: todos.value.length + 1,
11 }
12 todos.value.push(todo)
13 newTodo.value = ''
14}
15function toggleStatus(id) {
16 const idx = todos.value.findIndex((t) => t.id === id)
17 const todo = todos.value[idx]
18 todo.done = !todo.done
19 todos.value[idx] = todo
20}
21function removeTodo(id) {
22 const idx = todos.value.findIndex((t) => t.id === id)
23 todos.value.splice(idx, 1)
24}
25</script>
26
27<template>
28 <div>
29 <h1>Todos</h1>
30 <input
31 autofocus
32 v-model="newTodo"
33 class="text-black px-1"
34 @keyup.enter="addTodo"
35 />
36 <ul>
37 <li
38 :key="todo.id"
39 v-for="todo of todos"
40 :class="{ done: todo.done }"
41 @click="toggleStatus(todo.id)"
42 class="flex flex-row justify-between"
43 >
44 <span v-text="todo.name" />
45 <XIcon @click="removeTodo(todo.id)" />
46 </li>
47 </ul>
48 </div>
49</template>
50
51<style>
52.done {
53 color: indianred;
54 text-decoration: line-through;
55}
56</style>
It's often the case that state needs to be shared throughout the application.
To achieve this a parent component could define state and pass it to it's children using props.
This works but it creates a few problems:
Vue has a better solution:
The useState
method is provided by Vue.
To refactor todos
to a global state do the following.
Refactor the initialization of todos
to the following:
1<script setup>
2 -const todos = ref([])
3 +const todos = useState('todos', () => [])
4
5// etc...
6</script>
7
8<template>
9 <!-- etc... -->
10</template>
11
12<style>
13/* etc... */
14</style>
Now you can use the same state in other components by calling useState()
again.
This case for example, you grab the todos and count the done and undone to provide additional context to the user.
1<script setup>
2const todos = useState('todos', () => [])
3const countDone = computed(() => todos.value.filter((t) => t.done).length)
4const countUndone = computed(() => todos.value.filter((t) => !t.done).length)
5</script>
6<template>
7 <div>
8 <div>
9 <label>Done Count</label>
10 <div>
11 <span v-text="countDone" />
12 </div>
13 </div>
14 <div>
15 <label>Undone Count</label>
16 <div>
17 <span v-text="countUndone" />
18 </div>
19 </div>
20 </div>
21</template>
Now you'll see that when you add a todo
to your todos
list then both components, TodosMeta
& TodosContainer
update.