Simple Todo App with Vue, Google Firestore, and Google Authentication
Published: 2021-01-29 02:35:26

![Demo](https://dev-to-uploads.s3.amazonaws.com/i/4dwaupinzc0hdy9eoo1x.gif)

# Introduction

I find the fastest and easiest learning any programming language by doing the meaning projects. By doing a real project, you can benefit from the design, manage the project from start to end. In this article, we will learn to build an application using [Vue](https://vuejs.org/v2/guide/) and state management for Vue application by using [Vuex](https://vuex.vuejs.org/)

We will learn the following in this article
1. Create a project by using Vue CLI (version 2)
2. Create and use the Vue component
3. Using Bootstrap Vue to style the component
4. Using Vuex for state management
5. Authentication with Google firebase authentication
6. CRUD events with Google fire store

# Source Code
You can find the full source from the [GitHub](https://github.com/thangdc1982/todoapp-vue)

# Prerequiestes
* Install Visual Studio Code (VS Code) from [here](https://code.visualstudio.com/download)
* Install NodeJS from [here](https://nodejs.org/en/download/)
* Install the Vue CLI from [here](https://cli.vuejs.org/guide/installation.html)
```
npm install -g @vue/cli
```

# Create project
Make sure you have the Vue Command Line Interface (CLI) installed on your machine. You can install Vue CLI by run the command below in your terminal command

Create a todo application project
```
vue create todo-app
```
The command above will create a new Vue project with the name `todo-app`

Change the directory to the new project
```
cd todo-app
```

Install Vuex
```
npm install vuex --save
```

Install Vue Bootstrap
```
npm install bootstrap bootstrap-vue --save
```

Install firebase
```
npm install firebase --save
```

### Compiles and hot-reloads for development
```
npm run serve
```

### Compiles and minifies for production
```
npm run build
```

### Lints and fixes files
```
npm run lint
```

### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

# Open the project in VS Code
Open the project in VS Code
```
code .
```
You will see the default file structure generated by Vue CLI
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/u4tv8aplvtwr6quc2uow.png)

We will delete the HelloWorld.vue component for now. We will add our components later.

Open the terminal in the VS Code by pressing the combination key (Ctrl + J or CMD + J) 

# Create firebase.js
Firebase will provide the authentication and database events (CRUD)

Please make sure you already configured firebase project from [Google Firebase](https://console.firebase.google.com)

Now create a new named `firebase.js` under the `src` directory

Copy the following code to the file
```JavaScript
import firebase from 'firebase';
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
  // Add your firebase config here
};

const firebaseapp = firebase.initializeApp(firebaseConfig);

// database
const db = firebaseapp.firestore();

// authentication
const auth = firebase.auth();
const provider = new firebase.auth.GoogleAuthProvider();

export { auth, provider };
export default db;
```

Please make sure you modify the configuration `firebaseConfig` with yours

# Create store object for state management
We want to have all the states located in the store directory under the source (src) directory.
Now we create a new folder named store under the src folder.

### Create user state (store)
User state to manage the user who signs in/out by using Google firebase. It will control the user signed in or signed out. And the user interface will be updated based on the user status.

Create a new JavaScript file named `user.js` under the store directory.

Copy the following code to the file
```JavaScript
const state = {
  user: null
};

const getters = {
  user: state => state.user
};

const actions = {
  signIn ({commit}, user) {
    commit('signIn', user);
  },
  signOut({commit}) {
    commit('signOut');
  }
};

const mutations = {
  signIn: (state, user) => {
    state.user = user;
  },
  signOut: state => state.user = null
};

export default {
  state,
  getters,
  actions,
  mutations
};
```

### Create todos state (store)
Todos state to manage the todo list and todo item. Todos state will controll
1. Loading data from Google firestore database
2. Adding data to firestore database
3. Update data to firestore database
4. Delete data from firestore database

Create a new JavaScript file named `todos.js` under the store directory.

Copy the following code to the file

```JavaScript
import db from '../firebase'

// State declaration
const state = {
  /**
   * each todo item will have the format
   * {
   *  id: String,
   *  title: String,
   *  completed: Boolean
   * }
   */
  todos: []
};

// Getters
const getters = {
  // Get all todo items in the current state
  allTodos: state => state.todos
};

// actions
const actions = {
  /**
   * Loading todos   
   * This is fetch data from database
   */
  fetchTodos ({commit}, user) {        
    db.collection('user')
      .doc(user.uid)
      .collection('todos')
      .onSnapshot(snapshot => {
        commit('setTodos', snapshot.docs.map(doc => {
          return {
            id: doc.id,
            doc: doc.data()
          }
        }));
      })    
  },
  /**
   * Add a new todo action
   * This is fired when the user submit the form add a new todo 
   * @param {String} payload - The title string of the the new todo
   */
  addTodo({ commit}, payload) {
    const uid = payload.user.uid;
    const title = payload.title;

    const newtodo = {
      title,
      completed: false
    };

    db.collection('user')
      .doc(uid)
      .collection('todos')
      .add(newtodo)
      .then(docRef => {
        commit('addTodo', {
          id: docRef.id,
          data: newtodo
        });
      }) 
  },
  /**
   * Update the status of the todo item in the state
   * @param {Object} payload - The object contains
   * - the id to identify the todo item in the collection list
   * - completed property to update the status of the todo item
   */
  toggleCompleted ({commit}, payload) {
    const uid = payload.user.uid;
    const id = payload.id;
    const completed = payload.completed;
    db.collection('user')
      .doc(uid)
      .collection('todos')
      .doc(id)
      .update({
        completed: completed
      })
    commit('toggleCompleted', {
      id,
      completed
    });
  },
  /**
   * 
   * @param {*} param0 
   * @param {Number} payload - The number id to identify the todo item in the collection
   */
  deleteTodo ({commit}, payload) {
    const uid = payload.user.uid;
    const id = payload.id;
    db.collection('user')
      .doc(uid)
      .collection('todos')
      .doc(id)
      .delete();
    commit('deleteTodo', id);
  }
};

// mutations
const mutations = {
  setTodos: (state, todos) => {    
    state.todos = todos.map(todo => {
      return {
        id: todo.id,
        title: todo.doc.title,
        completed: todo.doc.completed
      }
    });
  },
  addTodo: (state, {id, data}) => {      
    console.log('addTodo');
    const newtodo = {
      id,
      title: data.title,
      completed: data.completed
    };  
    const oldTodos = [...state.todos];
    const index = oldTodos.findIndex(todo => todo.id === id);    
    // adding new todo the list
    if (index < 0) {
      state.todos.unshift(newtodo); // = [newtodo, ...state.todos];
    }
    
    console.log(state.todos);
  },
  toggleCompleted: (state, {id, completed}) => {
    const index = state.todos.findIndex((todo) => todo.id === id);
    if (index >= 0) {
      state.todos[index].completed = completed;
    }
  },
  deleteTodo: (state, id) => {
    const index = state.todos.findIndex((todo) => todo.id === id);
    if (index >= 0) {
      state.todos.splice(index, 1);
    }
  }
};

export default {
  state,
  getters,
  actions,
  mutations
};
```
# Create Vuex Store
Now we have states for both `user` and `todos`, we want to use them in the store collection

Create a new file named `store.js` under the `src` folder.

Copy the following code to the file
```JavaScript
// Create a store
import Vue from 'vue'
import Vuex from 'vuex'
import todos from './todos';
import user from './user';

Vue.use(Vuex);

// create store
export default new Vuex.Store({
  modules: {
    todos,
    user
  }
});
```

# Adding store to the project
Open the `main.js` to adding store to our application.

We also add the bootstrap to our application here

```JavaScript
import Vue from 'vue'
import App from './App.vue'
// Import store
import store from './store/store'
// Bootstrap
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'

// import bootstrap and BootstrapVue css
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

Vue.use(BootstrapVue);
Vue.use(IconsPlugin);

Vue.config.productionTip = false;

new Vue({
  render: h => h(App),
  store
}).$mount('#app')
```

# Create components
Now we have all the set-up. We will start to create our components for the application. Our application will have the following component
1. `Login` component - To show user logged in or logged out
2. `NewTodo` component - This is form component to add a new todo item
3. `TodoList` component - This is the list view to display each todo item
4. `TodoItem` component - This is the todo item to be display

## Create Login component
We create a new file `Login.vue` under the component folder. And copy the following script to the file
```JavaScript
<template>
  <div class="login">
    <div @click="logout" v-if="user" class="logout">
      <b-avatar variant="info" :src="imageSrc" class="mr-3"></b-avatar>
      <span class="mr-auto">{{user.user.displayName}}</span>      
    </div>
    <b-button v-else variant="primary" @click="login">Sign In</b-button>    
  </div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import { auth, provider } from '../firebase'

export default {
  name: 'Login',
  data() {
    return {
      imageSrc: ''
    };
  },
  computed: mapGetters(['user']),
  methods: {
    ...mapActions(['signIn', 'signOut']),
    login() {
      auth
      .signInWithPopup(provider)
      .then(authUser => {
        this.imageSrc = authUser.user.photoURL;
        this.signIn(authUser);
      })
      .catch(error => alert(error.message))      
    },
    logout() {
      this.signOut();
    }
  }
}
</script>
<style>
.login {
  text-align: center;
}

.logout {
  cursor: pointer;
}
</style>
```

## Create NewTodo component
We create a new file `NewTodo.vue` under the component folder. And copy the following script to the file
```JavaScript
<template>
  <b-form @submit="AddNewTodo">
    <b-form-group
      label="Add new todo"
      label-for="newTodo"
    >
      <b-form-input
        id="newTodo"
        placeholder="What needs to be done"
        v-model="todo"
        required
      >        
      </b-form-input>
    </b-form-group>
    <b-button type="submit" variant="primary">Add New Todo</b-button>
  </b-form>
</template>
<script>
// import actions
import { mapGetters, mapActions } from 'vuex'

export default {
  name: "NewTodo",
  data() {
    return {
      // data for binding to the input text
      todo: ''
    }
  },
  computed: mapGetters(['user']),
  methods: {
    ...mapActions(['addTodo']),
    /**
     * Form submit hanlder
     */
    AddNewTodo (event) {
      event.preventDefault();
      if (this.todo && this.todo !== "") {
        // update data base
        this.addTodo({
          user: this.user.user,
          title: this.todo
        })                  
        // clear todo
        this.todo = "";
      }      
    }
  }
}
</script>
<style>
  
</style>
```

## Create TodoItem component
We create a new file `TodoItem.vue` under the component folder. And copy the following script to the file.

`TodoItem` will take three properties from the parent (`TodoList`)
1. id - Identifier the item (this value is generated by firestore database)
2. title - This is string represent the item description
3. completed - This is a boolean represent the item has been completed or not
```JavaScript
<template>
  <b-form>
    <b-row>
      <b-col cols="11">
        <b-form-checkbox switch size="lg" :checked="completed" 
            @change="toggleComplete">                    
          <label :class="{complete: completed}">{{title}}</label>      
        </b-form-checkbox>       
      </b-col>      
      <b-col cols="1">
        <b-icon icon="trash" class="hover-trash" @click="removeTodo"></b-icon>
      </b-col>
    </b-row>
    
  </b-form>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'

export default {
  name: 'TodoItem',
  props: {
    id: String, // The todo item key
    title: String, // The title description of the todo item
    completed: Boolean // indicating if the todo item has been completed
  },
  computed: mapGetters(['user']),
  methods: {
    ...mapActions(['toggleCompleted', 'deleteTodo']),
    toggleComplete () {
      // change the status of the todo item
      this.toggleCompleted({
        user: this.user.user,
        id: this.id,
        completed: !this.completed
      })
    },
    removeTodo () {
      // delete the todo from the state collection
      this.deleteTodo({
        user: this.user.user,
        id: this.id
      });
    }
  }
}
</script>
<style scoped>
.complete {
  text-decoration: line-through;
}

.hover-trash:hover {
  color: red;
}

.row {
  border-bottom: 1px solid gray;
}
</style>
```

## Create NewTodo component
We create a new file `TodoList.vue` under the component folder. And copy the following script to the file

```JavaScript
<template>
  <b-container fluid class="ml-3">
    <h1>Todo list goes here</h1>
    <ul>
      <li v-for="todo in allTodos" :key="todo.id">
        <todo-item :title="todo.title" :completed="todo.completed" :id="todo.id" ></todo-item>        
      </li>
    </ul>
  </b-container>
</template>
<script>
import TodoItem from './TodoItem.vue'
// vuex actions and getters
import { mapGetters, mapActions } from 'vuex';

export default {
  name: 'TodoList',
  components: {
    TodoItem
  },
  computed: mapGetters(['allTodos', 'user']),
  methods: {
    ...mapActions(['fetchTodos'])
  },
  created() {
    this.fetchTodos(this.user.user);
  }
}
</script>
<style scoped>
ul li {
  list-style: none;
}
</style>
```

# Adding components to App component
Open the `App.vue` and update the code as follow
```JavaScript
<template>
  <div class="mt-5">
    <img alt="Vue logo" src="./assets/logo.png"> 
      <login></login>
      <div v-if="user">
        <b-container>        
          <new-todo></new-todo>      
        </b-container>
        <todo-list></todo-list>   
      </div>
  </div>
</template>

<script>
import NewTodo from './components/NewTodo.vue'
import TodoList from './components/TodoList.vue'
import { mapGetters } from 'vuex'
import Login from './components/Login.vue'

export default {
  name: 'App',
  computed: mapGetters(['user']),
  components: {
    NewTodo,
    TodoList,      
    Login
  }
}
</script>

<style>
* {
  margin: 0;
  padding: 0;
}

img {
  display: block;
  margin-left: auto;
  margin-right: auto;
}
</style>
```

# Build and deploy
Now we have completed all the components and have tested the application locally. it is time for build and deploy the application.

### Build the application
```
npm run build
```
This command will compile our application and generate all neccessary files under the folder `dist`. This is the folder will be deployed to google cloud hostting.

### Deploy
Please make sure you have `firebase-tools` installed globally
```
npm install -g firebase-tools
```

Now you will need to login
```
firebase login
```

Initalize firebase project
```
firebase init
```

Select the hosting option.

When be asked for `public directory`, type `dist`

When be asked for single-page app, type 'y'

Type `N` when asking for GitHub automation build.

Type `N` when be asked for overwrite the `index.html`. We want to keep our file

Then select the hosting project, you can select an existing one if you already created or create a new one.

Deploy the project
```
firebase deploy
```

Congrats!!! You have completed build the todo application from start to end, and successfully deploy the application.