FABs, Dialogs & Forms | Adding New Tasks | Vuetify To-Do List App Tutorial
Use FABs, dialogs, and form controls to enable adding of new tasks in the to-do list app.
Hello and welcome back to our ongoing tutorial series, where we’re going to build a todo app from start to finish using Vuetify and Vue JS. In our last episode, we learned how to display sample to-do list data using the Vuetify v-list
component. Today, we’re going to be adding functionality to enable users to populate the list with new tasks. We won’t need the mock task data we created last time when we’re done with this. Let’s get it to it right away!
Just getting started with Vuetify? Check out this article.
Showing the FAB
We’ll be using a floating action button to allow users to add new tasks, as this is meant to be the primary action in our app. We can create a FAB by setting the fab
prop on the Vuetify v-btn
component.
src/App.js<template>
<v-app>
<v-card>
<v-toolbar color="primary" elevation="3" dark rounded="0">
<v-toolbar-title>Tasks</v-toolbar-title>
</v-toolbar>
</v-card>
<v-card class="ma-4">
<v-list>
<v-list-item
v-for="(task, index) in tasks"
:key="index"
v-bind:class="{ 'task-completed': task.isCompleted }"
two-line
>
<v-checkbox
hide-details
v-model="task.isCompleted"
class="mt-0 mr-2"
></v-checkbox>
<v-list-item-content>
<v-list-item-title>{{ task.title }}</v-list-item-title>
<v-list-item-subtitle>{{ task.note }}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</v-list>
</v-card>
<v-btn fab fixed right bottom color="primary">
</v-btn>
</v-app>
</template>
...
The fixed
, right
and bottom
props should be self-explanatory enough — together they anchor the FAB to the bottom-right corner of the viewport:
The button will display the Material Design plus icon, to indicate that it’s meant for adding tasks:
src/App.js<template>
<v-app>
<v-card>
<v-toolbar color="primary" elevation="3" dark rounded="0">
<v-toolbar-title>Tasks</v-toolbar-title>
</v-toolbar>
</v-card>
<v-card class="ma-4">
<v-list>
<v-list-item
v-for="(task, index) in tasks"
:key="index"
v-bind:class="{ 'task-completed': task.isCompleted }"
two-line
>
<v-checkbox
hide-details
v-model="task.isCompleted"
class="mt-0 mr-2"
></v-checkbox>
<v-list-item-content>
<v-list-item-title>{{ task.title }}</v-list-item-title>
<v-list-item-subtitle>{{ task.note }}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</v-list>
</v-card>
<v-btn fab fixed right bottom color="primary">
<v-icon>mdi-plus</v-icon>
</v-btn>
</v-app>
</template>
...
Get the source code for this app
Sign up here to receive the latest source code for this great app!
Showing the dialog to add new tasks
Now let’s use the v-dialog
component to create a dialog:
src/App.js<template>
<v-app>
...
<v-btn fab fixed right bottom color="primary" @click="showNewTaskDialog = true">
<v-icon>mdi-plus</v-icon>
</v-btn>
<v-dialog v-model="showNewTaskDialog" width="500">
<v-card>
<v-card-title class="text-h5 grey lighten-2">Add New Task</v-card-title>
</v-card>
</v-dialog>
</v-app>
</template><script>
export default {
name: 'App',
data: () => ({
tasks: [...Array(10)].map((value, index) => ({
id: `task${index + 1}`,
title: `Task ${index + 1}`,
note: `Some things to note about task ${index + 1}`,
isCompleted: false,
})),
showNewTaskDialog: false,
}),
};
</script>
...
Using the v-model
directive we set up a two-way binding between the current open/close state of the dialog and a new showNewTaskDialog
variable we’ve created.
We use v-card
to create the body of the dialog and set its title using v-card-title
, which we’ve styled using some Vuetify classes.
Instead of manually setting showNewTaskDialog
to true
in the @click
event handler of the FAB as we did above, we could do it automatically by placing the FAB in the activator
slot of v-dialog
. Doing this gives the FAB access to two slot props, on
and attrs
, which are set the props and events of the FAB (using v-on
and v-bind
):
src/App.js<template>
<v-app>
...
<v-dialog v-model="showNewTaskDialog" width="500">
<template v-slot:activator="{ on, attrs }">
<v-btn fab fixed right bottom color="primary" v-on="on" v-bind="attrs">
<v-icon>mdi-plus</v-icon>
</v-btn>
</template>
<v-card>
<v-card-title class="text-h5 grey lighten-2">Add New Task</v-card-title>
</v-card>
</v-dialog>
</v-app>
</template>
...
Right now here’s what shows up when a user clicks the FAB:
Creating the form
We’ll construct the form using the Vuetify v-form
component. To get input for the title, we’ll set up another two-way binding, this time between a new v-text-field
component and the title
property of a new object we created for temporarily holding the new task data (newTask
):
src/App.js<template>
<v-app>
...
<v-dialog v-model="showNewTaskDialog" width="500">
<template v-slot:activator="{ on, attrs }">
<v-btn fab fixed right bottom color="primary" v-on="on" v-bind="attrs">
<v-icon>mdi-plus</v-icon>
</v-btn>
</template>
<v-card>
<v-card-title class="text-h5 grey lighten-2">Add New Task</v-card-title>
<v-form class="mx-4 mt-4">
<v-text-field
v-model="newTask.title"
label="Title"
required
></v-text-field>
</v-form>
</v-card>
</v-dialog>
</v-app>
</template><script>
export default {
name: 'App',
data: () => ({
...
newTask: {
title: '',
},
}),
};
</script>
...
Let’s use v-textarea
to create a text area for getting input for a task note since we expect it to be fairly large in size.
src/App.js...
<v-card>
<v-card-title class="text-h5 grey lighten-2">Add New Task</v-card-title>
<v-form class="mx-4 mt-4">
<v-text-field
v-model="newTask.title"
label="Title"
required
></v-text-field>
<v-textarea label="Note" v-model="newTask.note"></v-textarea>
</v-form>
</v-card>
</v-dialog>
</v-app>
</template><script>
export default {
name: 'App',
data: () => ({
...
newTask: {
title: '',
note: '',
},
}),
};
</script>
...
We can alter the design of text areas and text fields in Vuetify. There are 3 variants for text input: the non-box (default), the filled, and the outlined variant (the non-box variant has been deprecated and is no longer mentioned in the official Material Design guidelines, nevertheless Material Design frameworks such as Vuetify continue to support it). Let’s tweak it:
src/App.js
...
<v-card>
<v-card-title class="text-h5 grey lighten-2">Add New Task</v-card-title>
<v-form class="mx-4 mt-4">
<v-text-field
v-model="newTask.title"
label="Title"
required
outlined
></v-text-field>
<v-textarea label="Note" v-model="newTask.note" outlined></v-textarea>
</v-form>
</v-card>
</v-dialog>
</v-app>
</template>
...
Creating the form buttons
The form will need two buttons: one to cancel the adding of the new task and close the dialog, and another to submit the form input and create the task. The Cancel
button will be of the plain variant, while the Add
button will be raised:
src/App.js...
<v-card>
<v-card-title class="text-h5 grey lighten-2">Add New Task</v-card-title>
<v-form class="mx-4 mt-4 pb-4">
<v-text-field
v-model="newTask.title"
label="Title"
required
outlined
></v-text-field>
<v-textarea label="Note" v-model="newTask.note" outlined></v-textarea>
<div class="d-flex justify-end">
<v-btn plain class="mr-2">Cancel</v-btn>
<v-btn color="primary">Add</v-btn>
</div>
</v-form>
</v-card>
</v-dialog>
</v-app>
</template>
...
Notice we used some Vuetify flex helper classes, to align the two buttons to the right. We also added some space in between them with the mr-2
helper class:
Resetting form input
In addition to closing the dialog when the Cancel
button is clicked, we’ll also need to clear all the form inputs and reset any validation errors. To do this we can use the reset()
method, accessible by setting a ref on the form component:
src/App.js...
<v-card>
<v-card-title class="text-h5 grey lighten-2">Add New Task</v-card-title>
<v-form class="mx-4 mt-4 pb-4" ref="form">
<v-text-field
v-model="newTask.title"
label="Title"
required
outlined
></v-text-field>
<v-textarea label="Note" v-model="newTask.note" outlined></v-textarea>
<div class="d-flex justify-end">
<v-btn plain class="mr-2" @click="cancelButtonClick">Cancel</v-btn>
<v-btn color="primary">Add</v-btn>
</div>
</v-form>
</v-card>
</v-dialog>
</v-app>
</template><script>
export default {
name: 'App',
data: () => ({
...
}),
methods: {
cancelButtonClick() {
this.showNewTaskDialog = false;
this.$refs.form.reset();
}
}
};
</script>
...
Validating form input on submit
Now let’s ensure our form data is valid before creating the task. We set the lazy-validation
property on the form, which means that its inputs won’t be checked for validity until we call the validate()
method on the form ref. Also, we listen for the submit
event and use the .prevent
event modifier from Vue to stop the page from being reloaded when submission occurs.
src/App.js...
<v-form
class="mx-4 mt-4 pb-4"
ref="form"
@submit.prevent="newTaskFormSubmit"
lazy-validation
>
<v-text-field
v-model="newTask.title"
label="Title"
required
outlined
:rules="titleRules"
></v-text-field>
<v-textarea label="Note" v-model="newTask.note" outlined></v-textarea>
<div class="d-flex justify-end">
<v-btn plain class="mr-2" @click="cancelButtonClick">Cancel</v-btn>
<v-btn color="primary" type="submit">Add</v-btn>
</div>
</v-form>
</v-card>
</v-dialog>
</v-app>
</template><script>
export default {
name: 'App',
data: () => ({
...
newTask: {
title: '',
note: '',
},
titleRules: [(value) => Boolean(value) || 'Enter a title'],
}),
methods: {
cancelButtonClick() {
this.showNewTaskDialog = false;
this.$refs.form.reset();
},
newTaskFormSubmit() {
if (this.$refs.form.validate()) {
// add new task
}
},
},
};
</script>
...
Text input components in Vuetify carry out validation with a rules
prop. This prop takes a mixed array, whose elements can be functions, strings, or booleans. Each of the elements represents a rule. The single function we create in the array here takes the current value inputted and returns a boolean or string. If the return value of the function is true
(which in this case would mean a title has been entered and the input string is no longer truthy), then the validation is adjudged to be successful. But if the function’s return value is false
or a string (containing an error message), then the validation fails and the form input enters into an error state.
So when the user does not enter in a title and clicks Add
:
Adding a task on successful validation
What’s left is to add a new item to tasks
, close the dialog, and reset the form inputs after a successful validation:
src/App.js<template>
<v-app>
<v-card>
<v-toolbar color="primary" elevation="3" dark rounded="0">
<v-toolbar-title>Tasks</v-toolbar-title>
</v-toolbar>
</v-card>
<v-card class="ma-4" v-show="tasks.length > 0"> <!-- Hide card if there are no tasks -->
...
</v-card>
...
</v-app>
</template><script>
import { v4 } from 'uuid'; // Add the "uuid" module for random task ID generationexport default {
name: 'App',
data: () => ({
tasks: [], // We don't need sample data anymore
showNewTaskDialog: false,
newTask: {
title: '',
note: '',
},
titleRules: [(value) => Boolean(value) || 'Enter a title'],
}),
methods: {
cancelButtonClick() {
this.showNewTaskDialog = false;
this.$refs.form.reset();
},
newTaskFormSubmit() {
if (this.$refs.form.validate()) {
this.tasks.push({
id: v4(),
title: this.newTask.title,
note: this.newTask.note,
isCompleted: false,
});
this.showNewTaskDialog = false;
this.$refs.form.reset();
}
},
},
};
</script>
...
To be continued…
Today we learned how to use FABs, dialogs, forms, text input, and buttons to allow users to create their own tasks. We also encountered some useful form methods from Vuetify for validating and resetting input.
Stay tuned for our next episode, as together we build this to-do list app all the way from start to finish using this fantastic Material Design framework.
More content at plainenglish.io. Sign up for our free weekly newsletter. Get exclusive access to writing opportunities and advice in our community Discord.