Build a realtime app with Vue.js
A basic understanding of Vue.js and Node.js are needed to follow this tutorial.
In this tutorial, I will walk you through getting started with a Vue.js 2.0 app, and adding realtime functionality to it with Pusher Channels. The sample app we will be building is a movie review app called “revue”.
Here is what the final app will look like:
You can find the complete code hosted on Github.
Setting up with Vue-cli
Vue-cli is a great command line tool for scaffolding Vue.js projects, so we don’t have to spend too much time on configuration, and can jump right into writing code!
If you haven’t already, install vue-cli:
npm install -g vue-cli
We will create a project with the webpack template, and install the dependencies with this set of commands:
vue init webpack revue
cd revue
npm install
Webpack is a build tool that helps us do a bunch of things like parse Vue single file components, and convert our ES6 code to ES5 so we don’t have to worry about browser compatibility. You can check here for more details about the webpack template.
To run the app:
npm run dev
We can also optionally include Foundation in the index.html
file to take advantage of some preset styling:
<!-- ./index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- import foundation -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/foundation/6.3.1/css/foundation.min.css">
<title>revue</title>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
Creating the movie review app
We will get started by creating the movie and review components of the app:
touch ./src/components/Movie.vue
touch ./src/components/Reviews.vue
It is helpful to know that one of the things that makes Vue so powerful is its components, much like other modern JavaScript frameworks. A typical app should be a series of components built on top of one another. This keeps our app modular, and helps make different parts of the app reusable.
Searching and retrieving a movie
To show the movie to be reviewed, we will create a simple form which we will use to fetch a movie from the Netflix Roulette public database API:
<!-- ./src/components/Movie.vue -->
<template>
<div class="container">
<div class="row">
<form @submit.prevent="fetchMovie()">
<div class="columns large-8">
<input type="text" v-model="title">
</div>
<div class="columns large-4">
<button type="submit" :disabled="!title" class="button expanded">
Search titles
</button>
</div>
</form>
</div>
<!-- /search form row -->
</div>
<!-- /container -->
</template>
In the above code, we created a form, and specified a custom fetchMovie()
event handler on form submit. Don’t worry, we will define this handler in a bit.
The @submit
directive is shorthand for v-on:submit
. The v-on
directive is used to listen to DOM events and run actions (or handlers) when they’re triggered. The .prevent
modifier helps us abstract the need to write event.preventDefault()
in the handler logic… which is pretty cool.
You can read more on Vue.js event handlers here.
We also use the v-model
directive to bind the value of the text input to title
. And finally we bind the disabled
attribute of the button such that it is set to true if title
is absent, and vice versa. :disabled
is shorthand for v-bind:disabled
.
Next we define the methods and data values for the component:
<!-- ./src/components/Movie.vue -->
<script>
// define the external API URL
const API_URL = 'https://netflixroulette.net/api/api.php'
// Helper function to help build urls to fetch movie details from title
function buildUrl (title) {
return `${API_URL}?title=${title}`
}
export default {
name: 'movie', // component name
data () {
return {
title: '',
error_message: '',
loading: false, // to track when app is retrieving data
movie: {}
}
},
methods: {
fetchMovie () {
let title = this.title
if (!title) {
alert('please enter a title to search for')
return
}
this.loading = true
fetch(buildUrl(title))
.then(response => response.json())
.then(data => {
this.loading = false
this.error_message = ''
if (data.errorcode) {
this.error_message = `Sorry, movie with title '${title}' not found. Try searching for "Fairy tail" or "The boondocks" instead.`
return
}
this.movie = data
}).catch((e) => {
console.log(e)
})
}
}
}
</script>
In the above code, after defining the external URL we want to query to get movies from, we specify the key Vue options we need for the component:
data
: this specifies properties we’ll be needing in our component. Note that in a regular Vue construct it is an object, but it has to be returned as a function in a component.methods
: this specifies the methods we are using in the component. For now, we only define one method — thefetchMovie()
method to retrieve movies. Notice we also use the Fetch API for retrieving results, to keep things simple.
Note: The JavaScript Fetch API is great for making AJAX requests, although it requires a polyfill for older browsers. A great alternative is axios.
Next, we can add the code to display the movie, and show a notice when the movie title isn’t found, inside the <template>
:
<!-- ./src/components/Movie.vue -->
<template>
<!-- // ... -->
<div v-if="loading" class="loader">
<img src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/0.16.1/images/loader-large.gif" alt="loader">
</div>
<div v-else-if="error_message">
<h3>{{ error_message }}</h3>
</div>
<div class="row" v-else-if="Object.keys(movie).length !== 0" id="movie">
<div class="columns large-7">
<h4> {{ movie.show_title }}</h4>
<img :src="movie.poster" :alt="movie.show_title">
</div>
<div class="columns large-5">
<p>{{ movie.summary }}</p>
<small><strong>Cast:</strong> {{ movie.show_cast }}</small>
</div>
</div>
</template>
We use double curly braces for text interpolation. We also introduced new v-if
, v-else-if
and v-else
directives, which we use to conditionally render elements.
We can add some optional styling at the bottom of the component:
<!-- ./src/components/Movie.vue -->
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
#movie {
margin: 30px 0;
}
.loader {
text-align: center;
}
</style>
The final Movie.vue
file will look like this.
Retrieving and writing movie reviews
Next, we will edit the review component, which will contain the logic and view for reviews, using the same Single File Component approach.
First, we use a v-for
directive to loop through the available reviews for a movie and display it in the template:
<!-- ./src/components/Review.vue -->
<template>
<div class="container">
<h4 class="uppercase">reviews</h4>
<div class="review" v-for="review in reviews">
<p>{{ review.content }}</p>
<div class="row">
<div class="columns medium-7">
<h5>{{ review.reviewer }}</h5>
</div>
<div class="columns medium-5">
<h5 class="pull-right">{{ review.time }}</h5>
</div>
</div>
</div>
</div>
</template>
<script>
const MOCK_REVIEWS = [
{
movie_id: 7128,
content: 'Great show! I loved every single scene. Defintiely a must watch!',
reviewer: 'Jane Doe',
time: new Date().toLocaleDateString()
}
]
export default {
name: 'reviews',
data () {
return {
mockReviews: MOCK_REVIEWS,
movie: null,
review: {
content: '',
reviewer: ''
}
}
},
computed: {
reviews () {
return this.mockReviews.filter(review => {
return review.movie_id === this.movie
})
}
}
}
</script>
We create MOCK_REVIEWS
to mock the available reviews, gotten from a resource, for example, an API. Then, we use a computed property to filter out the reviews for a particular movie. This would typically be gotten from the API or resource.
Next, we add a form and method for adding a new review:
<!-- ./src/components/Review.vue -->
<template>
<div class="container">
<!-- //... -->
<div class="review-form" v-if="movie">
<h5>add new review.</h5>
<form @submit.prevent="addReview">
<label>
Review
<textarea v-model="review.content" cols="30" rows="5"></textarea>
</label>
<label>
Name
<input v-model="review.reviewer" type="text">
</label>
<button :disabled="!review.reviewer || !review.content" type="submit" class="button expanded">Submit</button>
</form>
</div>
<!-- //... -->
</div>
</template>
<script>
export default {
// ..
methods: {
addReview () {
if (!this.movie || !this.review.reviewer || !this.review.content) {
return
}
let review = {
movie_id: this.movie,
content: this.review.content,
reviewer: this.review.reviewer,
time: new Date().toLocaleDateString()
}
this.mockReviews.unshift(review)
}
},
//...
}
</script>
We can add some optional styling at the bottom of the component:
<!-- ./src/components/Review.vue -->
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.container {
padding: 0 20px;
}
.review {
border:1px solid #ddd;
font-size: 0.95em;
padding: 10px;
margin: 15px 0 5px 0;
}
.review h5 {
text-transform: uppercase;
font-weight: bolder;
font-size: 0.7em
}
.pull-right {
float: right;
}
.review-form {
margin-top: 30px;
border-top: 1px solid #ddd;
padding: 15px 0 0 0;
}
</style>
To fetch and post reviews, we need to use the movie
identifier, which is gotten in the Movie
component. Thankfully, component-to-component communication can be done really easily in Vue.
Component-to-component communication
As recommended in the official documentation, we can create a new Vue instance and use it as a message bus. The message bus is an object that components can emit and listen to events on. In a larger application, a more robust state management solution like Vuex is recommended.
Creating the message bus:
touch ./src/bus.js
// ./src/bus.js
import Vue from 'vue'
const bus = new Vue()
export default bus
To emit an event once a movie is found, we update the fetchMovies()
method:
<!-- ./src/components/Movie.vue -->
import bus from '../bus'
export default {
// ...
methods: {
fetchMovie (title) {
this.loading = true
fetch(buildUrl(title))
.then(response => response.json())
.then(data => {
this.loading = false
this.error_message = ''
bus.$emit('new_movie', data.unit) // emit `new_movie` event
if (data.errorcode) {
this.error_message = `Sorry, movie with title '${title}' not found. Try searching for "Fairy tail" or "The boondocks" instead.`
return
}
this.movie = data
}).catch(e => { console.log(e) })
}
}
}
Listening for the event in the Review
component, in the created
hook:
<!-- ./src/components/Review.vue -->
<script>
import bus from '../bus'
export default {
// ...
created () {
bus.$on('new_movie', movieId => {
this.movie = movieId
})
},
// ...
}
</script>
In the above code, we specify that whenever the new_movie
event is fired, we set the movie
property to be the value of the movieId
that is broadcast by the event.
For a better understanding of the Vue lifecycle hooks, you can check out the official documentation on the subject.
Finally to complete our base app, we register our components in App.vue
, and display the templates:
<!-- ./src/App.vue -->
<template>
<div id="app">
<div class="container">
<div class="heading">
<h2>revue.</h2>
<h6 class="subheader">realtime movie reviews with Vue.js and Pusher.</h6>
</div>
<div class="row">
<div class="columns small-7">
<movie></movie>
</div>
<div class="columns small-5">
<reviews></reviews>
</div>
</div>
</div>
</div>
</template>
<script>
import Movie from './components/Movie'
import Reviews from './components/Reviews'
export default {
name: 'app',
components: {
Movie, Reviews
}
}
</script>
<style>
#app .heading {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin: 60px 0 30px;
border-bottom: 1px solid #eee;
}
</style>
Now, we can run the app, and see the basic functionalities of retrieving movies and adding reviews!
npm run dev
Note: To retrieve movies from the public API, the movie titles have to be typed in full. Also, the available movies are limited, so don’t be too disappointed if you don’t find a title you search for. :)
Adding realtime updates to the app with Pusher Channels
We can add realtime functionality to our app so that whenever a review is added, it is updated in real time to all users viewing that movie.
We will set up a simple backend where we can process post requests with new reviews, and broadcast an event via Pusher whenever a review is added.
Channels setup
Head over to Pusher and register for a free account, if you don’t already have one. Then create a Channels app on the dashboard, and copy out the app credentials (App ID, Key, Secret and Cluster). It is super straight-forward.
Backend setup and broadcasting an event
We will build a simple server with Node.js. Let us add some dependencies we will be needing to our package.json
and pull them in:
npm install -S express body-parser pusher
Next, we create a server.js
file, where we will build an Express app:
// ./server.js
/*
* Initialise Express
*/
const express = require('express');
const path = require('path');
const app = express();
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname)));
/*
* Initialise Pusher
*/
const Pusher = require('pusher');
const pusher = new Pusher({
appId:'YOUR_PUSHER_APP_ID',
key:'YOUR_PUSHER_APP_KEY',
secret:'YOUR_PUSHER_SECRET',
cluster:'YOUR_CLUSTER'
});
/*
* Define post route for creating new reviews
*/
app.post('/review', (req, res) => {
pusher.trigger('reviews', 'review_added', {review: req.body});
res.status(200).send();
});
/*
* Run app
*/
const port = 5000;
app.listen(port, () => { console.log(`App listening on port ${port}!`)});
First we initialise an express
app, then we initialise Pusher with the required credentials. Remember to replace YOUR_PUSHER_APP_ID
, YOUR_PUSHER_APP_KEY
, YOUR_PUSHER_SECRET
and YOUR_CLUSTER
with your actual details from the Pusher dashboard.
Next, we define a route for creating reviews: /review
. Whenever this endpoint is hit, we utilise Pusher to trigger a review_added
event on the reviews
channel and broadcast the entire payload as the review.
The trigger
method has this syntax: pusher.trigger(channels, event, data, socketId, callback);
. You can read more on it here.
We are broadcasting on a public channel as we want the data to be accessible to everyone. Pusher also allows broadcasting on private (prefixed by private-
) and presence (prefixed by private-
) channels, which require some form of authentication.
Creating an API proxy
To access our API server from the front-end server created by the Vue Webpack scaffolding, we can create a proxy in config/index.js
, and run the dev server and the API backend side-by-side. All requests to /api
will be proxied to the actual backend:
// config/index.js
module.exports = {
// ...
dev: {
// ...
proxyTable: {
'/api': {
target: 'http://localhost:5000', // you should change this, depending on the port your server is running on
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
},
// ...
}
}
Then, we adjust our addReview
method to post to the API in ./src/components/Reviews.vue
:
<!-- ./src/components/Review.vue -->
<script>
// ...
export default {
// ...
methods: {
addReview () {
if (!this.movie || !this.review.reviewer || !this.review.content) {
alert('please make sure all fields are not empty')
return
}
let review = {
movie_id: this.movie, content: this.review.content, reviewer: this.review.reviewer, time: new Date().toLocaleDateString()
}
fetch('/api/review', {
method: 'post',
body: JSON.stringify(review)
}).then(() => {
this.review.content = this.review.reviewer = ''
})
}
// ...
},
// ...
}
</script>
Listening for events
Finally, in our view, we can listen for events broadcast by Pusher, and update it with details, whenever a new review is published. First we add the pusher-js
library:
npm install -S pusher-js
Updating Review.vue
:
<!-- ./src/components/Review.vue -->
<script>
import Pusher from 'pusher-js' // import Pusher
export default {
// ...
created () {
// ...
this.subscribe()
},
methods: {
// ...
subscribe () {
let pusher = new Pusher('YOUR_PUSHER_APP_KEY', { cluster: 'YOUR_CLUSTER' })
pusher.subscribe('reviews')
pusher.bind('review_added', data => {
this.mockReviews.unshift(data.review)
})
}
},
// ...
}
</script>
In the above code, first we import the Pusher
object from the pusher-js
library, then we create a subscribe
method that does the following:
- Subscribes to the
reviews
channel withpusher.subscribe('reviews')
- Listens for the
review_added
event, withpusher.bind
, which receives a callback function as its second argument. Whenever it receives a broadcast, it triggers the callback function with the data broadcast as the function parameter. We update the view in this callback function by adding the new object to themockReviews
array.
Bringing it all together
We can add node server.js
to our app’s dev/start script so the API server starts along with the server provided by the webpack template:
{
// ...
"scripts": {
"dev": "node server.js & node build/dev-server.js",
"start": "node server.js & node build/dev-server.js",
// ...
}
}
To compile and run the complete app:
npm run dev
Visit localhost:8080 to view the app in action!
Conclusion
In this tutorial, we have learned how to build a Vue.js app with the webpack template. We also learned how to work with Single File Components and common Vue template directives. Finally, we learned how to make our Vue.js app realtime, utilising the simplicity and power of Pusher.
In my opinion, Vue.js is a really robust, and yet simple framework. It provides a great base for building robust realtime applications. There is also another great example here of using Vue.js and Pusher for realtime applications.
28 June 2017
by Olayinka Omole