Template rendering with Vue for WordPress

Published Mar 19, 2018Last updated Mar 20, 2018
Template rendering with Vue for WordPress

Introduction

Rendering the template in WordPress is just as straightforward as in vanilla PHP. For example, this is usually how you would render your custom posts in WordPress:

<?php
if (get_query_var('paged')) $paged = get_query_var('paged');
if (get_query_var('page')) $paged = get_query_var('page');

$query = new WP_Query(array('post_type' => 'books', 'paged' => $paged));

if ($query->have_posts()) : ?>
    <?php while ($query->have_posts()) : $query->the_post(); ?>
        <div class="entry">
            <h2 class="title"><?php the_title(); ?></h2>
            <?php the_content(); ?>
        </div>
    <?php endwhile; wp_reset_postdata(); ?>
    <!-- show pagination here -->
<?php else : ?>
    <!-- show 404 error here -->
<?php endif; ?>

However, just like PHP itself, it is verbose and becomes messy when you mix PHP with loads of HTML. Needless to say more on the maintainability and readability when your template is very complicated. Hence, there are many PHP template engines to help you to cope with that, such as Twig.

Vue is a JavaScript front-end framework. One of its great features is the ability to render data to the DOM using straightforward template syntax:

<div id="app">
  {{ message }}
</div>

var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

This article is a basic example showing you how to render your WordPress posts with Vue.

WordPress API

First of all, we need to create a custom JSON API endpoint in funtions.php to get the custom posts data from WordPress database and then pass the JSON output to Vue object. For example, I have this block of code in funtions.php to fetch the posts with specimen post-type:

/**
 * Custom JSON API endpoint.
 * Usage: http://sample-project.com/wp-json/specimens/v1/parent/7/page/1
 */
function api_specimens($data) {
    $output = array();

    // Get post slug.
    $post = get_post($data['parent_id']);
    $slug = $post->post_name;

    // e.g. 1, 2, 3,...
    $paged = $data['page_number'] ? $data['page_number'] : 1;
    $query_args = array(
        'post_type' => 'specimen',
        'post_status' => array('publish'),
        'posts_per_page' => 10,
        'paged' => $paged,
        'orderby' => 'date',
    );

    // Create a new instance of WP_Query
    $the_query = new WP_Query($query_args);

    if (!$the_query->have_posts()) {
        return null;
    }

    while ($the_query->have_posts()) {
        $the_query->the_post();

        $post_id = get_the_ID();
        $post_title = get_the_title();
        $post_content = get_the_content();
        $post_excerpt = get_the_excerpt();
        $post_date = get_the_date('l, j F Y');

        // Get the slug.
        $post = get_post($post_id);
        $post_slug = $post->post_name;

        // Get image alt.
        $alt = get_post_meta(get_post_thumbnail_id(), '_wp_attachment_image_alt', true);
        $desc = get_post_meta(get_post_thumbnail_id(), '_wp_attachment_image_caption', true);

        // Get image description.
        $description = null;
        $thumbnail = get_posts(array('p' => get_post_thumbnail_id(), 'post_type' => 'attachment'));
        if ($thumbnail && isset($thumbnail[0])) {
            $description = $thumbnail[0]->post_content;
        }

        // Image data.
        $post_image = array(
            'id' => get_post_thumbnail_id() ,
            'url'  => get_the_post_thumbnail_url(),
            'caption' => get_the_post_thumbnail_caption(),
            'alt' => $alt,
            'description' => $description,
        );

        // Category data.
        $categories = get_the_category($post_id);
        $post_categories = array();
        foreach ($categories as $key => $category) {
            $post_categories[] = array(
                'name' => $category->name,
                'slug' => $category->slug,
                'url'  => get_category_link($category->cat_ID),
            );
        }

        // Push the post data into the array.
        $output[] = array(
            'id' => "post" . $post_id, // cannot use numbers, or hyphens between words for Zurb data toggle attribute
            'slug' => $post_slug,
            'title' => $post_title,
            'url' => get_permalink($post_id),
            'date' => $post_date,
            'content' => $post_content,
            'excerpt' => $post_excerpt,
            'image' => $post_image,
            'categories' => $post_categories
        );
    }

    $result = array(
        "next" => (int)$paged === (int)$the_query->max_num_pages ? null :  site_url() . '/' . $slug . '/page/' . ($paged + 1) . '/',
        "prev" => (int) $paged === 1 ? null : site_url() . '/' . $slug . '/page/' . ($paged - 1)  . '/',

        // Split the array every 4 items.
        "chunks" => array_chunk($output, 4)
    );

    // Reset the post to the original after loop. otherwise the current page
    // becomes the last item from the while loop.
    wp_reset_query();

    // Return json.
    return $result;
}
add_action('rest_api_init', function () {
    register_rest_route('specimens/v1', '/parent/(?P<parent_id>\d+)/page/(?P<page_number>\d+)', array(
        'methods' => 'GET',
        'callback' => 'api_specimens',
    ));
});

