September 22, 2017

Table of contents

  1. Introduction
  2. Server-side
  3. Client-side
  4. Conclusion
  5. Useful links

Introduction

A single-page application (SPA) with Vue.js
Single-page applications have many advantages, such as speed, really good UX, and, as for developing for Drupal, the full control over the markup. There are more and more sites using SPA; there are more and more tools that simplify the process of developing complex SPA. If you read our articles (if not, then you should do it), you've probably read about a young reactive framework called Vue.js, and how you can use it in Drupal. I suggest you plunge deeper into Vue and consider writing a simple SPA.
We will create a client-side application of a simple blog. This application will show a list of all articles, and also a full text of the article. And, of course, all changes will occur without reloading a page.
With the example of this application, you will get to know how to fetch data in Vue, how to create a router and also you will learn about a very interesting concept - Vue single file components.

Server-side

In this tutorial, we will only talk about writing a client with Vue. We will not talk about creating a REST server. So I will use the jsonplaceholder.typicode.com service which provides the fake online REST API. Anyway, if you want to use Drupal as a backend (jeez, it is the Drupal website, of course, you do), we’ve already written about how to organize a RESTful server with Drupal 8. Check the articles on RESTful in the block Useful links.

Client-side

Tools

It is really easy to start using Vue. But it might become even easier if you use the right tools.
There is a vue-awesome project which includes a list of all kinds of tools, components libraries and plugins for any occasion.

Vue-cli

For jump-starting a new project, I highly recommend Vue-cli. Using this you can start the project with some of the official Vue project templates, or one of the many of the open-source templates, and, of course, you can create your own one and use it anywhere.
So, first of all, we need to install vue-cli as a global package:

$ npm install -g vue-cli

Then initialize project with the chosen template; for this project I used webpack-simple which is rather enough for our purpose.

$ vue init webpack-simple vue-spa

Then go to the vue-spa folder and run npm install in terminal. After installing all the packages, we can run our application in the development mode.

$ npm run dev

This will automatically launch our project on the webpack dev server. In the browser, we can see our simplest Vue application. Of course, it does not look like how we want, it is just a base to start. To continue the work I suggest first to familiarize yourself with the structure of our template.

Webpack-simple template

Inside the webpack-simple template we have the following structure:
Webpack-simple structure
There is an index.html file with a simple HTML markup including only the element with identifier “app” in a body. It will be replaced by a vue-generated DOM. That is the reason why you should not use the tag body as a root element.
In the src directory we have the main.js file which is the entry point for webpack.The Vue components are imported here. And also here we have a root Vue instance which has two properties for now. The property ‘el’ provides the Vue instance with an existing DOM element to mount on. And another one is a render function which generates DOM from App.vue. In general, this is all we need to know about the structure of the webpack-simple template, not so much, isn’t it? The main part of our application will be coded in App.vue. The .vue extension indicates that this file is a single-file vue component. It is one of the Vue’s features, let's get to know it better.

Single File Components

Single File Components Each *.vue file consists of three types of blocks: <template>, <script> and optional <style>. As the result of it, we can divide the project into loosely-coupled components. Inside a component, its template, logics, and styles are inherently coupled, and collocating them actually makes the component more cohesive and maintainable. So now we are ready to create our Vue Blog.

Creating the application

Let's see what we are going to do. We have a header with the name of our blog at the top of the page. On the left side, we have a fixed sidebar in which we will display the headings of our articles, it will be something like a table of contents. And the rest of the page will be occupied by a dynamic block in which the article itself will be displayed.

Vue.js SPA

 

Step 1

First of all, remove all unnecessary lines from App.vue. And create a template in accordance with our requirements.

<template>
  <div id="app">
    <header>
      <h1>Vue.js SPA</h1>
    </header>
    <main>
      <aside class="sidebar">
      </aside>
      <div class="content">
      </div>
    </main>
  </div>
</template>

Second, we will create a Vue instance with the data property that we will place in the array with our posts. For now it’s empty, but soon we will put the data received from our server inside of the array. 
Once observed, you can no longer add reactive properties to the root data object. It is therefore recommended to declare all root-level reactive properties upfront before creating the Vue instance.

<script>
  export default {
    data () {
      return {
        posts: []
      }
    }
  }
</script>

Also, I’ve added some styles to make our application look better.
The application code lives on the github.com .Just clone the repository and switch the branch by the step number to follow the application creation step by step, for example:

$ git checkout step-1

At this moment we have absolutely nothing to display in our navigation bar, so let’s get the data from our server. To do this I chose Axios a really easy-to-use HTTP client. You can also use any other convenient way for you like a Vue-resource or native fetch or even jQuery Ajax.

Step 2

Install the Axios

$ npm install --save-dev axios

Then import it into a component App and create a method getAllPosts() which we will make a request to the Drupal server and set it to the property post. Call the method in the hook created(), which will be called after the Vue instance is created and data observation has been set up.

