Chat web-app using Phoenix and Vue.js — Part 6

In this part we’ll be looking at moving things in to separate components.

Here are all the parts in this series:
Github:
https://github.com/jespr/vue-phoenix-chat
Heroku: https://stormy-inlet-39179.herokuapp.com/
Part 1 — Introduction and getting a basic web-app with chat functionality going
Part 2 — Make it possible for a user to identify themselves by name before joining the chat
Part 3 — See who’s online in the chat with you
Part 4 — Prettier design + fun transitions
Part 5 — Persistence.
 
Part 6 (this article) — Move things in to separate components

Right now we have everything in one component. Things can quickly get messy, and before we move on to introducing multiple chat rooms — let’s go ahead and break things in to different components.

We have two very clear things that can be extracted to components. We have a sidebar element that shows the users connected to the chatroom and we have the list of messages.

In order to create components, it’ll be a good idea to have one place to handle state. Right now, since everything is in one component we just assign the users, messages etc as part of data() in that component.

In order to do so, we’ll introduce a new package called Vuex. Vuex is (as it so nicely states) Centralized State Management for vue.js.

So let’s go ahead and get that installed by running:

npm install vuex --save

That will also add it to our dependencies in package.json.

We now need to create the store. So let’s go ahead and create a new file web/static/js/store.js

That file should contain the following. There are some comments to tell what each thing is responsible for.

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
// root state object.
// each Vuex instance is just a single state tree.
const state = {
users: []
}
// mutations are operations that actually mutates the state.
// each mutation handler gets the entire state tree as the
// first argument, followed by additional payload arguments.
// mutations must be synchronous and can be recorded by plugins
// for debugging purposes.
const mutations = {
}
// actions are functions that causes side effects and can involve
// asynchronous operations.
const actions = {
}
// getters are functions
const getters = {
}
// A Vuex instance is created by combining the state, mutations, actions,
// and getters.
export default new Vuex.Store({
state,
getters,
actions,
mutations
})

Now we can tell the Vue app that we’re using a store, by adding it to web/static/js/app.js so let’s open up that file and add the store in there.

Near the top add the following:

import store from "./store.js"

So it now looks like this:

import "phoenix_html"
import Vue from 'vue'
import MyApp from "../components/my-app.vue"
import store from "./store.js"

Then down in the initalization of our Vue app, we add store just before our render call:

// And create the top-level view model:
new Vue({
el: '#app',
store,
render(createElement) {
return createElement(MyApp, {})
}
});

Perfect! Now let’s go ahead and extract our list of users into it’s own component. Open up web/static/components/my-app.vue find the div with the id users-list . Copy all of the div:

<div id="users-list">
<h3>Online</h3>
<ul>
<transition-group name="user-appear">
<li v-for="user in users" v-bind:key="user.user">
{{user.user}} ({{user.online_at}})
</li>
</transition-group>
</ul>
</div>

Let’s go ahead and create that new component file web/static/components/users-list.vue let’s begin by adding the HTML we just copied to the template of that component. So at the top of the new file, add your copied HTML inside of <template>..</template> tags, so it looks like this:

<template>
<div id="users-list">
<h3>Online</h3>
<ul>
<transition-group name="user-appear">
<li v-for="user in users" v-bind:key="user.user">
{{user.user}} ({{user.online_at}})
</li>
</transition-group>
</ul>
</div>
</template>

Let’s add our script section below that:

<script>
export default {
computed: {
users() {
return this.$store.state.users;
}
}
}
</script>

As you might have noticed, we added a new computed function named users() which references users coming from the global state. That right now references the empty array we added in our store.js .

Now let’s use this new component inside of web/static/components/my-app.vue instead of having the logic directly inside of that one component. At the top of our script section, we’ll import the new component by adding:

import UsersList from "./users-list"

We’ll then need to register that component in web/static/components/my-app.vue so we can use it in the template. So add a new components object below the data() function:

components: {
'users-list': UsersList
},

We can now also go ahead and remove the users: [] from the data() function in my-app.vue . You should now have something that looks like:

data(): {
return {
...
}
},
components: {
'users-list': UsersList
},
methods: {
...
}

I’ve replaced the actual content of data() and methods with ... for simplicity.

You can now go up to the template portion of my-app.vue and replace the <div id="users-list">..</div> with <users-list/> . Nice!

So you’ll now have the beginning of the main-container div looking like this:

<div id="main-container" v-else>
<users-list/>
<div id="messages-list">
<ul>
...

If you refresh your app and enter a name, you’ll see that there’s no users displayed. That’s obviously because we reference the empty users array from the global state store.

So when we fetch the users in my-app.vue where we normally assigned them to the local users data, we’ll know go ahead and update the global store instead.

Inside of web/static/components/my-app.vue find the assignUsers function and change it to this:

assignUsers(presences) {
let users = Presence.list(presences, (user, {metas: metas}) => {
return { name: user, online_at: metas[0].online_at }
})
this.$store.commit('addUsers', { users })
}

As you can see we now get all the users from the presence list and return objects containing name and online_at we then commit that to the store by calling commit — btw, you can always reference the global store via this.$store .

Let’s go ahead and open web/static/js/store.js and add the addUsers mutation over there.

So inside of const mutations = { ... } you’ll add the the following so it looks like this:

const mutations = {
addUsers (state, { users }) {
state.users = users
}
}

In here we’re simply just assigning the array of users to the state.users attribute.

We then need to make a change in our users-list component, so open up web/static/components/users-list.vue

Let’s changed the computed users function slightly, so it looks like this:

computed: {
users() {
let formatTimestamp = (timestamp) => {
timestamp = parseInt(timestamp)
let date = new Date(timestamp)
return date.toLocaleTimeString()
}
return this.$store.state.users.map(function(user) {
return {
name: user.name,
online_at: formatTimestamp(user.online_at) }
})
}
}
}

As you can see, I’ve changed the user name from user to name so let’s make that change in our template as well:

<li v-for="user in users" v-bind:key="user.name">
{{user.name}} ({{user.online_at}})
</li>

In here I’ve both changed the {{user.user}} to {{user.name}} but also the key binding from v-bind:key="user.user" to v-bind:key="user.name"

Voila! Everything should work and look the same!

Now let’s go ahead and do the same for the messages list. For the sake of keeping this post simple and shorter — I’ll go ahead and do it, and you can check out the commit on Github. A good exercise would be to see if you can do it yourself, based on what we just did with the users list.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.