Build a WordPress Plugin with Vue 2

#
By Jeff Gould

It’s been a while since we’ve played with Vue JS on this blog, so why don’t we take a beginner-focused look at how one might go about building a simple polling plugin for WordPress with Vue.

Why Vue? Vue can scale up to be used for full-blown single page applications, but you can also use it to add small bits of interactivity to sites, pages, or plugins where in the past you may have used jQuery. Vue is a great option here because it’s a quick and easy way to add interactivity, and it really streamlines the development process by bringing reactivity and component-based architecture to the table with fairly low overhead.

Why a polling plugin? There are scores of polling plugins for WordPress, most of them are free, and most of them will be better than the one we’re going to build today, but there is something missing from the current offering of polling plugins: none of them are polka themed…

Setting Up

I suppose that if we want this to be a WordPress plugin, then we’re going to need to write some PHP, so let’s just get this out of the way. My goal here is to actually write as little PHP as possible and handle as much as we can with Vue.

We’ll start off with the bare minimum for a plugin that allows us to process a shortcode:

<?php
/*
Plugin Name: Pollka King
Description: Live-updating polls for your WordPress website
Version: 0.1
Author: Jeffrey Gould
Author URI: https://jrgould.com
*/

if ( ! class_exists( 'PollkaKing' ) ) {
    class PollkaKing {

        private $shortcode_name = 'pollka';

        public function register() {
            add_shortcode( $this->shortcode_name, [$this, 'shortcode'] );
            add_action( 'wp_enqueue_scripts', [$this, 'scripts'] );
        }

        public function shortcode( $atts ) {
            return "loading poll...";
        }

        public function scripts() {
            global $post;
            // Only enqueue scripts if we're displaying a post that contains the shortcode
            if( has_shortcode( $post->post_content, $this->shortcode_name ) ) {
                wp_enqueue_script( 'vue', 'https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js', [], '2.5.16' );
                wp_enqueue_script( 'pollka-king', plugin_dir_url( __FILE__ ) . 'js/pollka-king.js', [], '0.1', true );
                wp_enqueue_style( 'pollka-king', plugin_dir_url( __FILE__ ) . 'css/pollka-king.css', [], '0.1' );
            }
        }

    }
    (new PollkaKing())->register();
}

Obviously, the plugin is called Pollka King and it enables a shortcode, [pollka]. Right now that shortcode will just be converted into the text “loading poll…”. Additionally, we’re enqueuing Vue from cdnjs.com, a css file (which you can download if you’re following along) and a script file called pollka-king.js which is where we’ll be writing all of our JavaScript.

Starting With a Shortcode

Next, we can define how we’d like to be able to use our shortcode and then use that as our guide for actually processing the shortcode. So here’s what I’d like to eventually be able to type into the editor to generate a new poll (newlines added for legibility):

[pollka
    id="polka affinity"
    question="How much do you like polka?"
    answer-1="Strike up the music, I love polka!"
    answer-2="Polka is lit, fam"
    answer-3="I guess polka is amazing..."
    answer-4="Frankie Yankovic is my hero." 
]

Based on that shortcode, it looks like we’ll need to update our shortcode()method to extract all of these attributes from the $atts argument and do some processing before letting Vue take over.

The first thing we’ll want to do is sanitize the id attribute so that it can be used as an HTML attribute or as a key when we save our poll to the database:

$id = sanitize_title_with_dashes( $atts['id'], '', 'save' );

Next, we’ll want to turn all of those answer-n attributes into an array so that they’re easier to iterate over:

$answers = [];
foreach ( $atts as $key => $val ) {
    if ( strstr( $key, 'answer-' ) ) {
        $answers[ str_replace( 'answer-', '', $key ) ] = $val;
    }
} 

Finally, we’ll want to pass all of this data along to our JavaScript. We’ll want to make sure that we can have multiple polls on a page so we can’t just output a script tag with this data populated into variables. Instead, we’ll create an HTML element and attach this data as attributes that we can access on the JS side a bit later.

First let’s create a JSON object that we can stick into an HTML attribute:

$vue_atts = esc_attr( json_encode( [
    'id'       => $id, 
    'question' => $atts['question'],
    'answers'  => $answers,
] ) );

Then, we’ll update our method to output a div with this data attached as an attribute:

return  "<div data-pk-atts='{$vue_atts}'>loading poll...</div>";

Here’s our updated shortcode() method in its entirety after a bit of refactoring:

public function shortcode( $atts ) {
    $id = sanitize_title_with_dashes( $atts['id'], '', 'save' );
    $answers = [];
    foreach ( $atts as $key => $val ) {
        if( strstr( $key, 'answer-' ) ) {
            $answers[ str_replace( 'answer-', '', $key ) ] = $val;
        }
    } 
    $vue_atts = esc_attr( json_encode( [
        'id'       => $id, 
        'question' => $atts['question'],
        'answers'  => $answers,
    ] ) );

    return "<div data-pk-atts='{$vue_atts}'>loading poll...</div>";
}

Initializing Polls

By now we’ve got our shortcode set up to output the necessary HTML, we’re enqueuing Vue from a CDN, and we’re enqueueing our empty pollka-king.js file – sounds like we’re ready to start writing some JavaScript. The first thing that we’ll want to do is find the div that our shortcode generated on the page which we can do with vanilla js™ using querySelectorAll() like so:

var  elements  =  document.querySelectorAll('[data-pk-atts]');

That will give us a JavaScript NodeList–which is very similar to an array–of any div elements containing our data-pk-atts attribute. Since we want to be able to have multiple polls show up on the same page, we can just loop over this NodeList and run whatever Vue-based magic we come up with on each of them.

Let’s create our loop and also grab the JSON out of our data-pk-atts attribute and parse it so that its usable as a JavaScript Object:

elements.forEach( function( element ) {
    var atts = JSON.parse( element.getAttribute('data-pk-atts') );
    // Do something with this element and its `atts`
});

Vue, From The Top

One of the things that I like about Vue is how flexible it is. While Vue does have a command line tool, vue-cli, that can be used to scaffold a new Vue app complete with build tooling for single file components (.vue files), it can also easily be added to any existing page or site without any tooling at all. We won’t be able to utilize the single file component idiom which is the preferred way to build with Vue, but we’re also not going to be writing a complicated single page application, so it’s easy enough to just include Vue from a CDN and start writing some plain ol’ JavaScript. So let’s get to it:

The next thing that we need to do to transform the divs that our shortcodes created into polls is to instantiate a Vue instance in place of each one and make the atts object available within the Vue instance. That may seem like a lot of instance/object talk, but it should actually be pretty simple. Let’s take a look:

elements.forEach( function( element ) {
    var atts = JSON.parse( element.getAttribute('data-pk-atts') );
    // Do something with this element and its `atts`
    var vm = new Vue({
        el: element,
        created: function() {
            this.atts = atts;
        }
    } );
});

Here we’re creating a new Vue instance that will live in the current element. We’re also using Vue’s created lifecycle method to add the atts object to the current instance. We could have used Vue’s data object for this as well, but this information doesn’t need to be reactive and I like to create a clear separation in the code between static and reactive properties.

Next we’ll need to provide Vue with something to replace our placeholder div with. For now let’s just add a new div with a .pk-container class, and we’ll display the question to make sure that everything is working. Since this won’t be much HTML, and we’re trying to keep things simple, we can just add a template property to our new Vue object and populate it with a string-based template:

var vm = new Vue({
    el: element,
    created: function() {
        this.atts = atts;
    },
    template: '<div class="pk-container">{{atts.question}}</div>'
} );

If everything went right, we should see our question displayed wherever we’ve added this shortcode: displaying the pollka king component

Componentize

Rather than doing everything in the main Vue instance, it makes sense to break this down into smaller components – one to display the poll, and one to display the results. We’ll start with the poll by creating a new Vue component that we’ll call pk-poll that will accept 1 attribute, our atts object. Let’s remove the current `{{atts.question}} placeholder and add the component we’re about to create to our template now.

template: '<div class="pk-container">\
            \<pk-poll :atts="atts" />\
            </div>',

Since we’re not using any fancy build tooling, we can just add this to the pollka-king.js file, and the only thing special that we need to do is to make sure to define our component before we use it, so we’ll add this to the top of the file, before we instantiate Vue on our elements:

var pkPoll = Vue.component('pk-poll',{
    props: ['atts'],
    data: function() { return {
        selectedAnswer: null,
    } },
});

As you can see, we’re still keeping things very simple, this just creates our pk-poll component, accepts the atts prop and also defines one reactive data property called selectedAnswer that we’ll use to track which answer a user selects. Next we’ll need to create a template for this component, which we’ll do just like we did for the main Vue instance:

template: '<div class="pk-poll">\
                <h2>{{atts.question}}</h2>\
                <div class="radio-group">\
                    <label v-for="(answer, key) in atts.answers" :key="key"><input type="radio" v-model="selectedAnswer" :value="key" >{{answer}}</label>\
                </div>\
                <button @click="submitPoll">Submit</button>\
            </div>',

This template will give us a wrapping div, classed pk-poll, display the question in an h2 and then generate radio inputs for each question by utilizing the v-for directive. We’re also adding a button with the @click directive (which is shorthand for v-on:click) set up to call a submitPoll() method which we’ll now create by adding a methods object to our component and a submitPoll function within that:

methods: {
    submitPoll: function() {
        if( null === this.selectedAnswer ) return; // don't run if no answer is selected
        var queryString = '?action=pk_submit_poll&id=' + this.atts.id + '&answer=' + this.selectedAnswer;
        fetch(window.ajaxurl + queryString);
    }
}

Here we’re just checking to see if an answer is selected, and if it is, we’re using fetch to perform a GET request to WordPress’ ajaxurl (wp-admin/admin-ajax.php) and we’re passing along an action name, pk_submit_poll which will help WordPress route our request as well as the poll’s id and the answer that the user selected. Back in our plugin php file, we can handle this request by adding the following to our register method:

add_action( 'wp_ajax_nopriv_pk_submit_poll', [$this, 'submit_poll'] );

This will cause requests with the action pk_submit_poll to be handled by a submit_poll method in our class, so we’ll add that method next:

public function submit_poll(){
    $id = sanitize_title_with_dashes( $_GET['id'], '', 'save' );
    $answer = sanitize_text_field( $_GET['answer'] );
    $option_name = 'pollka-poll_' . $id;
    $option_value = get_option( $option_name, [] );
    $answer_count = isset( $option_value[ $answer ] ) ? $option_value[ $answer ] : 0;
    $option_value[ $answer ] = $answer_count + 1; 
    update_option( $option_name, $option_value );
    exit( 'success' );
}

All we’re doing here is a minimal amount of sanitization on the data that we’ve sent via $_GET, grabbing the corresponding option from the database if it exists or creating it if it doesn’t, and then incrementing the count of how many times the provided answer has been selected. This should be enough to save our users’ responses.

We’ll also want to add the following to our scripts method to make sure that the window.ajaxurl variable is available to our frontend code:

wp_add_inline_script( 'pollka-king', 'window.ajaxurl = "' . admin_url( 'admin-ajax.php' ) . '"'); 

Here’s what we’ve got so far:

ready to poll

Currently, we can create polls on the fly using a shortcode, and visitors to our site can submit their answers to our polls. All that’s left is to display the results! Since we’ve already got pollka-king.php open, let’s start by adding another Ajax endpoint that will provide us with a poll’s data.

First, we’ll add an Ajax action to our register method to enable a pk_get_poll_data endpoint:

add_action( 'wp_ajax_nopriv_pk_get_poll_data', [$this, 'get_poll_data'] );

And then we’ll create the get_poll_data method to handle it:

public function get_poll_data() {
    $id = sanitize_title_with_dashes( $_GET['id'], '', 'save' );
    $option_name = 'pollka-poll_'  .  $id;
    $option_value = get_option( $option_name, [] );
    exit( json_encode( $option_value ) );
}

This method is even simpler than the last one, we just need to grab the poll id from $_GET and sanitize it like before, then grab the corresponding option from the database. If the option doesn’t exist, we’ll just send an empty array. The one “gotcha” here is that instead of returning the data, we’re using exit() to output the data as JSON and then end the request.

Back in our JavaScript file, we can create another Vue component called pk-results:

var pkResults = Vue.component('pk-results',{
    template: '<div class="pk-results">\
    </div>',
    props: ['atts'],
    data: function() { return {
    } },
    methods: {
    }
});

And then we can add that component to our main Vue instance template right after our pk-poll component:

template: '<div class="pk-container">\
                <pk-poll :atts="atts" />\
                <pk-results :atts="atts" />\
            </div>',

Next, we’ll want to update the pk-results template to show our question and answers:

template: '<div class="pk-results">\
                <h2>{{atts.question}}</h2>\
                <div claass="results-group">\
                    <p v-for="(answer, key) in atts.answers" :key="key"><span>{{answer}}</span></p>\
                </div>\
            </div>',

Now that we’ve got the question and answers, we need to grab the results from the API and figure out the best way to display them. We can use the mounted lifecycle method to query the API for our results:

mounted: function() {
    var queryString = '?action=pk_get_poll_data&id=' + this.atts.id
    fetch(window.ajaxurl + queryString)
        .then( function(response) { return response.json() })
        .then( function(json) { 
            this.results = json;
         }.bind(this) );
},

Here we’re using fetch again to query our pk_get_poll_data endpoint, and when the results come back, we’re taking the resulting JSON object and storing it as the property results on the component instance. I’m using bind to ensure that this inside the callback refers to the component instance, but you could also use an arrow function here instead.

There is one problem with the above code: we’re adding the data that we’ve received from our Ajax call to this.results but that property isn’t reactive so when this data shows up, our component won’t automatically do anything with it. To get this to be reactive, we can cheat a bit and create the property in our data object using a copy of the answers object that we passed along with atts as a template:

data: function() { return {
    results: Object.assign( {}, this.atts.answers )
} },

This will create results as a reactive object that we can then access and update using this.results. The only drawback here is that when the component is instantiated, the values of this object will be the answer strings rather than a number, but JavaScript is dynamically typed so that won’t cause us too much trouble, we’ll just use the created lifecycle method to zero out all of those values:

created: function() {
    Object.keys( this.results ).forEach( function( key ) {
        this.results[key] = 0;
    }.bind(this))
}, 

Now we can create some methods that will help us use this data in our template – we’ll probably want to display the results as ratios like 11/27, but we’ll also want to have a bar-chart style display so we’ll also create a method to get the results as a percentage that we can use in CSS.

methods: {
    getAnswerRatio( key ) {
        var total = Object.values(this.results).reduce( function( acc, cur ) { return acc+cur; }, 0 );
        var count = parseInt(this.results[key], 10) || 0;
        return count + ' / ' + total;  
    },
    getAnswerStyle( key ) {
        var total = Object.values(this.results).reduce( function( acc, cur ) { return acc+cur; }, 0 );
        var count = parseInt(this.results[key], 10) || 0;
        var percentage = (count / total) * 100;
        return 'width: '+ percentage + '%;';  
    }
}

And now we can use those methods to populate our template:

template: '<div class="pk-results">\
                <h2>{{atts.question}}</h2>\
                <div class="results-group">\
                    <p v-for="(answer, key) in atts.answers" :key="key">\
                        <span>{{answer}} ({{getAnswerRatio(key)}})</span>\
                        <span class="percentage-bar" :style="getAnswerStyle(key)"></span>\
                    </p>\
                </div>\
            </div>',

And with a bit of CSS magic, here’s how everything is looking:

displaying the poll and the results

Now we just have two small issues to fix. The first is that the results are loaded before our users get a chance to submit their answer, so they don’t see how their answer effects the poll. The second is that users can submit the poll multiple times. We can solve both of these issues by only showing the poll at first and then hiding the poll and showing the results when the user submits their answer.

We can do that by first adding a reactive data object to our main Vue instance with a pollSubmitted property that we’ll initially set to `false:

data: {
    pollSubmitted: false
},

Next, we’ll add an event listener to the pk-poll component in the main Vue instance’s template that will set pollSubmitted to true when pk-poll emits an event that we’ll call submitted. While we’re there, let’s use v-if and v-else directives to only display the poll when pollSubmitted is false and the results when pollSubmitted is true:

template: '<div class="pk-container">\
                \<pk-poll :atts="atts" @submitted="pollSubmitted=true" v-if="!pollSubmitted"/>\
                \<pk-results :atts="atts" v-else/>\
            </div>',

Finally, we just need to set up the pk-poll component to emit this submitted event when the user submits the form. We can do this by adding this.$emit('submitted'); to the component’s submitPoll method, I’ll actually add it to a fetch callback within that method to make sure that the data is submitted before we display the results:

submitPoll: function() {
    if( null === this.selectedAnswer ) return;
    var queryString = '?action=pk_submit_poll&id=' + this.atts.id + '&answer=' + this.selectedAnswer;
    fetch(window.ajaxurl + queryString).then( function() {
        this.$emit('submitted');
    }.bind(this) );

}

And we’re done! Here’s our final product in action:

pollka-king in action

And here’s all of the code on github: https://github.com/JRGould/pollka-king/

Conclusion

There are quite a few things that we didn’t cover here such as using nonces to add a bit of security to our endpoints, preventing users from submitting more than once, error handling in JS or PHP, the fact that storing data this way in the options table is inefficient, and I’m sure there are a few more things that you were planning to yell at share with me about in the comments. While all of those things are important for a plugin that someone might use on a real site, I hope that this was a good introduction to using Vue to add a bit of interactivity to a WordPress plugin with minimal setup.

There are a lot of next steps that we could take towards making this plugin ready to be distributed, and a lot of cool features that we could add like real-time updating, or an admin screen that shows an overview of all results. We could also focus on the code: set up a build process for our Vue code and break up the javascript into single file components.

Let us know in the comments if you’d be interested in a sequel to this post and what topics you’d like to see covered.

About the Author

Jeff Gould

Jeff is a problem solver at heart who found his way to the web at an early age and never looked back. Before Delicious Brains, Jeff was a freelance web developer specializing in WordPress and front-end development.