Skip to main content


By Muthu Kumaran - June 28, 2017

Vue.js - Todo App Using Vuex - Part 2

PART 2: With Components, and State Management.

Hope you have read VUE.JS - SIMPLE TODO APP - PART 1. It's a continuation which I'm gonna cover Todo APP with Components and State Management using Vuex

Components

Now we have built a Todo App. Let's using Vue Components to self-contain the app and make it reusable.

Vue Components

The component system is another important concept in Vue, because it’s an abstraction that allows us to build large-scale applications composed of small, self-contained, and often reusable components.

In Vue, a component is essentially a Vue instance with pre-defined options. Registering a component in Vue is straightforward:


// Define a new component called `my-component`
Vue.component('my-component', {
  template: '<li>This is a component</li>'
})
        

Now you can compose it in another component’s template:


<ol>
  <!-- Create an instance of the `my-component` component -->
  <my-component></my-component>
</ol>
      

This will render the text This is a component.

To learn more about Vue Components, Composing with Components

Todo Components

Let's modify the previous Todo App code to create components. I'm going to create two components, <todo-component> and <todo-list>.

<todo-component> - will have all Todo App markup and Todo List items (<todo-list>) will be a child component

<todo-list> - will hold Todo List items markup. Also used in the parent component <todo-component>

Let's update the markup


<script src="https://unpkg.com/vue"></script>

<div id="todoApp">
  <todo-component></todo-component>
</div>
      

Let's create <todo-component>.


Vue.component('todo-component', {
  template: `
    <div>
      <h3>{{message}}</h3>
      <form name="todo-form" method="post" action="" v-on:submit.prevent="addTask">
        <input name="add-todo" v-model="addTodo" type="text" v-bind:class="{error: hasError}"/>
        <button type="submit">Add</button>
      </form>

      <div class="todo-lists" v-if="lists.length">
        <h3>My Todo Tasks</h3>
        <ul>
          <todo-list v-for="list in filterLists" v-bind:todo="list" v-bind:key="list.id"></todo-list>
        </ul>
      </div>

    </div>`
});
      

<todo-list> component,


Vue.component('todo-list', {

  props: ['todo'],

  template: `
    <li>
      <input type="checkbox" v-on:change="completeTask(todo)" v-bind:checked="todo.isComplete"/>
      <span class="title" contenteditable="true" v-on:keydown.enter="updateTask($event, todo)" v-on:blur="updateTask($event, todo)" v-bind:class="{completed: todo.isComplete}">{{todo.title}}</span>
      <span class="remove" v-on:click="removeTask(todo)">x</span>
    </li>`
});
      

props - Every component instance has its own isolated scope. This means you cannot (and should not) directly reference parent data in a child component’s template. Data can be passed down to child components using props. A prop is a custom attribute for passing information from parent components. A child component needs to explicitly declare the props it expects to receive using the props option


// <todo-list> from parent component
<todo-list v-for="list in filterLists" v-bind:todo="list" v-bind:key="list.id"></todo-list>
      

Where we bind v-bind:todo and v-bind:key which are passed down to child components using props (props: ['todo', 'key']).

To learn more about Vue Components.

Vuex

Vuex is a state management pattern + library for Vue.js applications. It serves as a centralized store for all the components in an application, with rules ensuring that the state can only be mutated in a predictable fashion. Inspired by Flux, Redux and The Elm Architecture

To use Vuex, include the below script after Vue and it will install itself automatically:


<script src="https://unpkg.com/vue"></script>
<script src="https://unpkg.com/vuex"></script>
    

First, we'll need to create a store. A store is essentially a global reactive object which follows the normal Vue reactivity patterns. It cannot be accessed or modified directly in order to ensure a consistent state and allow easy tracking of changes. Here's a sample store,


const store = new Vuex.Store({

  state:{

  },
  getters:{ //similar to computed but this will cache for reuse

  },
  mutations:{

  },
  actions: {

  }

});
    

1. state

This is where you define your data structure for your app. You can also set initial state here. For the Todo App, initial state should be,


  state:{
        message: 'Welcome to Todo App with State Management',
        lists: [],
        hasError: false
  },
    