import axios from 'axios'

export default {
  data () {
    return {
      posts: null,
      endpoint: 'https://jsonplaceholder.typicode.com/posts/',
    }
  },

  created() {
    this.getAllPosts();
  },

  methods: {
    getAllPosts() {
      axios.get(this.endpoint)
        .then(response => {
          this.posts = response.data;
        })
        .catch(error => {
          console.log('-----error-------');
          console.log(error);
        })
    }
  }
}

And now display all the headings of articles in the sidebar.

<aside class="sidebar">
  <div v-for="post in posts">
    {{ post.title }}
  </div>
</aside>

So far we have just displayed the names of posts but we can not see the full posts. Now I’m going to display the full post in the content section according to the chosen title in the sidebar. At the same time, I want every article to be available at its unique address.

Step 3

I will use the official Vue library vue-router to implement this. As it should be clear from the name, this library allows configuring routing for our application.
Install the package:

$ npm install --save-dev vue-router

To configure router go back to the main.js file. Here we will define the settings of our router and add it to the Vue instance.

import Vue from 'vue'
import Router from 'vue-router'
import App from './App.vue'
import Post from './components/Post.vue'
import Hello from './components/Hello.vue'

Vue.use(Router)

const router = new Router({
 routes: [
   {
     path: '/',
     name:'home',
     component: Hello,
   },
   {
     path: '/post/:id',
     name:'post',
     component: Post,
     props: true,
   },
 ]
})

new Vue({
 el: '#app',
 render: h => h(App),
 router
})

In the route settings, we specify which component should be rendered on a specified path. Because the only component Post.vue will be responsible for rendering of each post, we don’t have to set the path for each post, all we need is to set a dynamic path.

path: '/post/:id'

This path has a dynamic segment :id which determines the specifics of our post. Herewith we have the access to this segment in component Post via this.$route.params.id. However, using $route in our component creates a tight coupling with the route which limits the flexibility of the component as it can only be used on certain URLs. Instead of this we can use the option props and set it to true. After that, the $route.params is set as the component Post props.
Now that we have a router created we can return to our application and add a few more lines to the template.

<main>
  <aside class="sidebar">
    <router-link
        v-for="post in posts"
        active-class="is-active"
        class="link"
        :to="{ name: 'post', params: { id: post.id } }">
      {{post.id}}. {{post.title}}
    </router-link>
  </aside>
  <div class="content">
    <router-view></router-view>
  </div>
</main>

Here we have two components of vue-router: <route>r-link and <router-view>. The first one is the component for enabling user navigation in a router-enabled app. The second component is a functional component that renders a matched component for the given path.
It remains only one step. We need to display the contents of the post.

Step 4

Let’s go to the Post.vue file, which we will create a simple template in:

<template lang="html">
  <div class="post" v-if="post">
    <h1 class="post__title">{{ post.title }}</h1>
    <p class="post__body">{{ post.body }}</p>
    <p class="post__id">{{ post.id }}</p>
  </div>
</template>

Then we need to set Vue instance settings for this component. Here everything will be very similar to the settings for displaying all posts. Declare the option props with variable id, which will get the number of our post. Next, define the data object the same as in App.vue:

import axios from 'axios';

export default {
  props: ['id'],
  data() {
    return {
      post: null,
      endpoint: 'https://jsonplaceholder.typicode.com/posts/',
    }
  }
}

Then create the method getPost() which will get only one post by an identifier and call it in the created() hook.

methods: {
  getPost(id) {
    axios(this.endpoint + id)
      .then(response => {
        this.post = response.data
      })
      .catch( error => {
        console.log(error)
      })
  }
},
 
created() {
  this.getPost(this.id);
},

Almost done. If we run the application now, we’ll see that although the URL changes, we see the only post that was rendered first. The point is that for the different posts rendering we have the same component and Vue doesn’t need to recreate it because of the extra waste of resources and this also means that the lifecycle hooks of the component will not be called.
To fix this we just need to set a watcher for the $route object.

watch: {
  '$route'() {
    this.getPost(this.id);
  }
}

Now it works exactly as it should. To get a production version of this application just run npm run build in your terminal. 

Conclusion

We built the simple single page application with Vue in four steps. We knew how easy to start your project with vue-cli. We figured out the concept of Vue single-file components which make your project more flexible and scalable. We learned how to fetch data from external API using Axios. And we saw how to configure routing with the vue-router. It is a basic knowledge, of course, but I hope it will help you to start using Vue.js with more power. If you have any questions, don’t hesitate to contact us.

Useful links

Link to the GitHub project
The vue-awesome project
RESTful Web Services in Drupal 8: quick start guide
How to create a headless Drupal site

Was this article helpful? Click to rate: 
Average: 4.9 (16 votes)

You may also like

IntroductionNowadays, websites more and more look like applications...
Starting with the 5th version, Drupal has jQuery out of the box. It...