You can test the endpoint on your browser, it should return the similar JSON data below:

{
    "next": "http:\/\/sample-project.com\/specimens\/page\/2\/",
    "prev": null,
    "chunks": [
        [{
            "id": "post230",
            "slug": "ginger-monkey-logo-2",
            "title": "GINGER MONKEY LOGO",
            "url": "http:\/\/sample-project.com\/specimen\/ginger-monkey-logo-2\/",
            "date": "Monday, 12 March 2018",
            "content": "",
            "excerpt": "",
            "image": {
                "id": "231",
                "url": "http:\/\/sample-project.com\/wp-content\/uploads\/2018\/03\/GM.png",
                "caption": "",
                "alt": "",
                "description": ""
            },
            "categories": null
        }, {
            "id": "post227",
            "slug": "magician-custom-type",
            "title": "MAGICIAN CUSTOM TYPE",
            "url": "http:\/\/sample-project.com\/specimen\/magician-custom-type\/",
            "date": "Monday, 12 March 2018",
            "content": "",
            "excerpt": "",
            "image": {
                "id": "228",
                "url": "http:\/\/sample-project.com\/wp-content\/uploads\/2018\/03\/MAGICIAN.png",
                "caption": "",
                "alt": "",
                "description": ""
            },
            "categories": null
        }, {
            "id": "post224",
            "slug": "lora",
            "title": "L\u2019ORA",
            "url": "http:\/\/sample-project.com\/specimen\/lora\/",
            "date": "Monday, 12 March 2018",
            "content": "",
            "excerpt": "",
            "image": {
                "id": "225",
                "url": "http:\/\/sample-project.com\/wp-content\/uploads\/2018\/03\/Lora.png",
                "caption": "",
                "alt": "",
                "description": ""
            },
            "categories": null
        }, {
            "id": "post221",
            "slug": "worlds-end-rum-logo",
            "title": "WORLD\u2019S END RUM LOGO",
            "url": "http:\/\/sample-project.com\/specimen\/worlds-end-rum-logo\/",
            "date": "Monday, 12 March 2018",
            "content": "",
            "excerpt": "",
            "image": {
                "id": "222",
                "url": "http:\/\/sample-project.com\/wp-content\/uploads\/2018\/03\/WER.png",
                "caption": "",
                "alt": "",
                "description": ""
            },
            "categories": null
        }],
        [{
            "id": "post218",
            "slug": "mb-monogram",
            "title": "MB MONOGRAM",
            "url": "http:\/\/sample-project.com\/specimen\/mb-monogram\/",
            "date": "Monday, 12 March 2018",
            "content": "",
            "excerpt": "",
            "image": {
                "id": "219",
                "url": "http:\/\/sample-project.com\/wp-content\/uploads\/2018\/03\/MB-MONOGRAM.png",
                "caption": "",
                "alt": "",
                "description": ""
            },
            "categories": null
        }, {
            "id": "post215",
            "slug": "qm-monogram",
            "title": "QM MONOGRAM",
            "url": "http:\/\/sample-project.com\/specimen\/qm-monogram\/",
            "date": "Monday, 12 March 2018",
            "content": "",
            "excerpt": "",
            "image": {
                "id": "216",
                "url": "http:\/\/sample-project.com\/wp-content\/uploads\/2018\/03\/QM-MONOGRAM.png",
                "caption": "",
                "alt": "",
                "description": ""
            },
            "categories": null
        }]
    ]
}

Vue

After making sure the endpoint is working, now on the JavaScript side, this is what I have:

// Import node modules.
import 'babel-polyfill'
import DocReady from 'es6-docready'
import $ from 'jquery'
import 'jquery-ui-bundle'
import Foundation from 'foundation-sites'
import Vue from 'vue/dist/vue.js'
import axios from 'axios'

// Must wait until DOM is ready before initiating the modules.
DocReady(async () => {
  // Render template with Vue.
  // Get specimen json.
  var element = document.getElementById('vue-specimen')
  if (element !== null) {
    var endpoint = $('#vue-specimen').data('posts-endpoint')
    var getData = await axios.get(endpoint)
    var specimen = new Vue({
      el: '#vue-specimen',
      data: {
        items: getData.data
      }
    })
  }
})

Below is what I have in the PHP file in my custom theme for rendering the data above:

<?php
// Get current url.
global $wp;
$current_url = home_url($wp->request);
$request = end(explode('/', $current_url));

