Building Reactive WordPress Plugins – Part 2 – Vue.js

In the first part of this series I showed you how to build WP Cron Pixie, a small WordPress dashboard widget that displays a site’s scheduled cron events. The article’s primary purpose was to introduce reactive style frontend development, using Backbone.js as the framework as it comes bundled with WordPress.

In this somewhat shorter article we’re going to jump a couple years forward in frontend JavaScript development technology and replace Backbone.js with Vue.js.

Gilbert used Vue.js with the WP-REST-API on his previous Creating a WordPress Theme using the REST API and Vue.js article, but this time we’re going to continue to use the admin-ajax.php based backend from Part 1.

Why Vue.js?

Umm, because it’s awesome!

Oh, you want a little more info? Well, when you look at the getting started docs for Vue.js you’d be forgiven for thinking there’s not a great deal to separate it from Backbone.js. Once you’ve instantiated an instance of the Vue class and attached it to the DOM, you can specify a template, data model, methods and events to play with, with two-way binding of model data. Nothing out of the ordinary there.

<div id="app">
    <p>{{ message }}</p>
    <input v-model="message">
</div>

new Vue({
    el: '#app',
    data: {
        message: 'Hello Vue.js!'
    }
});

Where Vue.js starts to shine is when your code base starts to get large enough that you start to think of your views in terms of discreet components.

<div id="example">
    <my-component></my-component>
</div>

// define
var MyComponent = Vue.extend({
    template: '<div>A custom component!</div>'
});

// register
Vue.component('my-component', MyComponent);

// create a root instance
new Vue({
    el: '#example'
});

The above example is not ideal. For one, the template is being defined in a string, which is gross. You’d normally reference a <template> instead. The biggest problem is that you’d traditionally update 3 separate files when making a change to a single component, the HTML, CSS and JavaScript files, often held in different locations. Wouldn’t it be great to have your nice discreet reusable component manageable as a single source file? Vueify has you covered.

// app.vue
<style>
    .red {
        color: #f00;
    }
</style>

<template>
    <h1 class="red">{{msg}}</h1>
</template>

<script>
export default {
    data () {
        return {
            msg: 'Hello world!'
        }
    }
}
</script>

The Plan

At the moment WP Cron Pixie uses a single main.js file for the frontend JavaScript, which contains a mix of functionality for listing and rendering cron Schedules and their Events. The HTML templates used for rendering the content is currently coming from a single place (the backend PHP is spitting it out as part of the widget content), and the CSS to be applied to the HTML is coming from a main.css file.

The plan is to set up the frontend with 4 small easy to work with components that provide the following structure once output.

<schedules>
    <schedule>
        <events>
            <event></event>
            <event></event>
        </events>
    </schedule>
    <schedule>
        <events>
            <event></event>
            ...
        </events>
    </schedule>
    ...
</schedules>

To do that we need to set up a couple of tools and libraries as WordPress doesn’t include Vue.js by default.

Setting Up

First we’re going to need to install Node.js if not already installed. If you’ve done any kind of web development in the last couple of years, the chances are you’ve already got this set up.

Install Browserify.

$ npm install -g browserify

Browserify is going to be used to compile our nicely organised Vue components and bootstrap JavaScript down to a single shippable build.js JavaScript file. It looks at the main JavaScript file that kicks off the frontend and follows all its dependencies (all the components we’ll require in our case) and shakes it all down to a single file that includes all the code that actually runs. Optionally you can run this file through something like uglifyjs to minimise it.

Create a package.json for the project so that we can pull in Vue.js and its dependencies with Node’s package manager, npm.

$ npm init

When you follow the prompts you’ll end up with a skeleton package.json file that holds the basic information required by npm based tools and packages. The most important setting is for the “main” JavaScript file, which in our case is src/js/main.js.

Install latest stable vue.js

$ npm install vue --save-dev

This adds the following to package.json:

"devDependencies": {
  "vue": "^1.0.24"
}

Install latest stable vueify so that we can use *.vue component files.

$ npm install vueify --save-dev

If running npm 3+, you’ll need to install babel dependencies too.

$ npm install --save-dev babel-core babel-preset-es2015 babel-plugin-transform-runtime babel-runtime

Now our devDependencies looks like this:

"devDependencies": {
  "babel-core": "^6.9.1",
  "babel-plugin-transform-runtime": "^6.9.0",
  "babel-preset-es2015": "^6.9.0",
  "babel-runtime": "^6.9.2",
  "vue": "^1.0.24",
  "vueify": "^8.5.2"
}

Once we’ve got started with our Vue based JavaScript we’ll be able to run browserify to transform our JavaScript like so…

