Creating a todo -app using VueJS and Framework7

In this tutorial, I will walk you through the steps of writing a simple app to create and control todo -lists. We will be using VueJS (v2.6.10) and Framework7 (v4.2.0), so basic knowledge of them is recommended.

To start off, please clone the core branch of my repository from https://github.com/nataliasulkama/todo-tutorial to a new folder:

Main view for our app :)
mkdir todo
cd todo
git clone -b core --single-branch https://github.com/nataliasulkama/todo-tutorial

Then go in the newly created project folder, install dependencies and start the development server:

cd todo-tutorial
npm install
npm start

We’re working with a 2-tab app with a side panel. We will implement:

  1. A homepage where the user can check and uncheck tasks, and delete the selected list
  2. A separate page to create todo -lists
  3. A side panel on the homepage to switch between created lists.

Open the project folder in your code editor — all the files we will make changes to are in the components folder (app.vue, home.vue, createlist.vue).

Note: Sometimes when making changes to app.vue, Framework7 re-initializes itself and the app stops working. You will get an error in the console. If this happens, just refresh the page.

List creation

Let’s first work on creating new lists. Have a look at createlist.vue. To successfully read form inputs, we need three things:

  1. A list with an id (mine is “list-form”)
  2. List inputs with “name” attributes (in this case, “listname” and “listitem”)
  3. A button outside the list that fires the method to get input data.

These are all already implemented in the form:

<f7-block>
<!-- List needs an id -->
<f7-list no-hairlines-md id=”list-form”>
<!-- List inputs need names -->
<f7-list-input v-on:input=”validateForm” name=”listname” type=”text” placeholder=”List name” clear-button required validate>
</f7-list-input>
<f7-list-input name=”listitem” type=”text” placeholder=”What needs to be done?”>
</f7-list-input>
</f7-list>
<!-- Button to get inputs is outside the list -->
<f7-row class=”button-row”>
<f7-col>
<f7-button style=”max-width: 150px;” fill @click=”createList” :disabled=”disabled == 1 ? true : false”>
Create list
</f7-button>
</f7-col>
<f7-col>
<!-- Button to add multiple tasks to the list -->
<f7-button style=”max-width: 120px;” raised @click=”addTask”>
+ Add task
</f7-button>
</f7-col>
</f7-row>
</f7-block>

First, we should write a method that is called when we click the “Create list” -button. What this method will do is read the user input and fill the createdList and listItems data properties with the input. In addition, we want to use the listid property to add an id to each created list. All of these are already created in the script:

<script>
export default {
data() {
return {
listName: ‘’,
listItems: [],
listItem: {},
createdList: {},
lists: [],
listid: 0,
disabled: 1
}
},
...
}
</script>

Our method will look like this:

IMPORTANT: Framework7’s VueJS components do not support v-model. So instead, we need to use Framework7’s custom DOM library, Dom7.

Here we read the form inputs with this.$f7.form and call a function convertToData to convert the inputs into a readable format. Then, we store the inputs, reset the form by emptying the fields (using Dom7 to target inputs that have a value), and reset the data properties. We will push the created list to an array of lists so that we can store them all.

This method is bound to the @click event in the button:

<f7-button style=”max-width: 150px;” fill @click=”createList” :disabled=”disabled == 1 ? true : false”>
Create list
</f7-button>

The :disabled property disables the button conditionally depending on whether the value of the disabled data property is 1 or 0. This is determined in the pre-created validateForm method.

