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.
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:
1vue init webpack revue 2cd revue 3npm 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:
1<!-- ./index.html --> 2<!DOCTYPE html> 3<html> 4 <head> 5 <meta charset="utf-8"> 6 <!-- import foundation --> 7 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/foundation/6.3.1/css/foundation.min.css"> 8 <title>revue</title> 9 </head> 10 <body> 11 <div id="app"></div> 12 <!-- built files will be auto injected --> 13 </body> 14</html>
We will get started by creating the movie and review components of the app:
1touch ./src/components/Movie.vue 2touch ./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.
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:
1<!-- ./src/components/Movie.vue --> 2<template> 3 <div class="container"> 4 <div class="row"> 5 <form @submit.prevent="fetchMovie()"> 6 <div class="columns large-8"> 7 <input type="text" v-model="title"> 8 </div> 9 <div class="columns large-4"> 10 <button type="submit" :disabled="!title" class="button expanded"> 11 Search titles 12 </button> 13 </div> 14 </form> 15 </div> 16 <!-- /search form row --> 17 </div> 18 <!-- /container --> 19</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:
1<!-- ./src/components/Movie.vue --> 2<script> 3// define the external API URL 4const API_URL = 'https://netflixroulette.net/api/api.php' 5// Helper function to help build urls to fetch movie details from title 6function buildUrl (title) { 7 return `${API_URL}?title=${title}` 8} 9 10export default { 11 name: 'movie', // component name 12 data () { 13 return { 14 title: '', 15 error_message: '', 16 loading: false, // to track when app is retrieving data 17 movie: {} 18 } 19 }, 20 methods: { 21 fetchMovie () { 22 let title = this.title 23 if (!title) { 24 alert('please enter a title to search for') 25 return 26 } 27 this.loading = true 28 fetch(buildUrl(title)) 29 .then(response => response.json()) 30 .then(data => { 31 this.loading = false 32 this.error_message = '' 33 if (data.errorcode) { 34 this.error_message = `Sorry, movie with title '${title}' not found. Try searching for "Fairy tail" or "The boondocks" instead.` 35 return 36 } 37 this.movie = data 38 }).catch((e) => { 39 console.log(e) 40 }) 41 } 42 } 43} 44</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 — the fetchMovie()
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>
:
1<!-- ./src/components/Movie.vue --> 2<template> 3<!-- // ... --> 4<div v-if="loading" class="loader"> 5 <img src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/0.16.1/images/loader-large.gif" alt="loader"> 6</div> 7 8<div v-else-if="error_message"> 9 <h3>{{ error_message }}</h3> 10</div> 11 12<div class="row" v-else-if="Object.keys(movie).length !== 0" id="movie"> 13 <div class="columns large-7"> 14 <h4> {{ movie.show_title }}</h4> 15 <img :src="movie.poster" :alt="movie.show_title"> 16 </div> 17 <div class="columns large-5"> 18 <p>{{ movie.summary }}</p> 19 <small><strong>Cast:</strong> {{ movie.show_cast }}</small> 20 </div> 21</div> 22</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:
1<!-- ./src/components/Movie.vue --> 2<!-- Add "scoped" attribute to limit CSS to this component only --> 3<style scoped> 4#movie { 5 margin: 30px 0; 6} 7.loader { 8 text-align: center; 9} 10</style>
The final Movie.vue
file will look like this.
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:
1<!-- ./src/components/Review.vue --> 2<template> 3 <div class="container"> 4 <h4 class="uppercase">reviews</h4> 5 <div class="review" v-for="review in reviews"> 6 <p>{{ review.content }}</p> 7 <div class="row"> 8 <div class="columns medium-7"> 9 <h5>{{ review.reviewer }}</h5> 10 </div> 11 <div class="columns medium-5"> 12 <h5 class="pull-right">{{ review.time }}</h5> 13 </div> 14 </div> 15 </div> 16 </div> 17</template> 18 19<script> 20const MOCK_REVIEWS = [ 21 { 22 movie_id: 7128, 23 content: 'Great show! I loved every single scene. Defintiely a must watch!', 24 reviewer: 'Jane Doe', 25 time: new Date().toLocaleDateString() 26 } 27] 28export default { 29 name: 'reviews', 30 data () { 31 return { 32 mockReviews: MOCK_REVIEWS, 33 movie: null, 34 review: { 35 content: '', 36 reviewer: '' 37 } 38 } 39 }, 40 computed: { 41 reviews () { 42 return this.mockReviews.filter(review => { 43 return review.movie_id === this.movie 44 }) 45 } 46 } 47} 48</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:
1<!-- ./src/components/Review.vue --> 2<template> 3 <div class="container"> 4 <!-- //... --> 5 <div class="review-form" v-if="movie"> 6 <h5>add new review.</h5> 7 <form @submit.prevent="addReview"> 8 <label> 9 Review 10 <textarea v-model="review.content" cols="30" rows="5"></textarea> 11 </label> 12 <label> 13 Name 14 <input v-model="review.reviewer" type="text"> 15 </label> 16 <button :disabled="!review.reviewer || !review.content" type="submit" class="button expanded">Submit</button> 17 </form> 18 </div> 19 <!-- //... --> 20 </div> 21</template> 22 23<script> 24export default { 25 // .. 26 methods: { 27 addReview () { 28 if (!this.movie || !this.review.reviewer || !this.review.content) { 29 return 30 } 31 let review = { 32 movie_id: this.movie, 33 content: this.review.content, 34 reviewer: this.review.reviewer, 35 time: new Date().toLocaleDateString() 36 } 37 this.mockReviews.unshift(review) 38 } 39 }, 40 //... 41} 42</script>
We can add some optional styling at the bottom of the component:
1<!-- ./src/components/Review.vue --> 2<!-- Add "scoped" attribute to limit CSS to this component only --> 3<style scoped> 4 .container { 5 padding: 0 20px; 6 } 7 .review { 8 border:1px solid #ddd; 9 font-size: 0.95em; 10 padding: 10px; 11 margin: 15px 0 5px 0; 12 } 13 .review h5 { 14 text-transform: uppercase; 15 font-weight: bolder; 16 font-size: 0.7em 17 } 18 .pull-right { 19 float: right; 20 } 21 .review-form { 22 margin-top: 30px; 23 border-top: 1px solid #ddd; 24 padding: 15px 0 0 0; 25 } 26</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.
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
1// ./src/bus.js 2import Vue from 'vue' 3const bus = new Vue() 4 5export default bus
To emit an event once a movie is found, we update the fetchMovies()
method:
1<!-- ./src/components/Movie.vue --> 2import bus from '../bus' 3 4export default { 5 // ... 6 methods: { 7 fetchMovie (title) { 8 this.loading = true 9 fetch(buildUrl(title)) 10 .then(response => response.json()) 11 .then(data => { 12 this.loading = false 13 this.error_message = '' 14 bus.$emit('new_movie', data.unit) // emit `new_movie` event 15 if (data.errorcode) { 16 this.error_message = `Sorry, movie with title '${title}' not found. Try searching for "Fairy tail" or "The boondocks" instead.` 17 return 18 } 19 this.movie = data 20 }).catch(e => { console.log(e) }) 21 } 22 } 23}
Listening for the event in the Review
component, in the created
hook:
1<!-- ./src/components/Review.vue --> 2<script> 3import bus from '../bus' 4export default { 5 // ... 6 created () { 7 bus.$on('new_movie', movieId => { 8 this.movie = movieId 9 }) 10 }, 11 // ... 12} 13</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:
1<!-- ./src/App.vue --> 2<template> 3 <div id="app"> 4 <div class="container"> 5 <div class="heading"> 6 <h2>revue.</h2> 7 <h6 class="subheader">realtime movie reviews with Vue.js and Pusher.</h6> 8 </div> 9 <div class="row"> 10 <div class="columns small-7"> 11 <movie></movie> 12 </div> 13 <div class="columns small-5"> 14 <reviews></reviews> 15 </div> 16 </div> 17 </div> 18 </div> 19</template> 20 21<script> 22 import Movie from './components/Movie' 23 import Reviews from './components/Reviews' 24 25 export default { 26 name: 'app', 27 components: { 28 Movie, Reviews 29 } 30 } 31</script> 32 33<style> 34 #app .heading { 35 font-family: 'Avenir', Helvetica, Arial, sans-serif; 36 -webkit-font-smoothing: antialiased; 37 -moz-osx-font-smoothing: grayscale; 38 text-align: center; 39 color: #2c3e50; 40 margin: 60px 0 30px; 41 border-bottom: 1px solid #eee; 42 } 43</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. :)
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.
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.
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:
1// ./server.js 2/* 3 * Initialise Express 4 */ 5const express = require('express'); 6const path = require('path'); 7const app = express(); 8const bodyParser = require('body-parser'); 9app.use(bodyParser.json()); 10app.use(bodyParser.urlencoded({ extended: true })); 11app.use(express.static(path.join(__dirname))); 12 13/* 14 * Initialise Pusher 15 */ 16const Pusher = require('pusher'); 17const pusher = new Pusher({ 18 appId:'YOUR_PUSHER_APP_ID', 19 key:'YOUR_PUSHER_APP_KEY', 20 secret:'YOUR_PUSHER_SECRET', 21 cluster:'YOUR_CLUSTER' 22}); 23 24/* 25 * Define post route for creating new reviews 26 */ 27app.post('/review', (req, res) => { 28 pusher.trigger('reviews', 'review_added', {review: req.body}); 29 res.status(200).send(); 30}); 31 32/* 33 * Run app 34 */ 35const port = 5000; 36app.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.
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:
1// config/index.js 2module.exports = { 3 // ... 4 dev: { 5 // ... 6 proxyTable: { 7 '/api': { 8 target: 'http://localhost:5000', // you should change this, depending on the port your server is running on 9 changeOrigin: true, 10 pathRewrite: { 11 '^/api': '' 12 } 13 } 14 }, 15 // ... 16 } 17}
Then, we adjust our addReview
method to post to the API in ./src/components/Reviews.vue
:
1<!-- ./src/components/Review.vue --> 2<script> 3// ... 4export default { 5 // ... 6 methods: { 7 addReview () { 8 if (!this.movie || !this.review.reviewer || !this.review.content) { 9 alert('please make sure all fields are not empty') 10 return 11 } 12 let review = { 13 movie_id: this.movie, content: this.review.content, reviewer: this.review.reviewer, time: new Date().toLocaleDateString() 14 } 15 fetch('/api/review', { 16 method: 'post', 17 body: JSON.stringify(review) 18 }).then(() => { 19 this.review.content = this.review.reviewer = '' 20 }) 21 } 22 // ... 23 }, 24 // ... 25} 26</script>
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
:
1<!-- ./src/components/Review.vue --> 2<script> 3import Pusher from 'pusher-js' // import Pusher 4 5export default { 6 // ... 7 created () { 8 // ... 9 this.subscribe() 10 }, 11 methods: { 12 // ... 13 subscribe () { 14 let pusher = new Pusher('YOUR_PUSHER_APP_KEY', { cluster: 'YOUR_CLUSTER' }) 15 pusher.subscribe('reviews') 16 pusher.bind('review_added', data => { 17 this.mockReviews.unshift(data.review) 18 }) 19 } 20 }, 21 // ... 22} 23</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:
reviews
channel with pusher.subscribe('reviews')
review_added
event, with pusher.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 the mockReviews
array.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:
1{ 2 // ... 3 "scripts": { 4 "dev": "node server.js & node build/dev-server.js", 5 "start": "node server.js & node build/dev-server.js", 6 // ... 7 } 8}
To compile and run the complete app:
npm run dev
Visit localhost:8080 to view the app in action!
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.