$ browserify -t vueify -e src/js/main.js -o src/js/build.js

But running that every time we make a change to our source gets old fast.

Let’s use npm run to save some key strokes and memory cells. Add the following to the "scripts" section of package.json:

"build-js": "browserify -t vueify -e src/js/main.js -o src/js/build.js"

Now we can simply run:

$ npm run build-js

And because manually running that command every time we change a file could get annoying, how about we automate the Browserify build? Watchify to the rescue:

$ npm install -g watchify

Add the following line to the "scripts" section of package.json:

  "watch-js": "watchify -v -t vueify -e src/js/main.js -o src/js/build.js"

Now we can run the following command from the root of our project and not have to worry about compiling our JavaScript before testing:

$ npm run watch-js

We include the -v option in the watchify command to make things a little more verbose. It’s good to get the confirmation that something was complied when you save a change.

Now our "scripts" section should look something like the following:

"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "build-js": "browserify -t vueify -e src/js/main.js -o src/js/build.js",
  "watch-js": "watchify -v -t vueify -e src/js/main.js -o src/js/build.js"
},

Maybe one day we’ll do something about that "test" command!

Backend Changes

Because we’re going to be using *.vue component files, the templates for our HTML don’t need to be output by the backend PHP any longer. All we need is a div to hang our Vue application code off of, with our top level component embedded.

/**
 * Provides the initial content for the widget.
 */
public function dashboard_widget_content() {
    ?>
    <!-- Main content -->
    <div id="cron-pixie-main">
        <cron-pixie-schedules :schedules="schedules"></cron-pixie-schedules>
    </div>
    <?php
}

That’s a good bit simpler than our previous HTML.

If you’re new to Vue you might be wondering what the following is all about.

<cron-pixie-schedules :schedules="schedules"></cron-pixie-schedules>

We’ve already introduced custom components, but what’s that :schedules="schedules" bit all about?

Vue exposes a bunch of directives such as v-bind, v-on, v-for and v-if that can be used to add functionality to your HTML templates and glue the JavaScript code to the HTML.

In this case we’re using v-bind‘s shortcut of : to bind some variable called “schedules” to a custom attribute called schedules. We could have written it in the long form:

<cron-pixie-schedules v-bind:schedules="schedules"></cron-pixie-schedules>

Later in the article you’ll get to see where that schedules variable comes from and how the custom attribute is defined on the custom component.

We’re also going to move all our CSS into our component files, so we can remove the main.css file entirely and remove the wp_enqueue_style call from enqueue_scripts.

We’ll also change the wp_enqueue_script call to remove the dependency on jQuery and Backbone, and reference the compiled build.js file rather than main.js source file. We’re going to keep the CronPixie JavaScript variable intact for its translatable strings and initial data set.

/**
 * Enqueues the JS scripts when the main dashboard page is loading.
 *
 * @param string $hook_page
 */
public function enqueue_scripts( $hook_page ) {
    if ( 'index.php' !== $hook_page ) {
        return;
    }

    $script_handle = $this->plugin_meta['slug'] . '-main';

    wp_enqueue_script(
        $script_handle,
        plugin_dir_url( $this->plugin_meta['file'] ) . 'js/build.js',
        array(),
        $this->plugin_meta['version'],
        true // Load JS in footer so that templates in DOM can be referenced.
    );

    // Add initial data to CronPixie JS object so it can be rendered without fetch.
    // Also add translatable strings for JS as well as reference settings.
    $data = array(
        'strings'      => array(
            'no_events'    => _x( '(none)', 'no event to show', 'wp-cron-pixie' ),
            'due'          => _x( 'due', 'label for when cron event date', 'wp-cron-pixie' ),
            'now'          => _x( 'now', 'cron event is due now', 'wp-cron-pixie' ),
            'passed'       => _x( 'passed', 'cron event is over due', 'wp-cron-pixie' ),
            'weeks_abrv'   => _x( 'w', 'displayed in interval', 'wp-cron-pixie' ),
            'days_abrv'    => _x( 'd', 'displayed in interval', 'wp-cron-pixie' ),
            'hours_abrv'   => _x( 'h', 'displayed in interval', 'wp-cron-pixie' ),
            'minutes_abrv' => _x( 'm', 'displayed in interval', 'wp-cron-pixie' ),
            'seconds_abrv' => _x( 's', 'displayed in interval', 'wp-cron-pixie' ),
            'run_now'      => _x( 'Run event now.', 'Title for run now icon', 'wp-cron-pixie' ),
        ),
        'nonce'        => wp_create_nonce( 'cron-pixie' ),
        'timer_period' => 5, // How often should display be updated, in seconds.
        'data'         => array(
            'schedules' => $this->_get_schedules(),
        ),
    );
    wp_localize_script( $script_handle, 'CronPixie', $data );
}

That’s it for backend changes, we’re only changing stuff directly related to the frontend output. All the other backend plugin set up and data handling functions are to remain exactly same.

Frontend Components

To get things started, let’s kick off the frontend code with the bare minimum to prove that we have a working Vue and Vueify setup.

// src/js/main.js
var Vue = require( 'vue' );
var Schedules = require( './components/schedules.vue' );

// Main Vue instance that bootstraps the frontend.
new Vue( {
    el: '#cron-pixie-main',
    data: CronPixie.data,
    components: {
        CronPixieSchedules: Schedules
    }
} );

Our main.js bootstrap file sets up an instance of Vue after requiring the vue library we installed during setup. That Vue instance attaches itself to the div we have in our widget’s HTML, references the initial data we know was exported as CronPixie.data and then makes the schedules.vue component available for use as cron-pixie-schedules (the camel cased CronPixieSchedules name given to the exposed component is transformed to meet the requirements for custom components).

That CronPixie.data variable will have a schedules property, so the data property of the Vue instance will have a top level schedules variable available too. That is why there is a schedules variable available to be bound to the schedules attribute of the cron-pixie-schedules component in the initial HTML.

Our bare minimum Schedules component just shows a header and placeholders for the schedules.

// src/js/components/schedules.vue
<template>
    <h3>Schedules</h3>
    <ul class="cron-pixie-schedules">
        <li v-for="schedule in schedules">
            {{ schedule.display }}
        </li>
    </ul>
</template>

<style>
</style>

<script>
    export default {
        props: {
            schedules: {
                default: function() {
                    return [];
                }
            }
        }
    }
</script>

The v-for loops through all the schedules, and {{ schedule.display }} just pulls out the contents of schedule.display to output the display name of a cron schedule.

But where did that schedules variable come from? Well, it’s a custom attribute of the component that is declared with the props section in the code. You can normally just declare your attributes in an array.

props: ['wibble', 'wobble', 'anotherProperty']

In my case I wanted to make sure schedules defaulted to an empty list, and as that requires an array instance to be created, a factory function format was required instead. Without the new instance being created and returned from the function we’d run the risk of returning a reference to a schedules object already defined on an instance of Vue further up the parent chain.

Let’s kick off our build for the first time…

$ npm run watch-js

> [email protected] watch-js /Users/ian/Dropbox/Projects/wp-cron-pixie
> watchify -v -t vueify -e src/js/main.js -o src/js/build.js

265570 bytes written to src/js/build.js (2.32 seconds)

… and see what we have…

WP Cron Pixie - Vue.js First Steps

Well, that’s awesome, but we’re not going to stop there!

Component All The Things

By now you’re probably getting a feel for how you require a component, expose it and use it on your template. So I’m going to move a little faster through the components and just show you the final versions.

// src/js/components/schedules.vue
<template>
    <h3>Schedules</h3>
    <ul class="cron-pixie-schedules">
        <li v-for="schedule in schedules">
            <cron-pixie-schedule :schedule="schedule"></cron-pixie-schedule>
        </li>
    </ul>
</template>

<style>
</style>

<script>
    var CronPixieSchedule = require( './schedule.vue' );

    export default {
        props: {
            schedules: {
                default: function() {
                    return [];
                }
            }
        },
        components: {
            CronPixieSchedule
        }
    }
</script>

The above cron-pixie-schedules component now uses a cron-pixie-schedule component to display each cron schedule.

// src/js/components/schedule.vue
<template>
    <span class="cron-pixie-schedule-display" title="{{ schedule.name }}">{{ schedule.display }}</span>
    <cron-pixie-events :events="schedule.events"></cron-pixie-events>
</template>

<style>
    .cron-pixie-schedule-display {
        font-weight: bold;
    }
</style>

<script>
    var CronPixieEvents = require( './events.vue' );

    export default {
        props: {
            schedule: {
                default: function() {
                    return {
                        name: 'the_schedule',
                        display: 'The Schedule',
                        events: []
                    };
                }
            }
        },
        components: {
            CronPixieEvents
        }
    }
</script>

This is the first time you’ve seen an embedded style sheet, simple isn’t it?

The cron-pixie-schedule component embeds a cron-pixie-events component that introduces a couple of new Vue features.

// src/js/components/events.vue
<template>
    <ul class="cron-pixie-events">
        <span v-if="empty" class="cron-pixie-event-empty">{{ strings.no_events }}</span>
        <li v-else v-for="event in events">
            <cron-pixie-event :event="event"></cron-pixie-event>
        </li>
    </ul>
</template>

<style>
    .cron-pixie-events {
        padding-left: 1em;
    }
    .cron-pixie-event-empty {
        color: grey;
    }
</style>

<script>
    var CronPixieEvent = require( './event.vue' );

    export default {
        props: {
            events: {
                default: function() {
                    return [];
                }
            }
        },
        computed: {
            empty: function() {
                return this.events.length === 0;
            }
        },
        components: {
            CronPixieEvent
        }
    }
</script>

The above cron-pixie-events uses the v-if and v-else directives to specify that if there are no events then display a little message, otherwise display the list. There’s a computed property that checks the length of the events list to determine whether it’s empty or not. A computed property is a “live” variable that re-computes its value as the state of any of the variables it references in its calculations change.

To display the “No Events” message we’re using a variable called strings.no_events. Where did that come from? It has come from our CronPixie global variable that the backend exposes. That has a strings property. However, you’ll have to bear with me for a couple more components before I circle back to a modified main.js file to show you how I made strings available to all the components.

The final component that you could have predicted is cron-pixie-event, and this is where we’ll catch the click to run a scheduled cron event now rather than when it’s due.

// src/js/components/event.vue
<template>
    <span @click="runNow" class="cron-pixie-event-run dashicons dashicons-controls-forward" title="{{ strings.run_now }}"></span>
    <span class="cron-pixie-event-hook">{{ event.hook }}</span>
    <div class="cron-pixie-event-timestamp dashicons-before dashicons-clock">
        <span class="cron-pixie-event-due">{{ strings.due }}:&nbsp;{{ due }}</span>
        &nbsp;
        <span class="cron-pixie-event-seconds-due">(<cron-pixie-display-interval :interval="event.seconds_due"></cron-pixie-display-interval>)</span>
    </div>
</template>

<style>
    .cron-pixie-event-run:hover {
        color: darkgreen;
        cursor: pointer;
    }
    .cron-pixie-event-timestamp {
        clear: both;
        margin-left: 1em;
        color: grey;
    }
</style>

<script>
    var CronPixieDisplayInterval = require( './display_interval.vue' );

    export default {
        props: {
            event: {
                default: function() {
                    return {
                        schedule: '',
                        interval: 0,
                        hook: 'the_hook',
                        args: [],
                        timestamp: 0,
                        seconds_due: 0
                    };
                }
            }
        },
        computed: {
            due: function() {
                return new Date( this.event.timestamp * 1000 ).toLocaleString();
            }
        },
        components: {
            CronPixieDisplayInterval
        },
        methods: {
            runNow: function() {
                // Only bother to run update if not due before next refresh.
                if ( this.event.seconds_due > this.timer_period ) {
                    // Tell the rest of the app that we're about to update an event.
                    this.$dispatch( 'update-event', 'begin' );

                    this.event.timestamp = this.event.timestamp - this.event.seconds_due;
                    this.event.seconds_due = 0;

                    // Send the request to update the event record.
                    this.resource.update( {
                        action: 'cron_pixie_events',
                        nonce: this.nonce,
                        model: this.event
                    } ).then( function( response ) {
                        // Tell the rest of the app that we've finished updating an event.
                        this.$dispatch( 'update-event', 'end' );
                    }, function( response ) {
                        // On failure, still need to tell the rest of the app that we've finished updating an event.
                        this.$dispatch( 'update-event', 'end' );
                        // Log error.
                        console.log( response );
                    } );
                }
            }
        }
    }
</script>

This is the biggest component, so there’s a couple of things to explain.

Instead of v-on:click="runNow" we’re using the @ shorthand for v-on: to get @click="runNow".

There’s a lot of other stuff going on in this component too, such as usage of another component that I haven’t mentioned so far called cron-pixie-display-interval. This just wraps the displayInterval function we had before to take a value of seconds and display it broken down into weeks, days, hours, minutes and seconds. You can find the source for that component on GitHub.

Apart from where strings comes from, which I promise we’ll get to very soon, the only other obvious mystery in the above code relates to how the updated event data is sent to the backend with…

// Send the request to update the event record.
this.resource.update( {
    action: 'cron_pixie_events',
    nonce: this.nonce,
    model: this.event
} )...

For that we need to introduce vue-resource.

Resources & Mixins

To handle sending data to WordPress’s wp-admin/admin-ajax.php without hand-coding XMLHttpRequest calls or pulling in jQuery’s $.ajax functionality, we’re going to install vue-resource.

$ npm install vue-resource --save-dev

This library has a nice feature where you create a common resource with a specified base URL to hit, and then you can use get, post, save, update and other convenience functions on the resource to send requests to the backend.

So in our main.js we add the following near the top.

// Use and configure vue-resource.
Vue.use( require( 'vue-resource' ) );
Vue.http.options.emulateJSON = true;
Vue.http.options.emulateHTTP = true;

This pulls in the vue-resource library, and sets the options we need for our “legacy web server” technology used by admin-ajax.php.

Now we want to create a resource for all Vue instances to use, including the components. To do that we create a resource property in a global Mixin.

// Create a global mixin to expose strings, global config, and single backend resource.
Vue.mixin( {
    computed: {
        strings: function() {
            return CronPixie.strings;
        },
        nonce: function() {
            return CronPixie.nonce;
        },
        timer_period: function() {
            return CronPixie.timer_period;
        },
        resource: function() {
            return this.$resource( '/wp-admin/admin-ajax.php' );
        }
    }
} );

Now every component and our top level Vue instance can get or post data to our backend using this.resource.

Oh look, and there on the mixin is our strings property, solving the mystery as to how were were able to use strings.no_events and its friends in our components.

Fetching Schedules

The last thing we need to implement from the original plugin is fetching the schedule data every five seconds. To do that we’ll just port the runTimer(), pauseTimer(), refreshData() functions and add them as methods on our top level Vue instance.

We’ll also use Vue’s ready() lifecycle hook to start the timer.

var Vue = require( 'vue' );
var Schedules = require( './components/schedules.vue' );

// Use and configure vue-resource.
Vue.use( require( 'vue-resource' ) );
Vue.http.options.emulateJSON = true;
Vue.http.options.emulateHTTP = true;

// Create a global mixin to expose strings, global config, and single backend resource.
Vue.mixin( {
    computed: {
        strings: function() {
            return CronPixie.strings;
        },
        nonce: function() {
            return CronPixie.nonce;
        },
        timer_period: function() {
            return CronPixie.timer_period;
        },
        resource: function() {
            return this.$resource( '/wp-admin/admin-ajax.php' );
        }
    }
} );

// Main Vue instance that bootstraps the frontend.
new Vue( {
    el: '#cron-pixie-main',
    data: CronPixie.data,
    components: {
        CronPixieSchedules: Schedules
    },
    methods: {
        /**
         * Retrieves new data from server.
         */
        refreshData: function() {
            this.resource.get( {
                action: 'cron_pixie_schedules',
                nonce: this.nonce
            } ).then( function( response ) {
                this.schedules = response.data;
            }, function( response ) {
                // Log error.
                console.log( response );
            } );
        },

        /**
         * Start the recurring display updates if not already running.
         */
        runTimer: function() {
            if ( undefined == CronPixie.timer ) {
                CronPixie.timer = setInterval( this.refreshData, CronPixie.timer_period * 1000 );
            }
        },

        /**
         * Stop the recurring display updates if running.
         */
        pauseTimer: function() {
            if ( undefined !== CronPixie.timer ) {
                clearInterval( CronPixie.timer );
                delete CronPixie.timer;
            }
        },

        /**
         * Toggle recurring display updates on or off.
         */
        toggleTimer: function() {
            if ( undefined !== CronPixie.timer ) {
                this.pauseTimer();
            } else {
                this.runTimer();
            }
        }
    },
    events: {
        'update-event': function( status ) {
            if ( 'begin' === status ) {
                this.pauseTimer();
            } else {
                this.runTimer();
            }
        }
    },
    ready: function() {
        // Start a timer for updating the data.
        this.runTimer();
    }
} );

Does It Still Work?

Of course it does!

wp-cron-pixie-demo-vuejs

You can find the full source to the Vue.js version of WP Cron Pixie on GitHub.

Wrap Up

So, that wasn’t as short an article as I hoped it would be, but it’s hard to keep the word count down when there’s a lot to be said for a great technology such as Vue.js. Hopefully I’ve whetted your appetite enough that you’ll investigate it further for any future frontend web development.

I seriously recommend the Learning Vue 1.0: Step by Step video course by Laracasts if you’d like to get a great introduction to Vue.js. It’s mostly not Laravel based, only a couple of the later episodes use the Laravel framework and even then a WordPress developer would have no problem following along.

Did I miss anything out in my explanation of how I re-implemented WP Cron Pixie’s frontend in Vue.js? Ask away in the comments section below.

About the Author

Ian Jones Senior Software Developer

Ian is always developing software, usually with PHP and JavaScript, loves wrangling SQL to squeeze out as much performance as possible, but also enjoys tinkering with and learning new concepts from new languages.