...
methods: {
validateForm: function() {
let formData = this.$f7.form.convertToData(‘#list-form’);
this.listName = formData.listname;
if (formData.listname == ‘’) {
this.disabled = 1;
} else if (this.listItems.length == 0) {
this.disabled = 1;
} else {
this.disabled = 0;
}
},
...

The button will be disabled if the list name input is empty, or if the listItems property is empty (no tasks are added). So now, we need to create the addTask method that’s run when we click the “Add task” button.

<f7-button style=”max-width: 120px;” raised @click=”addTask”>
+ Add task
</f7-button>

So now if you write something in the task input and click the “add task” button, it will push your input to the listItems array, and you can keep creating as many tasks as you’d like.


List preview

Now that our list creation is working, we’ll want to actually see what our list looks like. We’ll implement a list preview below the form and buttons:

<!-- Render the list name -->
<h2 id=”list-name-preview”> {{ listName }} </h2>
<f7-list inset no-hairlines-md id=”tasks-preview”>
<!-- Dynamically rendering the list items using v-for -->
<f7-list-item id=”list-item-preview” v-for=”(listItem, index) in listItems”>
{{ index+1 }}. {{ listItem.item }}

<!-- Icon span with a @click to remove tasks -->
<span @click=”removeTodo(index)”>
<f7-icon f7=”close” size=”25px” color=”red”></f7-icon>
</span>
</f7-list-item>
</f7-list>

We are using v-for to dynamically print the tasks as they are added. The pre-created validateForm method stores the list name input to the listName property, so now the preview updates on input.

validateForm: function() {
let formData = this.$f7.form.convertToData(‘#list-form’);
this.listName = formData.listname;
...
}

To remove tasks in the preview, we’ve got a span element with the removeTodo method that fires on click. The method is simple:

removeTodo(selectedItem) {
this.listItems.splice(selectedItem, 1);
  // If the listItems array is empty, prevent list creation
if (this.listItems.length <= 0) {
this.disabled = 1;
}
}

Because we are printing the list items dynamically with v-for, we don’t need to do anything else.


Emitting list to app.vue

Next, we need a way to display the list on the homepage automatically as it’s created. On the homepage we will want to check and uncheck tasks, and delete the list.

First, we need to emit the created list to app.vue. In the createList method of createlist.vue, add this at the end:

...
createList: function() {
let formData = this.$f7.form.convertToData(‘#list-form’);
if (formData.listname != ‘’ && formData.listname != undefined && this.listItems.length != 0) {
...
// Add the following:
this.$emit(‘created’, this.createdList);
this.$f7.tab.show(tab1, true);
} else {
return false;
}
},
...

This will emit a custom created event and pass the createdList data property to the parent which in this case is app.vue. Then it will change the view to the home tab.

In app.vue, we need to make a method to read the emitted data. We should bind the created event to the create-list element:

<f7-view main class=”safe-areas” url=”/”>
<f7-page :page-content=”false”>
...
<f7-tabs class=”tabs” swipeable>
...
<f7-tab id=”tab2" class=”page-content”>
<create-list @created=”getList”></create-list>
</f7-tab>
</f7-tabs>
</f7-page>
</f7-view>

This means that we are listening for the created event that is emitted from the createlist.vue component, and when the parent (app.vue) detects that event, it will fire the getList method, which looks like this:

methods: {
// Here, the "list" parameter is the createdList data property that we passed from createlist.vue
getList: function(list) {
// Pass the parameter to a data property
this.todoList = {
id: list.id,
name: list.name,
items: list.items,
checked: []
};
    // Add the created list to an array of lists, so we can display it on the side panel
this.lists.push(this.todoList);
},
...
}

Remember: editing app.vue will reinitialize Framework7 and it will stop working. If this happens, refresh the page.

After making this method and adding a todoList data property, if we console.log this.todoList or this.lists, we can see that the object has been emitted successfully and we can now pass them both forward to home.vue as props to use later.

<home-page :todoList=”todoList” :lists=”lists”></home-page>

Side panel

In app.vue, let’s display the created lists in the side panel. Near the top of the template, in the f7-panel element, let’s create a list element:

Here we are displaying the number of lists we have, and then dynamically generating a list view of all existing lists. We also want to have a quick glance at how many tasks the list has and how many of them are checked done. Clicking the list items will fire a method selectList, which looks like this:

methods: {
getList: function(list) {
...
},
selectList: function(selection) {
this.todoList = selection;
this.$f7.panel.close();
},

...
}

For now, nothing visible will happen as the method only stores the selected list in the todoList data property and closes the side panel. We’ll move on to the homepage view now to implement the main list view and functions.

5. List display in home.vue, checking tasks

Since we already passed the todoList and lists props in app.vue, like this:

<home-page :todoList=”todoList” :lists=”lists”></home-page>

All we need to do in home.vue now, is receive them:

<script>
export default {
data() {
return {
listSelected: false
}
},
props: [‘todoList’, ‘lists’],
...
</script>

Now we can use them right away to render our list:

Here we are printing the homepage content conditionally. If no lists exist, it will display a message, and if one or more exist, they will be printed with the checked and unchecked tasks separated. Since the homepage is where we use the props, we should create computed properties out of them. In Vue, you should never mutate props directly. Instead, we can mutate the computed properties.

<script>
export default {
data() {
...
},
props: [‘todoList’, ‘lists’],
computed: {
activeList: function() {
return this.todoList;
},
listArray: function() {
return this.lists;
},
uncheckedTasks: function() {
return this.activeList.items.filter(function(item) {
return !item.done;
})
},
checkedTasks: function() {
return this.activeList.items.filter(function(item) {
return item.done;
})
}

},
...
}
</script>

Now we can start writing the methods to mutate our list without touching the props directly. However, first let’s set a watcher to the todoList prop. This means that we can call a function whenever the value of the prop changes.

computed: {
...
},
watch: {
todoList: function(newList, oldList) {
if (Object.keys(newList).length != 0 && newList.constructor === Object) {
this.listSelected = true;
}
}

},
...

In this function we check if the new value of the prop is empty or not — so when a list is selected, this.listSelected will change to true and the list will be displayed on the homepage tab.

Now we need methods for checking tasks and deleting the displayed list:

...
methods: {
checkTask: function(task) {
task.done = !task.done;
this.$emit(‘checked’, this.checkedTasks);
},
deleteList: function(list) {
// Find the index of the list to delete (so we do not delete lists with the same name, for example)
let listToDelete = this.listArray.indexOf(list);
this.listArray.splice(listToDelete, 1);
    // Go back to default view ("no list is selected")
this.listSelected = false;
}
}

We’re emitting a checked custom event as we check tasks, so now in app.vue let’s listen for it:

<f7-tabs class=”tabs” swipeable>
<f7-tab id=”tab1" class=”page-content” tab-active>
<home-page :todoList=”todoList” :lists=”lists” @checked=”checkedTasks”></home-page>
</f7-tab>
...
</f7-tabs>

And the last thing we need to do is add the checkedTasks method:

methods: {
getList: function(list) {
...
},
selectList: function(selection) {
...
},
checkedTasks: function(list) {
this.todoList.checked = list;
}
}

And with that — we are done. For the finished code, you can check out my repository: https://github.com/nataliasulkama/todo-tutorial.

Thank you for reading! 🌸