A little background and introduction
We needed to create a UI component on the edit screen of posts in WordPress. We wanted the user to be able to add location to a post by inputting in the address. We needed to geocode that address to latitude and longitude. Surely, there would be a plugin already out there and, yes, there are but practically all use Google Maps and Google Maps Terms of Service forbid us to save the data received from them. (You could cache for a short period of time). We need to be able to save latitude and longitude in a custom database table for fast searches. After finding another location api that allows saving of data (geocoder.opencagedata.com), we then needed a way input an address, send a request to search the location api, and then add the location to the database.
Creating a user interface can be a tedious task. To help us we decided to use the JavaScript framework, Vue.js. It is a simple, solid, fast framework that gives us most of the bells and whistles of Angular and React but without the learning curve.
Separating UI in three different ‘screens’
After drawing out some wireframes and getting a basic un-styled view of the UI’s functionality. We needed three ‘screens’ (I guess this could be called routes if we were actually creating a single page web app). ‘Screen’ is being used loosely here to represent a group of markup the user will see in the WordPress meta box. The other ‘screens’ will not show at the same time. For example, the initial screen will show the current location with an Edit or Remove button. When they click the Edit button, this screen will be hidden and now the user will see the edit ‘screen’ containing the address fields and buttons for ‘OK’, ‘Cancel’, or ‘Geocode’. The third ‘screen’ is used when the user clicks the ‘Geocode’ button and a list of possible addresses are shown. Here the user then selects the address that is the closest match and they are taken back to the initial screen with the new address filled in as the current address.
Click to download 0.6MB gif file.
Big picture view here is that all we are doing is creating three divs each containing the ‘screen’s’ markup. Only one of the three divs is showing at a time as determined by which button the user presses. It’s all contained in a single WordPress meta box and no page refreshes are being done. Javascript events are used to determine when to show and hide divs. The divs are shown or hidden using one of two methods:
- Adding or removing the divs from the DOM
- Altering the CSS style to hide or show the div
Initial Prototyping using CodePen.io
We started developing using CodePen.io. We setup our Pen with the following: download the Vue.js library from a CDN, compile the javascript using Babel so we can use all the ES6 goodies, and compile the HTML using Pug templating engine(used to be called Jade and very much like HAML). A link to a CodePen is found below. Of course, you don’t have to use pug or ES6 to use Vue.js.
Setup data to match initial data
Our first step after setting up the Codepen.io is to create a JavaScript object that will contain initial data we will need from WordPress. For example, we will need the post’s current location if they have one. We will need the some WordPress items like the ajax action and nonce. We also create another JavaScript object to represent the data we received from geocoding the address.
Here is an example of the object containing some of the initial data we will need:
// Simulate the data WP will populate on page creation
let loc_8_fwp = {
result: {
lat: '32.3336368',
long: '-95.2930722',
address: '1329 S Beckham Ave',
address2: '',
city: 'Tyler',
state: 'TX',
zip: '75701',
country: 'USA',
},
action: 'loc_8_geocode',
ajax_url: 'http://staging3.adv.jhtechservices.com/wp-admin/admin-ajax.php'
Here is an example result from geocoding with multiple possible results the user will have to choose from:
let fwpResults = {
results: [{
lat: '30.0075278',
long: '-95.7369283',
address: '16125 Country Fair Ln',
city: 'Cypress',
state: 'TX',
zip: '77433',
country: 'US'
}, {
lat: '29.9316274',
long: '-95.6702606',
address: '11655 Green Canyon Dr',
city: 'Houston',
state: 'TX',
zip: '77095',
country: 'US'
}, {
lat: '39.612032',
long: '-82.904623',
address: '1476 Lancaster Pike',
city: 'Circleville',
state: 'OH',
zip: '43113',
country: 'US'
}]
}
Setup initial div for component and a div for each of the ‘screens’
On the HTML edit screen let’s add the main component div with id of loc-8-component
and the three ‘screens’.
#loc-8-component
#loc-8-view
#loc-8-edit
#loc-8-choice
Again, we setup the HTML screen in CodePen.io to use Pug. You can type out the HTML manually if you wish. This Pug code will become:
Let’s jump right in with Vue here. We know that these div’s will not appear at the same time. We are going to use the vue attribute, v-if
, to tell it when to show this div. The value of the v-if
is any JavaScript expression that evaluates to true or false. Let’s use a variable called uiState
. Later we will set the value of this variable with methods in our Vue Component.
In pug this will look like:
#loc-8-component
#loc-8-view(v-if='uiState === "view"')
#loc-8-edit(v-if='uiState === "edit"')
#loc-8-choice(v-if='uiState === "choice"')
Instead of v-if
we could use v-show
. The difference is that v-if
adds and removes the div from the DOM. v-show
uses CSS to hide or show the divs.
Create the main vue component
Next lets jump over to JavaScript and begin the main components creation. We’ll start by binding it to the #loc-8-component
div and defining the data object we will want this component to watch and react to.
// Start and bind Vue framework
let vm = new Vue({
el: '#loc-8-component',
data: {
loc: {
lat: '',
long: '',
address: '',
address2: '',
city: '',
state: '',
zip: '',
country: '',
geo_date: ''
},
uiState: 'view', //'view, 'edit', 'choice',
request: false,
results: [],
selected: null
}
});
Defining these variables here tells Vue to watch them for changes.
Setup `created` function to initialize component data
All Vue components have a lifecycle and it has predefined hooks we can add code too. For instance during the creation phase of the component we can setup a function to run during this phase. To hook into this phase we create a key in the object we pass in called created
and it’s value will be the function we want to run. We could initialize our data variables directly with the loc_8_fwp variable that WordPress will eventually provide us, but instead let’s use the created
hook.
let vm = new Vue({
el: '#loc-8-component',
data: {
...//previous code
},
created: function() {
if(typeof(loc_8_fwp) !== 'undefined') {
if (typeof(loc_8_fwp.result) !== 'undefined') {
this.copyLocation(loc_8_fwp.result, this.loc)
}
this.ajax_url = loc_8_fwp.ajax_url;
this.action = loc_8_fwp.action;
this.ajax_nonce = loc_8_fwp._ajax_nonce;
}
});
When the Vue component created, it automatically moves all the data variables so that you can access them using the this
keyword or externally using the vm
variable. So instead of accessing the latitude via vm.data.loc.lat
, you can access as vm.loc.lat
. This is, also, done on any methods we define as in this.copyLocation()
. We’ll see how this method is defined later. Any variable starting with the underscore (‘_’) or dollar sign (‘$’) will not be moved hence I renamed the _ajax_nonce variable to ajax_nonce.
Setup a Computed Property
We want our first screen to have a title ‘Current Location’ if the post has a location already. If it doesn’t have latitude or longitude, we want to title it ‘Address Only’. We’ll use the same Vue directive of v-if
for this. The JavaScript expression we will us will be loc.lat && loc.long
. If any value is empty, we’ll get a false value. We’ll probably need this in other places so let’s use a computed property. The values of these properties are determined by a function and the value is also cached so that it only recomputes the value if any of the underlying variables change.
let vm = new Vue({
el: '#loc-8-component',
data: {
...//previous code
},
created: function() { ... },
computed: {
hasLocation: function() {
return this.loc.long && this.loc.lat;
},
}
});
So now we can use this computed property in our markup to determine whether to have the title Current Location or Address Only:
#loc-8-component
#loc-8-view.row(v-if='uiState==="view"')
h2(v-if='hasLocation') Current Location
h2(v-else) Address Only
Create a helpful method
Sometimes we will need methods that are not tied to specific data. We want a method that will return the full address of a location to show on the screen. It could be the current location or one of the many results we get back after geocoding. In Vue we put these helpful methods under the key methods
. We saw another example above called copyLocation.
let vm = new Vue({
el: '#loc-8-component',
data: {
...//previous code
},
created: function() { ... },
computed: { ... },
methods: {
fullAddress: function(loc = {}) { //ES6 code that gives a default value to parameter of one isn't given
let result = '';
result += loc.address ? loc.address : '';
result += loc.address2 ? ', ' + loc.address2 : '';
result += loc.city ? ', ' + loc.city : '';
result += loc.state ? ', ' + loc.state : '';
result += loc.zip ? ', ' + loc.zip : '';
result += loc.country ? ', ' + loc.country : '';
// return result after removing any beginning comma
return result.replace(/^,/g, '').trim();
},
copyLocation: function(from, to) {
to.lat = from.lat;
to.long = from.long;
to.address = from.address;
to.address2 = from.address2;
to.city = from.city;
to.state = from.state;
to.zip = from.zip;
to.country = from.country;
}
}
});
Below we are showing the address and coordinates if at least one exist. If not, we are showing ‘No Location’. The template
element allows us to group markup together. The final html when viewed in the browser will not show a template
tag.
#loc-8-component
#loc-8-view.row(v-if='uiState==="view"')
h2(v-if='hasLocation') Current Location
h2(v-else) Address Only
tempate(v-if='hasLocation || fullAddress(loc) !== ""')
p.l8-address {{ fullAddress(loc) }}
p.l8-latlong(v-if="hasLocation") ({{ loc.lat + ', ' + loc.long }})
template(v-else)
p.l8-address No Location
For those not familiar with Pug, here’s the compiled markup:
Current Location
Address Only
{{ fullAddress(loc) }}
({{ loc.lat + ', ' + loc.long }})
No Location
(Isn’t writing in Pug much cleaner looking and easier to read?)
Adding Buttons and Events
Let’s add a “Edit” button and tie it to a method called currentEdit
.
... {previous code}
button(v-on:click.prevent="currentEdit()") Edit
v-on
allows us to bind to an event. In this case, we are binding to the ‘click’ event. We could link to other events such as ‘mouseover’ or ‘keyup’, etc. The ‘.prevent’ is a modifier that is telling Vue to prevent any default actions to run on this event. It’s just like running ‘event.preventDefault() in your event handler. We could still run that function in the currentEdit function as long as we define the event argument in the function definition. Any any case, if the default actions are not prevented, when we move this component to the WordPress edit post page, a button press would signal a ‘submit’ action causing the page to refresh. We don’t want this.
Once we capture a click event what do we want currentEdit to do? We will have it copy the current location to the ‘editLoc’ object that the user can alter and geocode, but still press “cancel” if they want to keep the current location unchanged. We could use our copyLocation
method here or we could use a typical JavaScript way of making a deep copy of an object using JSON.parse and stringify. We will need currentEdit to change the uiState variable so that our ‘screen’ will change to the ‘edit’ screen as defined by the div with id loc-8-edit. As soon as uiState is changed to ‘edit’, the screen will change automatically. Here’s the complete currentEdit method:
let vm = new Vue({
el: '#loc-8-component',
data: {
...//previous code
},
created: function() { ... },
computed: { ... },
methods: {
... //previous code
currentEdit: function() {
this.editLoc = JSON.parse(JSON.stringify(this.loc));
this.uiState = 'edit';
}
}
});
We also want to add a ‘Remove’ button to remove the address and location. We’ll have it’s event handler call a method currentRemove that will simply change the values of the this.loc object to empty strings. The cool thing here is that is all we have to do. The logic of the markup with the ‘v-if’ will automatically change our screen once this is done. It it will change it’s title and show ‘No Address’.
We also add this logic to what buttons we show since we don’t want the “edit” button if there is no address; we would want an “Add” button instead.
This is the awesomeness of Vue is that it reacts automatically to changes in the data.
The complete markup with all the button logic we have so far for the ‘view’ screen.
#loc-8-component
#loc-8-view.row(v-if='uiState==="view"')
h2(v-if='hasLocation') Current Location
h2(v-else) Address Only
tempate(v-if='hasLocation || fullAddress(loc) !== ""')
p.l8-address {{ fullAddress(loc) }}
p.l8-latlong(v-if="hasLocation") ({{ loc.lat + ', ' + loc.long }})
template(v-else)
p.l8-address No Location
template(v-if='hasLocation')
button(v-on:click.prevent="currentEdit()") Edit
button(v-on:click.prevent="currentRemove()") Remove
template(v-else-if='!hasLocation && fullAddress(loc) === ""')
button(v-on:click.prevent="currentEdit()") Add
template(v-else)
button(v-on:click.prevent="currentEdit()") Edit & Geocode
Creating our Edit Screen
After we copy the this.loc
to this.editLoc
we can use this to store our edited location until the user is happy with it and presses OK. At which point we will copy this.editLoc
to this.loc
and go back to the Current Location screen. If they press Cancel, we can simply just go back to the Current Location screen which will still show the original value of this.loc
.
In this screen we will have a series of <input>
tags. We will use the Vue attribute of v-model
to bind the value of each input to the corresponding value of location in this.editLoc
. Here’s the code:
#loc-8-component
#loc-8-view.row(v-if='uiState==="view"')
// ... previous code
#loc-8-edit.row(v-if='uiState==="edit"')
//-
This is the Edit state
User can input address or Lat/Long and do a geocode search
When the Geocode completes - goes to Choice state
h2 Edit Location
label(for="l8address") Address
input(type="text" v-model="editLoc.address" id="l8address")
label(for="l8address2") Address2
input(type="text" v-model="editLoc.address2" id="l8address2")
label(for="l8city") City/Town
input(type="text" v-model="editLoc.city" id="l8city")
label(for="l8state") State/Province/Region
input(type="text" v-model="editLoc.state" id="l8state")
label(for="l8zip") Zip/Postal Code
input(type="text" v-model="editLoc.zip" id="l8zip")
label(for="l8country") Country
input(type="text" v-model="editLoc.country" id="l8country")
label(for="l8lat") Latitude
input(type="text" v-model="editLoc.lat" id="l8lat")
label(for="l8long") Longitutde
input(type="text" v-model="editLoc.long" id="l8long")
button(v-on:click.prevent="editOk()") OK
button(v-on:click.prevent="geocode()") Geocode
button(v-on:click.prevent="editCancel()") Cancel
#loc-8-spinner(v-show="request") Requesting....
The great thing about v-model
is that when the value of this.editLoc
changes, whether it’s by the user or our code, the value of the <input>
will change at the same time.
The #loc-8-spinner div will only show if this.request
is true. We’ll use this to show the user we are in the middle of our Ajax or REST request to WordPress. As shown above, we added this key to our data
object and gave it an initial value of false
.
Let’s see what happens when a user presses a button. In the method section of our main Vue component we will have these straightforward functions. We’ll define the geocode() function in the next section.
//...previous code
// OK pressed so go back to View Stat
// Move editLoc to loc
// Check to see if address or loc has changed
editOk: function() {
this.copyLocation(this.editLoc, this.loc);
this.uiState = "view";
},
// Cancel pressed so go back to View State
editCancel: function() {
this.uiState = "view";
},
Creating a method and response to mimic a request to geocode the address
For this prototype, let’s mimic an Ajax request using the setTimeout function. The geocode function will be defined in our methods
section.
let vm = new Vue({
el: '#loc-8-component',
data: {
...//previous code
},
methods: { ... },
geocode: function() {
this.request = true;
let that = this;
setTimeout(
function() {
that.request = false;
that.results = fwpResults.results;
that.uiState = 'choice';
that.selected = null;
}, 500);
},
First we set request to true
so our spinner shows. We put the value of this
in a variable we can use in the setTimeout function. When the our function is finally called, it will turn off our spinner by setting that.request
to false
. It will set our results to an array of locations and set our uiState to ‘choice’ so that our third ‘screen’ will now appear in the WordPress meta box. The that.selected
will hold the index in the that.results
array if the user selects one of the locations in the next screen.
Creating our Choice Screen
This screen will show a list of possible locations that the user can select from. We will use Vue’s v-for
attribute to allow us to enumerate over the results array. We will put each item in the results array in its own <li>
tag. There are three things we need to do for each <li>
tag:
- Show the address
- Bind a click event that will pass the index of the array to a function
- Make its class ‘selected’ if was the last one clicked on
Here’s the PUG for that:
#loc-8-choice.row(v-if='uiState === "choice"')
ul
li(v-for='(item, index) in results' v-bind:class="{'selected': selected == index}" v-on:click.prevent='choiceSelect(index)')
p {{ fullAddress(item) }}
p {{ item.lat + ', ' + item.long }}
Our ‘choiceSelect’ function will only need to change the value of this.selected
to the index assigned to the <li>
that was clicked on. Because of Vue’s data-binding the <li>
of the same index will automatically get the class ‘selected’
// If user clicks on address, assign selected to index
choiceSelect: function(index) {
this.selected = index;
},
Now let’s add some buttons
#loc-8-choice.row(v-if='uiState === "choice"')
ul
li(v-for='(item, index) in results' v-bind:class="{'selected': selected == index}" v-on:click.prevent='choiceSelect(index)')
p {{ fullAddress(item) }}
p {{ item.lat + ', ' + item.long }}
button(v-show="selected != null" v-on:click.prevent="choiceUseSelected()") Use Selected
button(v-on:click.prevent='uiState="edit"') None of these, Search again
Here we are only showing the ‘Use Selected’ button once a selection is made. Otherwise, the ‘None of these’ buttons simply sets ‘uiState’ to edit which will put the user back to the ‘Edit’ screen.
The ‘choiceUseSelected’ function will take copy the location at the this.selected
index to the this.loc
location and put the user back in the first screen showing the new current location.
choiceUseSelected: function() {
this.copyLocation(this.results[this.selected], this.loc);
this.selected = null;
this.uiState = 'view';
},
What’s next
That gives a pretty good overview so far. During the prototype process we also created a second Vue component called ‘map-component’ to a add a map showing the location. Because we only need to pass in the location or an array of locations, we can re-use this map component in different contexts.
Unfortunately, we won’t go into making this map component for now as we want to get into adding interface to WordPress. Stay tuned for part II.
This codepen is not exactly the same as some of this code as this also adds the map-component and few other divs to format the final product a bit.
See the Pen Loc-8 component – vPost by Jerod Hammerstein (@jer0dh) on CodePen.