Development
04 minsCreating a to-do list with the Vue 3 Composition API
Building a Vue 3 to-do list with typed props and emits.
Daniel Jarrett
06.01.23
First off let’s create a typescript interface, describing how our data will be structured. To-do components are pretty simple, we need to know a couple of things: the name of each item and whether it is checked or not. We will also handle the ToDoListItem component props as a separate interface, using the initial interface as a base.
A good reason to start with typings before handling data is so you can have an overview of what is required and plan how you’re going to achieve your goal. We know that we need a name and checked status and we know we need to handle deletion and change (checked) at the item level – so we can cater for that and set up all the relations at the beginning.
Create a new folder and file at src/types/to-do-item.ts – this is where we will write our types for the ToDoListItem. The first interface we will create is ToDoItem, this will describe the name and checked properties and corresponding types: string and boolean respectively.
1interface ToDoItem {2 name: string3 checked: boolean4}5 6export type {7 ToDoItem,8}
copied!
Don’t forget to export your types!
This interface will be used in the parent ToDoList component to help structure our data and used in the ToDoListItem component to define the props.
1 2import { ref, defineEmits, defineProps } from 'vue' 3import type { ToDoItem } from '../types/to-do-item' 4import CheckIcon from './icons/CheckIcon.vue' 5import DeleteIcon from './icons/DeleteIcon.vue' 6 7const props = defineProps() 8 9const emit = defineEmits()10 11const isChecked = ref(props?.task?.checked || false)12 13const handleDelete = (): void => emit('delete', props.task)14 15const handleChange = (event: Event): void => {16 const { checked } = event.target as HTMLInputElement17 isChecked.value = checked18 emit('changed', checked)19}
copied!
Both defineProps and defineEmits use a generic type which allows us to pass the types to the functions in question with this syntax: myFunction<MyType>(). So we will pass ToDoItem to our definition of props and for our emit definition, we will add another event for delete.
Now with access to our delete event, we can create a function called handleDelete to emit the event with our props.task payload.
With access to our props, we can pass props.checked as the default value to the ref assigned to isChecked.
1 2 3 4 5 6 {{ props.task.name }} 7 8 9 10 11 12 13import { ref, defineEmits, defineProps } from 'vue'14import type { ToDoItem } from '../types/to-do-item'15import CheckIcon from './icons/CheckIcon.vue'16import DeleteIcon from './icons/DeleteIcon.vue'17 18const props = defineProps()19 20const emit = defineEmits()21 22const isChecked = ref(props?.task?.checked || false)23 24const handleDelete = (): void => emit('delete', props.task)25 26const handleChange = (event: Event): void => {27 const { checked } = event.target as HTMLInputElement28 isChecked.value = checked29 emit('change', checked)30}31 32 33 34 .item {35 width: 100%;36 list-style: none;37 display: flex;38 align-items: center;39 justify-content: space-between;40 padding-bottom: 8px;41 42 &__label {43 display: flex;44 align-items: center;45 flex-grow: 1;46 cursor: pointer;47 position: relative;48 }49 50 &__checkbox {51 width: 16px;52 height: 16px;53 border-radius: 4px;54 border: 1px solid #fff;55 background-color: transparent;56 appearance: none;57 cursor: pointer;58 transition: border 0.2s, background 0.2s;59 60 &:hover {61 border-color: var(--blue);62 }63 64 &--complete {65 border-color: var(--blue);66 background-color: var(--blue);67 68 & + .item__check {69 opacity: 1;70 }71 }72 }73 74 &__check {75 width: 10px;76 position: absolute;77 left: 7px;78 opacity: 0;79 transition: opacity 0.2s;80 }81 82 &__name {83 padding-left: 12px;84 85 &--complete {86 text-decoration: line-through rgba(255, 255, 255, 0.75);87 }88 }89 90 &__delete {91 cursor: pointer;92 width: 24px;93 }94 }
copied!
We will use the @click directive to assign the function we created a moment ago, handleDelete. Clicking the DeleteIcon then will trigger our delete event - just as we wanted! We will also update .item__name content with {{ props.task.name }} - we use the handlebars syntax to bind and display data in the template.
Next up is rendering the list of items based off a bit of mock data for now. So we will have to create a component named ToDoList and render our tasks.
1 2 3 4 5 6 7 8import { ref } from 'vue' 9import type { ToDoItem } from '../types/to-do-item'10import ToDoListItem from './ToDoListItem.vue'11 12const props = defineProps()13 14const tasks = ref(props.tasks)15 16const handleChange = (task: ToDoItem, checked: boolean): void => {17 const index = tasks.value.indexOf(task)18 tasks.value[index].checked = checked19}
copied!
We move our simple <ul> and <ToDoListItem> elements from App.vue and put it in our new ToDoList component. In our script section we set up our props to receive tasks, and utilise the ref API and pass the tasks prop as our default value.
Don’t forget to give your ToDoListItem a key relative to your v-for loop - so we can just use task.name + ix here.
We will require an event handling function for when the ToDoListItem gets checked - we need to find the index of the current task and then set its checked value to be what is passed via the parameters for the event handler.
As we have tasks setup with the ref API - we can easily update the data via the .value property, selecting our index and then setting the checked property with our boolean! Nice and simple.
Let’s not forget to change the App.vue to check our new component is working!
1 2 3 4 5 6import ToDoList from './components/ToDoList.vue' 7 8const tasks = [ 9 { name: 'say hello', checked: false },10 { name: 'say goodbye', checked: true },11]12 13 14 15:root {16 --blue: #4D9DE0;17}
copied!
Next up we should add an input to add new tasks and do some general styling updates. We add some basic markup and styling to our new markup.
1 2 3 4 5 6 7 8 9 10 11import { ref } from 'vue'12import type { ToDoItem } from '../types/to-do-item'13import ToDoListItem from './ToDoListItem.vue'14 15const props = defineProps()16 17const tasks = ref(props.tasks)18 19const handleChange = (task: ToDoItem, checked: boolean): void => {20 const index = tasks.value.indexOf(task)21 tasks.value[index].checked = checked22}23 24const handleDelete = (task: ToDoItem): void => {25 const index = tasks.value.indexOf(task)26 tasks.value.splice(index, 1)27}28 29const handleEnterKey = (event: KeyboardEvent): void => {30 const input = (event.target as HTMLInputElement)31 32 if (!input.value) return33 34 tasks.value.unshift({35 name: input.value,36 checked: false,37 })38 39 input.value = ''40}41 42 43 44.todo {45 width: 300px;46 47 &__input {48 width: 100%;49 border: 0;50 border-bottom: 1px solid white;51 background-color: transparent;52 outline: 0;53 padding: 8px 0;54 box-sizing: border-box;55 font-size: 16px;56 transition: border 0.3s ease-in-out;57 margin-bottom: 8px;58 59 &:focus,60 &:hover {61 border-color: var(--blue);62 }63 }64 65 &__list {66 list-style: none;67 padding: 0;68 }69}
copied!
The last part we need to do now is hook up an event handler when the user presses the Enter key. Luckily in Vue we can use a “key modifier” - which allows us to check for specific keys. We will add this event and key modifier, then use handleEnterKey as the name for our event handler.
Our event handler handleEnterKey will add a new ToDoItem object at the beginning of the tasks array (using unshift) and we will reset the inputs value so we can easily add another task afterwards.
1const handleEnterKey = (event: KeyboardEvent): void => { 2 const input = (event.target as HTMLInputElement) 3 4 if (!input.value) return 5 6 tasks.value.unshift({ 7 name: input.value, 8 checked: false, 9 })10 11 input.value = ''12}
copied!
With all of this, we should now have a working to-do list that can add tasks, remove tasks and check/uncheck tasks!