DEV Community

Thang Cam Duong
Thang Cam Duong

Posted on

Simple Todo App with Vue, Google Firestore, and Google Authentication

Demo

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 and state management for Vue application by using Vuex

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

Prerequiestes

  • Install Visual Studio Code (VS Code) from here
  • Install NodeJS from here
  • Install the Vue CLI from here
npm install -g @vue/cli
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The command above will create a new Vue project with the name todo-app

Change the directory to the new project

cd todo-app
Enter fullscreen mode Exit fullscreen mode

Install Vuex

npm install vuex --save
Enter fullscreen mode Exit fullscreen mode

Install Vue Bootstrap

npm install bootstrap bootstrap-vue --save
Enter fullscreen mode Exit fullscreen mode

Install firebase

npm install firebase --save
Enter fullscreen mode Exit fullscreen mode

Compiles and hot-reloads for development

npm run serve
Enter fullscreen mode Exit fullscreen mode

Compiles and minifies for production

npm run build
Enter fullscreen mode Exit fullscreen mode

Lints and fixes files

npm run lint
Enter fullscreen mode Exit fullscreen mode

Customize configuration

See Configuration Reference.

Open the project in VS Code

Open the project in VS Code

code .
Enter fullscreen mode Exit fullscreen mode

You will see the default file structure generated by Vue CLI
Alt Text

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

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

Copy the following code to the file

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;
Enter fullscreen mode Exit fullscreen mode

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

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
};
Enter fullscreen mode Exit fullscreen mode

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

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
};
Enter fullscreen mode Exit fullscreen mode

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

// 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
  }
});
Enter fullscreen mode Exit fullscreen mode

Adding store to the project

Open the main.js to adding store to our application.

We also add the bootstrap to our application here

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')
Enter fullscreen mode Exit fullscreen mode

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

<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>
Enter fullscreen mode Exit fullscreen mode

Create NewTodo component

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

<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>
Enter fullscreen mode Exit fullscreen mode

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
<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>
Enter fullscreen mode Exit fullscreen mode

Create NewTodo component

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

<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>
Enter fullscreen mode Exit fullscreen mode

Adding components to App component

Open the App.vue and update the code as follow

<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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now you will need to login

firebase login
Enter fullscreen mode Exit fullscreen mode

Initalize firebase project

firebase init
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)