Yes, the data structure is similar to PART1: Simple Todo App.

To access the store in your components, use this.$store. So, this.$store.state.message will return Welcome to Todo App with State Management.

2. getters

Vuex allows us to define "getters" in the store. You can think of them as computed properties for stores. Like computed properties, a getter's result is cached based on its dependencies, and will only re-evaluate when some of its dependencies have changed.

Getters will receive the state as their 1st argument:


getters:{
      filterLists: function(state){
        return _.orderBy(state.lists, ['isComplete', false])
      }
},
    

Now we have movedfilterLists to the Vuex store. In components, it can be accessed through this.$store.getters.filterLists

3. mutations

The only way to actually change state in a Vuex store is by committing a mutation. Vuex mutations are very similar to events: each mutation has a string type and a handler. The handler function is where we perform actual state modifications, and it will receive the state as the first argument and payload as second argument

In most cases, the payload should be an object so that it can contain multiple fields, and the recorded mutation will also be more descriptive.

For the Todo App, we will move all existing methods to the mutations,


mutations:{

      addTask: function(state, payload){
        if(!payload.title){
          state.hasError = true;
          return;
        }

        state.hasError = false;
        state.lists.push({id:state.lists.length+1, title: payload.title, isComplete: false});
      },

      updateTask: function(state, payload){
        var i = _.findIndex(state.lists, payload.list);
        if(!state.lists[i]) return;

        if(payload.targetElem.innerText.trim() !== ''){
          state.lists[i].title = payload.targetElem.innerText;
        }else{
          payload.targetElem.innerText = state.lists[i].title;
        }

        payload.targetElem.blur();
      },

      completeTask: function(state, payload){
        var index = _.findIndex(state.lists, payload.list);
        state.lists[index].isComplete = !state.lists[index].isComplete;
      },

      removeTask: function(state, payload){
        var index = _.findIndex(state.lists, payload.list);
        state.lists.splice(index, 1);
      }
},
    

In the component, you cannot directly call a mutation handler. Think of it more like event registration: "When a mutation with type addTask is triggered, call this handler." To invoke a mutation handler, you need to call this.$store.commit with its type:

this.$store.commit('addTask')

To commit with payload then it should be,


      this.$store.commit('updateTask', {targetElem: e.target, list: list});
    

One important rule to remember is that mutation handler functions must be synchronous. It's cannot handle asynchronous operations. To handle asynchronous operations, let's use Actions.

4. actions

Actions are similar to mutations, the differences being that:

Action handlers receive a context object which exposes the same set of methods/properties on the store instance, so you can call context.commit to commit a mutation, or access the state and getters via context.state and context.getters.


  actions: {
        addTask: function(context, payload){
          context.commit('addTask', payload);
        },

        updateTask: function(context, payload){
          context.commit('updateTask', payload);
        },

        completeTask: function(context, payload){
          context.commit('completeTask', payload);
        },

        removeTask: function(context, payload){
          context.commit('removeTask', payload);
        }
  }
    

Actions are triggered with the this.$store.dispatch method:

this.$store.dispatch('addTask', {title: this.addTodoInput});

This may look dumb at first sight: if we want to add a Todo item, why don't we just call this.$store.commit('addTask') directly? Well, remember that mutations must be synchronous? Actions don't. We can perform asynchronous operations inside an action

5. Modules

Modules are useful when you are working on big projects. Todo is a small app and we are not using modules. You can learn from here, Modules.

Todo App Store

Let's put all the pieces together and the Todo App store will be:


const store = new Vuex.Store({
  state:{
        message: 'Welcome to Todo App with State Management',
        lists: [],
        hasError: false
  },
  getters:{ //similar to computed but this will cache for reuse
        filterLists: function(state){
          return _.orderBy(state.lists, ['isComplete', false])
        }
  },
  mutations:{
        addTask: function(state, payload){

          if(!payload.title){
            state.hasError = true;
            return;
          }

          state.hasError = false;
          state.lists.push({id:state.lists.length+1, title: payload.title, isComplete: false});
        },

        updateTask: function(state, payload){
          var i = _.findIndex(state.lists, payload.list);
          if(!state.lists[i]) return;//close button and content-editable are close-by and both are triggered at once causes JS error.
          if(payload.targetElem.innerText.trim() !== ''){
            state.lists[i].title = payload.targetElem.innerText;
          }else{
            payload.targetElem.innerText = state.lists[i].title;
          }
          payload.targetElem.blur();
        },

        completeTask: function(state, payload){
          var index = _.findIndex(state.lists, payload.list);
          state.lists[index].isComplete = !state.lists[index].isComplete;
        },

        removeTask: function(state, payload){
          var index = _.findIndex(state.lists, payload.list);
          state.lists.splice(index, 1);
        }
  },
  actions: {

        addTask: function(context, payload){
          context.commit('addTask', payload);
        },

        updateTask: function(context, payload){
          context.commit('updateTask', payload);
        },

        completeTask: function(context, payload){
          context.commit('completeTask', payload);
        },

        removeTask: function(context, payload){
          context.commit('removeTask', payload);
        }
  }

})
    

Inject store into Todo App

Now we have a store ready and we need to find a way to inject it into the Todo App. Vuex provides a mechanism to "inject" the store into all child components from the root component with the store option.

Let's update the Todo app Vue constructor to inject the store:


var todoApp = new Vue({
  el: '#todoApp',
  store
});
    

By providing the store option to the root instance, the store will be injected into all child components of the root and will be available on them as this.$store.

Updating Components

Let's update the components to dispatch the actions.

todo-component Component


Vue.component('todo-component', {
  template: `
        <div><h3>{{message}}</h3>
          <form name="todo-form" method="post" action="" v-on:submit.prevent="addTask">
            <input name="add-todo" v-model="addTodo" type="text" v-bind:class="{error: hasError}"/>
            <button type="submit">Add</button>
          </form>

          <div class="todo-lists" v-if="lists.length">
            <h3>My Todo Tasks</h3>
            <ul>
              <todo-list v-for="list in filterLists" v-bind:todo="list" v-bind:key="list.id"></todo-list>
            </ul>
          </div>

        </div>
  `,

  data: function(){
        return{
          addTodoInput: ''
        }
  },

  computed: {
        message: function(){
          return this.$store.state.message
        },
        lists: function(){
          return this.$store.state.lists
        },
        hasError: function(){
          return this.$store.state.hasError
        },
        filterLists: function(){
          return this.$store.getters.filterLists;
        }
  },

  methods: {
        addTask: function(){
          this.$store.dispatch('addTask', {title:this.addTodoInput});
          this.addTodoInput = "";
        }
  }
});
    

Here you might have notice two things,

1. data - Must be a function. Dont' get confused with Vue constructor data which is an object.

2. addTodoInput - It's a Vue model and I don't want this to be part of store.

todo-list Component

Now we are updating todo-list component to dispatch actions.


Vue.component('todo-list', {
  props: ['todo'],

  template: `
        <li>
          <input type="checkbox" v-on:change="completeTask(todo)" v-bind:checked="todo.isComplete"/>
          <span class="title" contenteditable="true" v-on:keydown.enter="updateTask($event, todo)" v-on:blur="updateTask($event, todo)" v-bind:class="{completed: todo.isComplete}">{{todo.title}}</span>
          <span class="remove" v-on:click="removeTask(todo)">x</span>
        </li>
  `,

  methods:{
        removeTask: function(list){
          this.$store.dispatch('removeTask', {list});
        },

        updateTask: function(e, list){
          e.preventDefault();
          this.$store.dispatch('updateTask', {targetElem:e.target, list});
        },

        completeTask: function(list){
          this.$store.dispatch('completeTask', {list})
        }
  }
});
    

Final Piece

Finally, we have come to end. Now the simple Todo app is componentized and using Vuex to manage the states.

Complete HTML, CSS, and JavaScript code for Todo App will be available at my CodePen.

Here's the Todo App demo:

See the Pen Todo App with Vuex - State Management - Vue.js by Muthu Kumaran (@mkumaran) on CodePen.



If you like this, please share



Comments


Thank you for visiting my page. Please shares your views and suggestion in the comment box below.