Building Data-Driven Web Applications with WordPress (WordPress App Design part 3)

In the last post in this series, we built a REST interface to our server data, which consists of Events and Venues. The REST interface provides the functionality to perform all the necessary CRUD (Create, Read, Update and Delete) operations from a client (e.g. a browser or Curl from a command line) and makes use of WordPress roles and capabilities to determine whether the user making the request has the necessary permissions to perform the requested operation.

Our goal in this post is to create a well-structured data-driven Web Application embedded in a WordPress page that makes use of good modern design patterns and leverages the REST interface we built in the previous tutorial.

This is what we will be creating:

The embedded app above allows you to view, create and edit and delete Venues. I put it on the page by simply adding a shortcode, [ev-venues] to my page. If you’ll remember from when we were building the REST interface, we didn’t put any restrictions on reading Venues, but you need special access to create, edit or delete venues. Since you don’t have any permissions on my this server, you’ll only be able to list and see the details of venues. Here though is a video of what a user with full permissions can do with this:

The Implementation

I’ve put all of the code up on GitHub as a plugin that you can download and install on your own WordPress server, or at least to make it easier to follow along with your own copy of the code.

WordPress, JavaScript & Shortcodes

First, it’s important to understand how to properly get JavaScript pages included into a WordPress page.  For a script to be included, it must either be explicitly enqueued, or be registered with WordPress and be a dependency of an enqueued script.

To register a script, we use wp_register_script:

wp_register_script($handle, $src, $deps, $ver, $in_footer );

$handle - Used to reference this script when later printing it or declaring it as a dependency for another registered script

$src - Path to the script

$deps - Array of script handles

$ver - Optional script version number

$in_footer - Set this to true to put the script in the footer.

In general, pages load much faster if you put your script declarations in the footer so I always set $in_footer to true if possible.

Any web application we build is going to have some core JavaScript that will always be needed. We’ll put that in a file called ev-ui.js. We’ll go over the details of that file in a moment. First, let’s get it onto the page. Here’s an updated version of events_and_venues.php, mentioned in the previous tutorial in this series. The first addition you’ll notice is:

add_action('init', 'register_events_and_venue_scripts');
function register_events_and_venue_scripts() {
    wp_register_script('ev-ui', 
        plugins_url('ev-ui.js', __FILE__), array(), false, true);

    wp_localize_script('ev-ui', 'EVUIEnv', array(
        'tpl_dir' => 
            plugins_url('/components/templates/', __FILE__),
        'theme_dir' => get_bloginfo('template_url')
    ));
}

register_events_and_venue_scripts() will get called when the init action is called on the plugin. This will in turn register the ev-ui.js script, which is found in the same directory as the plugin, has no dependencies, and will be placed in the footer. The file won’t be rendered to the page yet, but at least WordPress now knows about it, with its handle ev-ui.

Next, we need a mechanism for our various scripts to reference other files on the server (e.g. templates). We don’t want to hardcode server paths into scripts. Instead, we use  wp_localize_script to include some dynamic JavaScript on the page. The code above will declare an object in JavaScript called EVUIEnv which will have two properties, tpl_dir which points to the templates folder on the server, and theme_dir which points to the theme directory.

The second addition to events_and_venues.php scans the components directory for PHP files and loads these in:

// Load all of the php files in the components folder
$file_tmp = glob(dirname(__FILE__).'/components/*.php', 
    GLOB_MARK | GLOB_NOSORT);
foreach ($file_tmp as $item){
    if (substr($item, -1) != DIRECTORY_SEPARATOR) {
        require_once($item);
    }
}

Each of these components will be embeddable on a page with its own shortcode. Take a look at components/ev-venues.php:

<?php
add_shortcode('ev-venues', 'ev_venues_handler');
function ev_venues_handler($atts) {
    global $ev_add_venues_scripts;

    $ev_add_venues_scripts = true;

    return '<div id="ev-venues">
                <div class="ev-venues-list"></div>
                <div class="ev-venue-details"></div>
            </div>';
}

