Vue on Django, Part 2

Ryan Palo - Jun 24 '17 - - Dev Community

Note: This tutorial is a couple of years old and uses out of date versions of both Django and Vue. I don't currently have enough time to go through and update it, but I'd be happy to collaborate if someone wanted to do the heavy lifting. As for now, these articles should get the concepts across, but the code won't match current Vue or Django docs.

This is Part 2 of my tutorial for making a Vue app powered by a Django REST backend. In the first part, we set up just the Vue side. Part 2 will be the hardest part, setting up the data model. Finally, part 3 -- next time -- will be the Django backend. The tutorial will assume a working knowledge of JavaScript syntax, but I'll try to explain some of the infrastructure involved. Once again, let's get to it.

1. Introducing Vuex

Vuex is the "back of the frontend" that we'll be using to interface with (eventually) the REST backend. The nice thing about setting this up is that we can tie all of the front-end components into our data-store and point the data-store nowhere. With that in place, the store will operate only on what is in-memory. But once we're good and ready to point it at our django server, all we have to do is adjust a little bit and we don't have to change actions all over the front end. Once hooked up, Vuex will help us appear to be doing things super-fast, when we are, in reality, possibly sending a slow network call to update the database. It will take care of that overhead in the background, leaving the user to wreak havoc on the todo list in the meantime.

In order to get started, all we'll need is what you had after Part 1 -- and to install Vuex.

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

2. Creating the Store

Create a folder src/store/ and create store.js inside that folder. Vuex Stores are made of up a few modular parts, similar to the way Vue components are. For this app, we'll only use three of these: the state, the mutations, and the actions. The following shows their relationship to the rest of our app.

Vuex process flowchart

State represents the state of your data. What is the current value of todos? Your components can read from this state directly (or through getters if there is some processing to be done. We won't cover that now), but in order to write to it, they need to go through actions. A component can dispatch an action, and actions can be as asynchronus as you like. Once the action has completed all side-effects, like hitting the database, it can commit a mutation. A mutation is a synchronus change to the Vuex state, and they must be synchronus. Get it? Actions: asynchronus. Mutations: the last step of the action, totally synchronus. Anyways, too much theory. Boo theory. More code! Here are the contents of store.js.

// store.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex) // only required if you're using modules.
              // We're using modules, so there you go.

const store = new Vuex.Store({
  state: {
    todos: [
      { text: 'Learn Vue.' },
      { text: 'Do hard things.' }
    ]
  },
  mutations: {
    'ADD_TODO': function (state, todo) {
      state.todos.push(todo)
    },
    'CLEAR_TODOS': function (state) {
      const todos = state.todos
      todos.splice(0, todos.length)
    }
  },
  actions: {
    addTodo (store, todo) {
      store.commit('ADD_TODO', todo)
    },
    clearTodos (store) {
      store.commit('CLEAR_TODOS')
    }
  }
})

export default store

Enter fullscreen mode Exit fullscreen mode

You can see most of the pieces we talked about above. Our actions could (and will) do more, but currently they just pass through and commit their respective mutations. You can see that if we had very many more mutations and/or actions, we would want to break them out into separate files: actions.js and mutations.js and import them separately. The mutations don't have to be in all caps, either. You could declare them as in-place functions just like the actions (and even use the same names as the actions if you want). The tutorial I saw used SCREAMING_SNAKE case, and I kind of like it because it helps me tell the difference between the actions and the mutations. Do what you want, it's your life.

3. Using the Store

OK! Now we need to make this delicious little store available to our app. Modify your main.js file to look like the following.

// main.js

import Vue from 'vue'

import App from './App'
import store from './store/store.js'

/* eslint-disable no-new */
new Vue({
  el: 'body',
  store: store,
  components: { App }
})

Enter fullscreen mode Exit fullscreen mode

Now our store will be available from within the Components. The following is the new shiny TodoList.vue.

// src/components/TodoList.vue

<template>
  <div id="todolist">
    <ul>
      <todo v-for="todo in todos"
            v-bind:todo="todo" 
            v-bind:key="todo.id">
      </todo>
    </ul>
  </div>
</template>

<script>
import Todo from './Todo.vue'

export default {
  components: {
    Todo
  },
  computed: {
    todos () {
      return this.$store.state.todos
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

You'll notice that there was really only one change for the store. We changed the data key of the component to a computed section. That makes it so that our component will update as the outside data changes. Computed contains functions for each variable you reference. In our case that's just todos. We reference the store object with $store and can read from it directly, like we talked about before. Please also notice that I tweaked the template as well to make it work better. I took out the li element and placed the v-for directly within the todo. I'm not sure, but I kind of think it wouldn't have worked the way we had it previously. Whoopsie. That's all the changes here. Next is Todo.vue.

// src/components/Todo.vue

<template>
  <li>
    {{ todo.text }}
  </li>
</template>

<script>
export default {
  props: ['todo']
}
</script>
Enter fullscreen mode Exit fullscreen mode

As you can see, the only change was fixing my previous booboo, and placing the li element that we took out of TodoList.vue into here, replacing the p tags. It's also slightly more intuitive, I think. Moving on to the UserInput.vue.

// src/components/UserInput.vue

<template>
  <div id="user-inputs">
    <input v-model="newTodoText" v-on:keyup.enter="createTodo">
    <button v-on:click="clearTodos">
      Clear
    </button>
  </div>
</template>

<script>
export default {
  data: function () {
    return { newTodoText: '' }
  },
  methods: {
    createTodo () {
      this.$store.dispatch('addTodo', {text: this.newTodoText})
      this.newTodoText = ''
    },
    clearTodos () {
      this.$store.dispatch('clearTodos')
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

No changes to the template. We updated the createTodo method here to dispatch the createTodo action of our store. Note that we can pass a new Todo object as a second argument to dispatch. clearTodos functions in a similar manner. This is interesting because you can see that not all state is kept in the store. The newTodoText variable doesn't need to be saved for later, so we keep that in the data section of the component and the store never even worries about it.

4. Check out our Work!

And that's it! After a quick npm run dev, you should be able to see your todo list with preloaded two items. Try typing a new todo and hitting enter. The todo list updates! Try clearing everything. Everything works great!

Part 2 demo

Celebrate!

A few common errors. If you see an error that says something like no such method as dispatch, it means you probably created your store like this: const store = {} as opposed to const store = new Vuex.Store({}). Again, if it says could not get /, you have probably angered the ESLint gods. Check your terminal and put a darn space before those function parens.

Like before, I'm pretty much writing this as I learn the things, so if I do something wrong or you've got a better way, let me know about it. Stay tuned for Part 3!

Originally posted on my blog

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .