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.

Tari Ibaba
JavaScript in Plain English

--

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 generation
export 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>
...
A new task added with the form

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.

--

--