add_action('wp_footer', 'ev_enqueue_venues_scripts');
function ev_enqueue_venues_scripts() {
    global $ev_add_venues_scripts;

    /* Test if the 'ev-venues' shortcode exists on the page */
    if ($ev_add_venues_scripts) {
        wp_enqueue_script('ev-venue-model', 
            plugins_url('js/models/venue-model.js', __FILE__),
            array('backbone', 'underscore'), false, true);
        wp_enqueue_script('ev-venue-list-view', 
            plugins_url('js/views/venue-list.js', __FILE__),
            array('ev-ui', 'jquery', 'backbone', 'underscore'), 
            false, true);
        wp_enqueue_script('ev-venue-details-view', 
            plugins_url('js/views/venue-details.js', __FILE__),
            array('ev-ui', 'jquery', 'backbone', 'underscore'), 
            false, true);
        wp_enqueue_script('ev-venues', 
            plugins_url('js/venues-app.js', __FILE__),
            array('ev-venue-model', 'ev-venue-list-view', 
                  'ev-venue-details-view'), false, true);
    }
}
?>

First we define the ev-venues shortcode. When [ev-venues] is placed on a page, the HTML returned from ev_venues_handler() will be rendered instead. In the same handler, we also set the global $ev_add_venue_scripts to true.

Next we tell WordPress to call ev_enqueue_venues_scripts() when executing the wp_footer action. Essentially, every time a footer is rendered on a page, it will first check if $ev_add_venue_scripts is set to true (i.e. the ev-venues shortcode is on this page). If that’s the case it will enqueue the various scripts required for this component. Note that many of the scripts declare a dependency on ‘ev-ui’, which will ensure ev-ui.js is included on the page before the current script.

Ok, so now we understand how to manage scripts. How best should we structure our code though to make most effective use of our REST interface? You can write your own code the makes  AJAX calls to the REST interface to your heart’s content. In this example, I’ve chosen to go with the most excellent Backbone.js library.

backbone

Backbone will allows us to cleanly separate our code into Models, Collections, Views and Routers.

Models

Models know about our business objects, Venues. The Venue model knows how these Venue objects should communicate with the server to perform the necessary CRUD operations, what kind of properties they contain, etc. Fortunately, the REST interface we created in the previous tutorial was designed exactly in line with how Backbone.js expects a REST interface to behave.

Collections

Collections are essentially ordered sets of Models. In our example, we’ll be able to use a VenueCollection to manage our list of Venues.

Views

Views manage the display and user interaction for a specific set of features. Say for example we want to have a View that manages a list of Venues. The view would get the list rendered, update the display as necessary when the list changes and would know what to do when the user clicked the “New Venue” button.

Routers

Routers allow us to provide linkable, bookmarkable, shareable URLs for important locations within our app. Assuming you haven’t clicked anything in the embedded Venues app above, (don’t worry if you couldn’t resist and clicked around), take a look at the URL in the adress bar. Now, click on the first Venue (Wang Theatre) and you’ll see ‘#venues/6′ appended to the URL.

Now click on some of the other Venues, and you’ll see that no only will the details change in the display, but the  ID will change in the URL.

These URLs are all in your browser’s history. Hit the back button in your browser, and you’ll be back to the Wang Theatre details, all within this same post. Click back again and the Venue details will disappear.

The difference here with a normal WordPress page, and this is key, is that the whole page wasn’t refreshed with every click, not even when we hit the back button on the browser. There was only a request for new data which repopulated the Venue details view. You can bookmark or share any of these URLs and get back to exactly where you were within the app at a later time. You could actually send someone a link to this blog post, with the venues list providing the details of a specific venue. Very useful stuff indeed.

App Code

Alright, let’s dive into some of the JavaScript for the ev-venues app. We already know about ev-ui.js. This file defines an object called EVUI.  When initialized, EVUI overrides and creates some Backbone methods.

