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:
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:
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 return
ing 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:
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:
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.
What's your preferred JavaScript framework when building a WordPress plugin's UI?
— Delicious Brains (@dliciousbrains) July 12, 2018