Vue.js Tutorial: How to Create Blog as Single Page Application with Vue.js? Part 2

In the second and last part of this tutorial, I enable routing and create blog as a single page application based on Vue.js JavaScript framework.

SOURCE CODE

In the previous part of the tutorial, the paginated list of all articles has been created. Now, it would be fine to click on the article and see the detail.

This is going to be single page application, but URL address should change on every post detail, otherwise you couldn’t link to it in real world.

Generally, I need to create two routes:

  1. ‘/’ as a home page with the article list
  2. ‘/post/id’ as a post detail, where id is unique for each post

Single Page Application in Vue.js

I need to install another plugin to enable routing – Vue router:

npm install vue-router --save

and enable using it in the app, modify main.js:

import Vue from 'vue'
import App from './App.vue'
import VueResource from 'vue-resource'
import VuePaginate from 'vue-paginate'
import VueRouter from 'vue-router'

// ENABLE USE 3RD PARTY PLUGINS
Vue.use(VueResource);
Vue.use(VuePaginate);
Vue.use(VueRouter);

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

As planned in the preview part of the tutorial, I need 3 components. But why when there are only 2 features (list of articles, detail)?

App.vue becomes only a template used for both states. In the template, only header and footer remains, because it is used in whole app.

Also JavaScript part will be empty in App.vue and moves somewhere else.

You can think about it like App.vue is the “parent” component, always displayed, and the list of articles (Blog.vue) and post detail (PostDetail.vue) are “children” components loaded when called by the change of the route.

Vue.js Routing

I already installed vue router and added to the app, now:

  1. routes can be defined
  2. initialized by vue router plugin
  3. add to the main Vue instance

As you might guess, main.js is the right place to specify app routing. After editing it can look like this:

import Vue from 'vue'
import App from './App.vue'
import VueResource from 'vue-resource'
import VuePaginate from 'vue-paginate'
import VueRouter from 'vue-router'
import PostDetail from './PostDetail.vue'
import Blog from './Blog.vue'

// ENABLE USE 3RD PARTY PLUGINS
Vue.use(VueResource);
Vue.use(VuePaginate);
Vue.use(VueRouter);

// ENABLE SINGLE PAGE APP ROUTING
const routes = [
  { path: '/', component: Blog },
  { path: '/post/:id', component: PostDetail }
];

const router = new VueRouter({
  mode:'history',
  routes
})

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

Paths are defined with the proper components, what about App.vue? It still used as the main component, initialized by default in the Vue instance, so I need to declare somehow, what component should display on each URL.

When Vue router is installed, you have access to new tags, where 2 are needed in this blog app:

  • router-view, this is the tag, where the code from “children” components is displayed
  • router-link, used to create special HTML “a” tag to not fully load whole page, but only switch the right component to display

The right place for the router-view is in the App.vue component. Only a header and footer remains in the template, all other code must be moved to the Blog component, which holds the list of all articles.

Also the JavaScript part must be cut and added to Blog, and App.vue becomes simple template.

If this is done and you open the browser, everything should work as before. But now, content is not defined in the App component, but via routing in Blog.

How to Display a Detail?

Route for a detail is defined as a ‘/post/:id’ where id is unique for each post. In the PostDetail component I want to do couple of things:

  1. create a template displaying post detail
  2. dynamically load content from the server
  3. enable displaying comments with the button click

The detail content depends on the id, so I need to extract it somehow from the URL. You have access to the URL via this.$route method, so parameter can be extracted by the means:

this.$route.params.id

This is needed to build up URL for GET request in our methods. After preparing a template PostDetail component can look like this:

<template>
 <article class="col-sm-12">
  <h1>{{ post.title }}</h1> 
  <p>{{ post.body }}</p>

  <nav aria-label="breadcrumb">
    <ol class="breadcrumb">
      <li class="breadcrumb-item"><router-link :to="'/'">Home</router-link></li>
      <li class="breadcrumb-item active" aria-current="page">{{ post.title }}</li>
    </ol>
  </nav>

  <h2>Discussion</h2>
  <button @click="showComments" v-if="!showCommentBox" class="btn btn-primary">show comments</button>

  <ul class="list-group" v-if="showCommentBox">
    <li v-for="comment in comments" class="list-group-item"><strong>{{ comment.email }}</strong> <em>wrote:</em> {{ comment.body }}</li>
  </ul>
 </article>
</template>

<script>
export default {
 data() {
   return {
    post: {},
    comments: [],
    showCommentBox: false,
  }
 },
 created() {
   this.$http.get("http://jsonplaceholder.typicode.com/posts/" + this.$route.params.id)
    .then(response => response.json(), error => console.log(error))
    .then(json => this.post = json, error => console.log(error));
 },
 methods: {
 showComments(){
   this.$http.get("http://jsonplaceholder.typicode.com/comments?postId=" + this.$route.params.id)
     .then(response => response.json(), error => console.log(error))
     .then(json => this.comments = json, error => console.log(error))
     .then(() => this.showCommentBox = true);
   }
 }
}
</script>

<style>

</style>

Again, created lifecycle hook is used to display the content, and one method is added to show a discussion if wanted.

Router Link

Look closer at the breadcrumbs navigation. It can navigate you home if clicked, but no classic “a” tag is used. Instead, router-link is placed there, but if you open the console in the browser and inspect element, you find proper “a” tag generated.

Using router-link is necessary in single page app, it tells the Vue.js, that only a displayed component should be changed.

If you try to create standard HTML link and click on it, whole page would reload, what is the behaviour we don’t want.

Links in the List of Articles

The last thing to do is to link articles in the list with a detail. Move to the Blog component and modify rendered section in the paginate tag:

...
<section v-for="blog in paginated('blogs')">
 <h2>{{ blog.title }}</h2>
 <router-link :to="'/post/' + blog.id" class="btn btn-primary">read more</router-link>
 <hr>
</section>
...

Router link must be unique for each article, so I need to build correct URL by passing id dynamically.

Summary

That’s it! I hope, everything went smoothly and now you have fully working blog simple page app powered by Vue.js.

If anything is not clear, feel free to post a comment or download the source code from the GitHub.