For example, this link will directly hit our REST interface and returns the list of venues in JSON format. Backbone needs to know that any objects returned from our REST interface can be found in the objects property of the returned data (requests for single objects will return the object in an object property). To do so, we override the parse() method of Backbone.Collection as follows:

Backbone.Collection.prototype.parse = function(response) {
   if (response.hasOwnProperty('objects'))
      return response.objects;
   else
      return response;
};

As well, EVUI provides methods for loading and managing templates that we will explain shortly.

Venue Models & Collections

Let’s take a look at the Venue model in components/js/models/venue-model.js.

Venue is defined as a Backbone Model with its urlRoot set to ‘/rest/venues/’. This is almost all we have to do to manage all the CRUD operations with the REST interface located at http://blattchat.com/rest/venues/. You’ll see that we also override the url() method to ensure that requests always end with a slash (/) since our REST interface requires that.

VenueCollection is defined as a Backbone Collection with its urlRoot also set to ‘/rest/venues/’ and its model is Venue, indicating that it is a collection of Venues.

Venue Views

components/js/views/venue-list.js defines VenueListView, a Backbone View which manages the list of Views. The details for a particular Venue are managed by VenueDetailsView in components/js/views/venue-details.js.

I’m not going to go line by line here, but rather give you an idea of what’s going on here. The rest should be fairly  self-explanatory.

When VenueDetailsView is initialized, we get the view’s template ‘venue-details’ using the EVUI.tpl.get() as defined in ev-ui.js.

We also tell Backbone to call this view’s render() method whenever the model, a Venue, changes. That is a pretty powerful aspect of Backbone, not to be overlooked. We can have multiple views on the page, any of which could be changing the model. Instead of having to keep track of who needs to know what and when, we know that if anyone changes the Venue in question, the VenueDetailsView will get re-rendered with the new values. That definitely keeps the spaghetti code at bay.

The render() function uses the the venue-details template and replaces variables in the template with values from the model. For instance, with the following code in the venue-details template:

<input type="text" id="venue_name" name="venue_name" value="<%= name %>" />

<%= name %> would get replaced with the name of the Venue and we the page would get rendered with something to the effect of:

<input type="text" id="venue_name" name="venue_name" 
   value="Wang Theatre" />

Next, we declare some event mappings:

events: {
 "click .save": "saveVenue",
 "click .delete": "deleteVenue",
 "click .close": "closeVenue"
}

The first event mapping declares, if a click event occurs on a element in the page with a class of “save”, then call the saveVenue()  method of this View.  If you look at the bottom of the venue-details template, you’ll see there is a Save button declared as:

<button class="save">Save</button>

If the user clicks this button, saveVenue() will get called because of the events declaration. saveVenue() in turn updates the model with the latest values from the form declared in the template and then either saves the model if we’re doing an update, or calls create() on the main application’s (to be explained shortly) list of venues (a VenueCollection) if we’re creating a new Venue. Both Venue.save() and VenueCollection.create() will automatically make the appropriate REST calls to the server based on their urlRoot attributes. In both cases though, we set wait to true to handle cases where the server returns an error. This prevents the model from being updated, only to find that there was some reason we shouldn’t have done that.

Now truth be told, if a user doesn’t have permission to create, edit or delete venues, they should not even be given those UI options. We’re not going to get into that in this exercise.  We can only go so far in this tutorial. That being said, if you take a look at deleteVenue() in VenueDetailsView, you’ll see that we call the user_can method on the server with the parameter, cap=delete_venues. Basically, this is asking the server, does the current user belong to a role which has the delete_venues capability. Are they allowed to delete venues? The user_can() method returns its answer in the JSON result in the msg attribute. If the user is allowed to delete venues, we will go ahead and delete this venue. Otherwise, we’ll popup an alert. Don’t worry though, even if we didn’t do this check in the JavaScript, the server wouldn’t let a user delete a venue if they weren’t allowed to delete venues.

I’ll leave components/js/views/venue-list.js as an exercise to the reader. It’s much simpler than venue-details.js.

The Router