// JSON API endpoint.
$paged = is_numeric($request) ? $request : 1;
$endpoint = site_url() . '/wp-json/specimens/v1/parent/' . $post->ID .'/page/' . $paged;
?>

<!-- vue template -->
<div id="vue-specimen" data-posts-endpoint="<?php echo $endpoint; ?>">

<template v-if="items" >

    <!-- loop each chunk -->
    <template v-for="specimens in items.chunks">

        <div class="row row-scale">
            <div class="grid-container">
                <div class="grid-x grid-padding-x grid-items">

                    <!-- loop each item in specimens -->
                    <template v-for="(specimen, index) in specimens">
                    <div class="large-3 cell cell-item text-center">
                        <a href="#">
                            <img :src="specimen.image.url" :alt="specimen.image.alt">
                        </a>
                        <h2 class="heading-specimen">{{ specimen.title }}</h2>
                   </div>
                    </template>
                    <!-- loop each item in specimens -->

                </div>
            </div>
        </div>

    </template>
    <!-- loop each chunk -->

    <!-- row pagination -->
    <div class="row row-pagination">
        <div class="grid-container">
            <div class="grid-x grid-padding-x">

                <div class="small-10 cell">
                    <nav v-if="items.prev || items.next" aria-label="Pagination">
                      <ul class="pagination">
                        <li v-if="items.prev" class="previous"><a :href="items.prev" class="button-arrow-left" data-tooltip aria-haspopup="true" data-disable-hover="false" tabindex="1" title="Previous Page" data-position="right" data-alignment="center"><i class="icon-arrow-left"></i></a></li>
                        <li v-else class="previous"><a :href="items.prev" class="button-arrow-left disabled" data-tooltip aria-haspopup="true" data-disable-hover="false" tabindex="1" title="Previous Page" data-position="right" data-alignment="center"><i class="icon-arrow-left"></i></a></li>
                        <li v-if="items.next" class="next"><a :href="items.next" aria-label="Next page" class="button-arrow-right" data-tooltip aria-haspopup="true" data-disable-hover="false" tabindex="1" title="Next Page" data-position="right" data-alignment="center"><i class="icon-arrow-right"></i></a></li>
                        <li v-else class="next"><a :href="items.next" aria-label="Next page" class="button-arrow-right disabled" data-tooltip aria-haspopup="true" data-disable-hover="false" tabindex="1" title="Next Page" data-position="right" data-alignment="center"><i class="icon-arrow-right"></i></a></li>
                      </ul>
                    </nav>

                </div>

                <div class="small-2 cell">
                    <nav class="nav-button float-right">
                        <a href="#" class="float-right button-arrow-up" data-tooltip aria-haspopup="true" data-disable-hover="false" tabindex="1" title="Go Up" data-position="left" data-alignment="center"><i class="icon-arrow-up"></i></a>
                    </nav>
                </div>

            </div>
        </div>

    </div>
    <!-- row pagination -->

</template>

</div>
<!-- vue template -->

We should get this layout on the browser screen:

vue-rendering.png

You can see how the similar data above rendered here.

Conclusion

At last, we have successfully separated the data logic from HTML! You can see how 'clean' it is with:

<template v-for="(specimen, index) in item">
<div class="large-3 cell cell-item text-center">
    <a href="#">
        <img :src="specimen.image.url" :alt="specimen.image.alt">
    </a>
    <h2 class="heading-specimen">{{ specimen.title }}</h2>
</div>
</template>

Comparing to:

<?php
if (get_query_var('paged')) $paged = get_query_var('paged');
if (get_query_var('page')) $paged = get_query_var('page');

$query = new WP_Query(array('post_type' => 'books', 'paged' => $paged));

if ($query->have_posts()) : ?>
    <?php while ($query->have_posts()) : $query->the_post(); ?>
        <div class="entry">
            <h2 class="title"><?php the_title(); ?></h2>
            <?php the_content(); ?>
        </div>
    <?php endwhile; wp_reset_postdata(); ?>
    <!-- show pagination here -->
<?php else : ?>
    <!-- show 404 error here -->
<?php endif; ?>

However we must bear in mind that this is only a client side rending with Vue. We can go even further if you are interested in server side rendering with Vue then we must use Nuxt, which is a framework for creating Universal Vue.js Applications. I wrote a few articles before on Nuxt if you want to learn more about it, for example Creating a Nuxt application with Koa, Express, and Slim and Nuxt: Decoupling the view and controller in your PHP application. We can abandon all standard WordPress template rendering with PHP, including the navigations, headers, and so on. We only use WordPress API to fetch the data we need for our Nuxt application which is served as our main site for public access.

Hope this basic idea and example is helpful. Let me know what you think. Any suggestions and insights, please leave a comment below.

Discover and read more posts from LAU TIAM KOK
get started