Finally, we have the router defined in components/js/venues-app.js. First, we define our routes:

routes: {
 "": "listVenues",
 "venues/new": "newVenue",
 "venues/:id": "venueDetails"
 }

The first route is the default route, relative to the post in which the app is embedded. If you just load up this blog post directly, it will use the default route and listVenues() will get called. Relative to this post, if you navigate to #venues/new, newVenue() will be called. And if you navigate to #venues/100, venueDetails() will be called with the ID (100 in this case) passed in as the argument.

listVenues() creates venueList as a VenueCollection if it has not already been defined. Remember we referenced venueList previously in the VenueDetailsView when creating a new Venue. venueList.fetch() is called to retrieve the list of Venues from the server and populate venueList with the results. If that was successful, we create a VenueListView and pass the venueList in as its model (are you starting to see how this comes together or have I completely lost you?). We then take the rendered HTML of the VenueListView and use that to fill the contents of any elements on the page whose class is ev-venues-list, which in fact is exactly what is produced by the output of our [ev-venues] shortcode.

newVenue() and venueDetails() operate very similarly.

Getting the Ball Rolling…

Last but not least, in the router we have:

jQuery(document).ready(function() {
   EVUI.tpl.loadTemplates(['venue-list-head', 'venue-list-item', 
                           'venue-details'], function() {
      app = new AppRouter();
      Backbone.history.start();
   });
});

When the document is ready, we call EVUI.tpl.loadTemplates() with a list of templates we want to preload (to be used by the various views). loadTemplates() goes through the array of names and looks for HTML files using those names, located in the components/templates folder. Once those have all been successfully loaded, we create an instance of the router, which will simply initialize the router, and then call Backbone.history.start(), which will cause the routes to be evaluated.

Summary

And that is it!  We’ve built a REST interface using WordPress and then built a data-driven application which was embedded in this blog post using a simple shortcode.  If you stuck through all the details here, congratulations and thank you very much for spending the time reading this.  I hope you found this information useful.  It may seem a bit daunting at first, but once you go through it and get the hang of it, it’s quite straightforward to create your own apps, or add your own functionality.

Post comments below if you have any thoughts or questions.  I value your feedback and would be interested to hear if anyone plays around with this.

Tagged , , , . Bookmark the permalink.

8 Responses to Building Data-Driven Web Applications with WordPress (WordPress App Design part 3)

  1. Pingback: Building a REST interface with WordPress (WordPress App Design Part 2) - Alan Greenblatt

  2. Pingback: Modern Web App Design with Wordpress – Part 1 - Alan Greenblatt

  3. Alex Ko says:

    Most excellent post I have ever seen…wow.
    Thanks for sharing your valuable knowledge and experience.

  4. Man, this post series is really good, thanks for sharing this

  5. Jon says:

    I’m trying to extend this code to allow for a third kind of model, but for some reason I can’t make the REST aspect work. I added a new controller caterers_controller.php and a new model caterer.php but when I go to mydomain/rest/caterers/ I get “Call to a member function paginate() on a non-object”.

    If I add in a line in index_json in EVPublicController like “echo $this->model” then my requests to rest/events and rest/venues fail with “Object of class Event [or Venue] could not be converted to string”, while my request to rest/caterers gives “{status: “ok”}”. If I instead add a line that says “echo $this->model” then all three requests fail with “Object of class *Controller could not be converted to string”. So it seems like the $this is the right thing, but the CaterersController’s model isn’t a proper model, but I’m not sure why.

    Sorry that went on for a while … do you have any thoughts on how to proceed? Thanks a lot for a fantastic article series!

  6. Jason says:

    Hi Alan,

    First of all, thank you for such an awesome tutorial, it opened up a lot of possibilities for me to develop my apps.

    I am having a very interesting problem here, in the EVPublicController, the current_user_can method within the user_can method always return false even though I have added the “delete_venues” capability in the roles capabilities database. I double checked this by forcing the user_can method to always return true and I can delete the venues fine.

    Any thought about this?

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>