JavaScript - - By Almir Bijedic

Single Page Apps Using AngularJS and the WordPress REST API

This article was peer reviewed by Jay Shields and Simon Codrington. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

Automattic and the WordPress community are working on integrating the WordPress REST API plugin into the WP core, which means that the API will be available without the actual plugin, and we could see more of WordPress integration with front-end JavaScript frameworks.

This would allow a faster time to market for applications, since some basic functionality that is often implemented from scratch in JavaScript apps is already implemented; we could use WordPress’ user management, WooCommerce for product handling, static pages, and the list goes on! It is also possible to add custom endpoints to the API.

One way to use this API would be to include a front-end JS framework as part of a WordPress theme. This can be inconvenient, though, as it enforces a non-standard directory structure and is generally not a best-practice use of either the framework or the WordPress theme.

A better solution is to use a REST API to expose WP data and make two apps that are completely structurally separated, but connected with a “session sharing” technique, in order to use the best from both.

In this tutorial, I will show you how to do this “session sharing”, where WordPress is used for user management and login, and on the client-side, an Angular app that allows easy, SPA-like (Single Page Application) post editing for authorized users.

You can find the code for the WordPress child theme and the Angular client app on GitHub:

Requirements

Before starting this tutorial, it is recommended that you already have some experience with the following:

Additionally, we will be using:

  • Vagrant (two virtual machines, one for WP and the other for running the Angular app on a grunt server)
  • Yeoman (to scaffold our Angular app)
  • Grunt

as development tools, but you should be also fine if you use some other way to run your local WP site and other or no scaffolding tool.

A WordPress REST API

In order to achieve our goal architecture, we have to install the WordPress REST API plugin, which is currently in it’s beta phase and there are things to consider, see here.

The official recommendation is not to use version 2 in production, as the latest stable version is version 1. Also, the rumor is that the plugin will never get a stable version, but instead when the plugin is ready to move out of beta, it will be released as part of the WordPress core. In this tutorial we will use the version 2 plugin nonetheless.

Authentication

There are several authentication methods that can be used to secure the REST API (see here for details), but let’s take a brief look at the pros and cons of each:

Basic Authentication

This is the most basic authentication type, where we would send the username and password in the header with every request. It requires SSL, as otherwise we would open the username and password to attack. Also, we are giving the Client ID and secret to the client.

Cookie Authentication

The standard, built-in wordpress login system also works. The trouble with this approach is that in order to avoid CSRF issues, we have to use it on the inside of WP. Since we need to access the API from outside, from another app, this approach is not for us.

OAuth Authentication

We are going to be using a non-standard way of implementing OAuth here. We will not let the user authorize an application explicitly, but instead we will seamlessly hook into the WordPress login function, authorize our Angular app for the user and retrieve an access token, which we forward to the JS app in order to authorize the API requests.

It’s very important to mention that, even with this approach, we need SSL. Since this is an educational tutorial we will not be introducing the complexity of SSL even though, regarding the code itself, there would be no difference with or without SSL.

This approach should be preferred over basic auth, since we do not have to store the username and password of the user in the browser or give the Client ID and Secret to the user. The OAuth approach is only possible with a plugin, and for this reason we are going to use WP OAuth Server.

Sharing the Token

After we generate the token upon logging in to WP, we need to somehow share it with our JS app. There are multiple ways to do this, depending on your needs and personal preferences. In this tutorial we will describe two of these approaches. It’s worth mentioning that both of these approaches might be constrained by browser version.

Setting Wildcard Domain Cookie

The first option, which is possible only if both apps are on same subdomain, is to set a wildcard cookie and then use that cookie on Angular side. If using this approach, it’s important to take care of your local hosts file, in order to have the same subdomain.

I personally use Vagrant and Vagrant host manager, and then set the hostname of the WordPress VM to sitepoint.local and the other Vagrant VM’s hostname to app.sitepoint.local. This will automatically edit your hosts file like this:

192.168.33.16  app.sitepoint.local  # VAGRANT: 297067a67f66f631aa1962e3b49ab73c (default) / cc52bb5a-3972-4b55-8e0c-0c805b9c6f1c
192.168.33.15  sitepoint.local  # VAGRANT: 6a5b8c757547fe9f0a32a0c747a91a79 (default) / b08c3e7d-cba8-4130-a11b-b26494574280

This is the approach we will use in this tutorial, but if you feel that this is an additional complication for you, consider the iframe option.

Posting to localStorage via iframe

This approach is better for someone who wants to avoid handling cookies and/or does not have the WP app and Angular app on same sub-domains. We expose an iframe from the SPA to WP and then we post a message to the iframe, inside which there is a listener waiting for incoming messages to take the access token and store it in the JS app’s localStorage, for later use. I will explain the modifications needed in a separate section at the end of the article. See here for browser compatibility.

The Server

First we are going to create a template for the login screen containing the code to generate the access token. Then, we will hook into some login/logout functions in order to handle the cookie (or iframe, if you go with that option).

Create the server VM

Time to grease some elbows! I like starting out a WordPress project by cloning the Scotchbox Vagrantfile .

Open your favorite terminal application and navigate to the folder where you wish to keep the project. In my case this is /boxes. Then, clone the Scotchbox repo and navigate inside.

cd /boxes
git clone https://github.com/scotch-io/scotch-box.git sitepoint-wp-rest-api
cd sitepoint-wp-rest-api

# remove .git folder in order not to get confused if you later initialise your own repository
rm -rf .git

# Initialise a vagrant machine
vagrant up

At this point, you may want to open the sitepoint-wp-rest-api folder in your favorite IDE or text editor, since we need to edit the Vagrantfile.

This is how the Vagrantfile looks by default when cloned:

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|

    config.vm.box = "scotch/box"
    config.vm.network "private_network", ip: "192.168.33.10"
    config.vm.hostname = "scotchbox"
    config.vm.synced_folder ".", "/var/www", :mount_options => ["dmode=777", "fmode=666"]

    # Optional NFS. Make sure to remove other synced_folder line too
    #config.vm.synced_folder ".", "/var/www", :nfs => { :mount_options => ["dmode=777","fmode=666"] }

end

There are two thing to take care of here:

  1. Change the IP
  2. Change the hostname

In case you already have a network device or another VM running on 192.168.33.10, make sure to change that. You can choose any IP address as long as it’s in the private address space.

If you want to use the approach with sub-domain cookie, it is also important to change the hostname to a FQDN.

Optional: You can also increase the RAM/CPU of the VM. This can be especially handy later for the second VM ,when we will need to run bash npm install – it will take less time if we increase the performance of the VM. Check the commented-out part at the end of the code snippet for how to achieve this. Although it’s sometimes good to see how the system runs on 512MB RAM and 1 CPU (Vagrant’s default), for development purposes we want it to be fast. Later when testing/optimizing speed of the WP site, you can tune this down again.

The final Vagrantfile looks like this

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|

    config.vm.box = "scotch/box"
    config.vm.network "private_network", ip: "172.31.255.254"
    config.vm.hostname = "sitepoint-wp-rest-api.test"
    config.vm.synced_folder ".", "/var/www", :mount_options => ["dmode=777", "fmode=666"]

    # Optional NFS. Make sure to remove other synced_folder line too
    #config.vm.synced_folder ".", "/var/www", :nfs => { :mount_options => ["dmode=777","fmode=666"] }

    # Optional performance tweaking
    #config.vm.provider "virtualbox" do |v|
    #   v.memory = 2048
    #   v.cpus = 2
    #end
end

One last step before we turn on the VM: the vagrant-hostupdater plugin install. What this plugin does is, when you run vagrant up, it updates your local hosts file with the IP and hostname given in the Vagrantfile, you will see what that means in a few moments.

# Install Vagrant host udpater plugin
vagrant plugin install vagrant-hostsupdater

Now it’s time to boot up the machine:

vagrant up

You may get asked for the sudo password since vagrant-hostupdater will be editing the /etc/hosts file, which requires admin permissions.

This will take a minute (or a few minutes!) depending on whether you already have the scotch/box image locally – it has to be downloaded first if you do not.

Now, visit http://172.31.255.254/ and you should see the Scotchbox information page (what you actually see is the index.php located in the public folder of the cloned Scotchbox repository).

The result of what the hostupdater plugin did can be seen if you visit http://sitepoint-wp-rest-api.test/ – it’s still the same page. Sitepoint-wp-rest-api.test resolves to 172.31.255.254 via your local hosts file. From now on we will use this FQDN to access our REST API.

Install WordPress

Now that we have a server running, it’s time to install WordPress. Go into your local user’s folder and download WordPress. Unpack it, create a configuration file and then copy raw files to the public folder inside of sitepoint-wp-rest-api.

cd ~
curl -OL http://wordpress.org/latest.tar.gz
tar xzvf latest.tar.gz
cd ~/wordpress
cp wp-config-sample.php wp-config.php

# Do not forget to change /boxes to the actual folder onb *your* system
sudo rsync -avP ~/wordpress/ /boxes/sitepoint-wp-rest-api/public

Now, head back to your IDE and open wp-config.php. Fill in the DB credentials as found here. Additionally, it is a good practice to add WP_HOME and WP_SITEURL variables in the configuration files. They will override the values from the database, making it easier for versioning and changing between environments/hosts.

// wp-config.php

/** The name of the database for WordPress */
define('DB_NAME', 'scotchbox');

/** MySQL database username */
define('DB_USER', 'root');

/** MySQL database password */
define('DB_PASSWORD', 'root');

/** This is new, WP SiteURL and Home globals override */
define('WP_SITEURL', 'http://sitepoint-wp-rest-api.test/');
define('WP_HOME', WP_SITEURL);

Change these values, and save the file.

Visit http://sitepoint-wp-rest-api.test/ again, and this time you should see a WordPress install wizard.

WordPress install - step 2

Click next a few times and finally you should see a default WP site with the twentysixteen theme running.

Install the REST API plugin

Head to the WordPress REST API plugin page and download the plugin. Unzip the downloaded file and copy it to wp-content/plugins/. Go to http://sitepoint-wp-rest-api.test/wp-admin, click Plugins on the left menu, find WordPress REST API and activate it. No additional configuration or setup is needed for the API – it just works!

Now, if you visit http://sitepoint-wp-rest-api.test/wp-json/wp/v2/posts you should get a JSON array containing all the posts:

[
   {
      "id":1,
      "date":"2016-05-09T17:49:50",
      "date_gmt":"2016-05-09T17:49:50",
      "guid":{
         "rendered":"http:\/\/sitepoint-wp-rest-api.test\/?p=1"
      },
      "modified":"2016-05-09T17:49:50",
      "modified_gmt":"2016-05-09T17:49:50",
      "slug":"hello-world",
      "type":"post",
      "link":"http:\/\/sitepoint-wp-rest-api.test\/2016\/05\/09\/hello-world\/",
      "title":{
         "rendered":"Hello world!"
      },
      "content":{
         "rendered":"<p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing!<\/p>\n"
      },
      "excerpt":{
         "rendered":"<p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing!<\/p>\n"
      },
      "author":1,
      "featured_media":0,
      "comment_status":"open",
      "ping_status":"open",
      "sticky":false,
      "format":"standard",
      "categories":[
         1
      ],
      "tags":[

      ],
      "_links":{
         "self":[
            {
               "href":"http:\/\/sitepoint-wp-rest-api.test\/wp-json\/wp\/v2\/posts\/1"
            }
         ],
         "collection":[
            {
               "href":"http:\/\/sitepoint-wp-rest-api.test\/wp-json\/wp\/v2\/posts"
            }
         ],
         "about":[
            {
               "href":"http:\/\/sitepoint-wp-rest-api.test\/wp-json\/wp\/v2\/types\/post"
            }
         ],
         "author":[
            {
               "embeddable":true,
               "href":"http:\/\/sitepoint-wp-rest-api.test\/wp-json\/wp\/v2\/users\/1"
            }
         ],
         "replies":[
            {
               "embeddable":true,
               "href":"http:\/\/sitepoint-wp-rest-api.test\/wp-json\/wp\/v2\/comments?post=1"
            }
         ],
         "version-history":[
            {
               "href":"http:\/\/sitepoint-wp-rest-api.test\/wp-json\/wp\/v2\/posts\/1\/revisions"
            }
         ],
         "wp:attachment":[
            {
               "href":"http:\/\/sitepoint-wp-rest-api.test\/wp-json\/wp\/v2\/media?parent=1"
            }
         ],
         "wp:term":[
            {
               "taxonomy":"category",
               "embeddable":true,
               "href":"http:\/\/sitepoint-wp-rest-api.test\/wp-json\/wp\/v2\/categories?post=1"
            },
            {
               "taxonomy":"post_tag",
               "embeddable":true,
               "href":"http:\/\/sitepoint-wp-rest-api.test\/wp-json\/wp\/v2\/tags?post=1"
            }
         ],
         "curies":[
            {
               "name":"wp",
               "href":"https:\/\/api.w.org\/{rel}",
               "templated":true
            }
         ]
      }
   }
]

You can also try to query some other routes. Check the API documentation for a full list.

The API will only show information that is publicly available. Try adding a private post and request all the posts from the API again — the private post will not be included.

This is where the WP OAuth plugin comes into play. We do not want to keep the user’s username and password on the client side, but we do not want to send credentials with every request, and we cannot use the default WordPress login cookie on the Angular side.

Install the OAuth Server plugin

Go to the WP Oauth Server plugin page and download the plugin, unzip it and copy the unzipped folder into wp-content/plugins/. Then go again to http://sitepoint-wp-rest-api/wp-admin/, click Plugins on the left side menu and activate the OAuth Server plugin.

There are multiple ways to authorise with OAuth. The Authorization Code grant type allows a client app to post username and password to the auth server together with the Client ID and Secret and get back the access token which is later used as authorization code for every API request.

Create a child theme

Before we start implementing the WordPress template for the login page, we have to create a child theme in order not to mess with the original theme files. Under wp-content/themes create a new folder and call it twentysixteen-child. Then, create two files inside the newly created folder: functions.php and style.css.

Inside of functions.php add this:

<?php
// wp-content/themes/twentysixteen-child/functions.php

add_action( 'wp_enqueue_scripts', 'theme_enqueue_styles' );
function theme_enqueue_styles() {
    wp_enqueue_style( 'parent-style', get_template_directory_uri() . '/style.css' );
    wp_enqueue_style( 'child-style',
        get_stylesheet_directory_uri() . '/style.css',
        array('parent-style')
    );
}

This will enqueue both the original CSS from the twentysixteen theme, and our custom style.css after it, in order to make it easier to override the styles.

and into the style.css:

/*
 Theme Name:     twentysixteen Child Theme
 Author:         Almir Bijedic
 Author URI:     http://almirbijedic.rocks
 Template:       twentysixteen
 Version:        1.0.0
*/

/* Add Custom CSS after this line */

Don’t forget to go into the WordPress dashboard and actually activate the child theme. From the left side menu click Themes, find twentysixteen Child Theme and click activate.

Now that we have a child theme go ahead and add a new file, login.php, into twentysixteen-child, and put this inside:

<?php
// wp-content/themes/twentysixteen-child/login.php

/*
Template Name: Login Page
*/

$client_id = '';
$client_secret = '';

You can get the Client ID and Secret by going into Auth Server menu from the WordPress dashboard, in the left side menu, click the Clients tab, and add your application.

WP OAuth Server - Client ID and Secret generation 1

Name it ‘AngularApp’ and set the redirect to the relative url /login.

WP OAuth Server - Client ID and Secret generation 2

This application we’ve created will act as a client for our SPA, and every user logging in will be logged in via this same client.

WP OAuth Server - Client ID and Secret generation 3

Now go ahead and paste the Client ID and Secret into the template.

<?php
// wp-content/themes/twentysixteen-child/login.php

/*
Template Name: Login Page
*/

$client_id = 'CWwPjFsaCaXaT8qRl2UIwc9AI8Ex4i';
$client_secret = 'vHe1wyHYIk9Gv3OVU9YPSu124KbJif';

The next step is to redirect the user to the OAuth authorize page, which will in turn redirect the user to the usual wp-login.php with a special redirect in the query string needed for OAuth plugin to pickup the credentials that the user entered and generate the access_token.

Add the following beneath the client secret:

$url = site_url() . '?oauth=authorize&response_type=code&client_id=' . $client_id;

// Redirect to OAuth login page
header('Location: ' . $url);
die();

Go ahead and try it yourself, if the local URL of your WordPress site is http://sitepoint-wp-rest-api.test, visit http://sitepoint-wp-rest-api.test/?oauth=authorize&response_type=code&client_id=CWwPjFsaCaXaT8qRl2UIwc9AI8Ex4i. Do not forget to change the Client ID to your own.

After submitting the username and password, you will be redirected to /login?code=”code-here”, which will return a 404 page since we still did not set our template to an actual page on the ‘http://sitepoint-wp-rest-api.test/login’ URL. The code in the query string is the code which is used in the next step of authentication with OAuth.

Now, we need to submit this code together with the Client ID and Secret in order to get back the access token. So let’s go back to our template and add a POST call to the OAuth server.

<?php
// wp-content/themes/twentysixteen-child/login.php

/*
Template Name: Login Page
*/

$client_id = 'CWwPjFsaCaXaT8qRl2UIwc9AI8Ex4i';
$client_secret = 'vHe1wyHYIk9Gv3OVU9YPSu124KbJif';

$code = false;
if(isset($_GET['code'])) {
    $code = $_GET['code'];
}

// If there is no code present, first get the code by redirecting to login page
if(!$code) {

    $url = site_url() . '?oauth=authorize&response_type=code&client_id=' . $client_id;
    header('Location: ' . $url);
    die();

} else {

    // Encode the client ID and secret with base64 in 
    // order to add it to the Authorization header
    $auth = base64_encode($client_id.':'.$client_secret);
    try {

        // Making the Call to get the access token
        $args = [
            'headers' => [
                'Authorization' => 'Basic ' . $auth
            ],
            'body' => [
                'grant_type'    => 'authorization_code',
                'code'          => $code
            ],
    ];

        // Send the actual HTTP POST with the built-in WordPress HTTP library.
        $json = wp_remote_post( site_url() . '?oauth=token', $args );

        if(is_array($json) && isset($json['body'])) {

            $json = json_decode($json['body']);

            // Retrieve the access token from the response
            $auth_token = $json->access_token;
            $user_id = get_current_user_id();


            // Set the cookie
            setcookie('wordpress_access_token', $auth_token, time() + 3600, '/', preg_replace('#^https?://#', '', rtrim(site_url(),'/')), 0);

            // Save the cookie to user meta
            // Can be useful for debugging or if needed to refresh the cookie
            update_user_meta($user_id, 'wordpress_access_token', $auth_token);
        } else {
            print_r($json);
            die();
        }

        // All set, redirect to the home page
        header('Location: ' . site_url());
    } catch (Exception $e) {
        var_dump($e);
    }
}

Now that our template is ready, go ahead and create a new WordPress page with the Template set to Login Page. Click Pages from the left side menu, choose Add New, title it with whatever you want, and the important thing is to set the template to Login Template, from the right side menu in the page creation screen.

Create login page

Do not forget to set permalinks to pretty, since we set the redirect URL in the OAuth clients to /login. To do this, go to the admin dashboard again, from the left side menu choose Settings —> Permalinks. Under Common Settings, choose Post name, and click Save.

Now you can visit http://sitepoint-wp-rest-api.test/login and try it out! After you log in you can open the developer tools and under Resources -> Cookies check if the wordpress_access_token is visible.

Check wordpress_access_token cookie

If you are using a Virtual Machine without Vagrant, it could happen that the VM cannot resolve sitepoint-wp-rest-api.test domain, because it is only set in your local hosts file. In case you have errors, ssh into the VM and add sitepoint-wp-rest-api.test domain to point to localhost.

127.0.0.1 sitepoint-wp-rest-api.test

The specific part of code that would trigger an error if this was the case is this one:

$json = wp_remote_post( site_url() . '?oauth=token', $args );

site_url() here will be set to (in my case) http://sitepoint-wp-rest-api.test, and the VM will not know how to resolve this.

Before we try to request something from the WordPress API, let’s do one more thing. Let’s use the meta field we set to the user to display the access_token of the current user on top left of the dashboard screen (exactly like the ‘hello dolly’ plugin!).

So go to the child theme of your WordPress theme, and in the functions.php file, add this:

// wp-content/themes/twentysixteen-child/functions.php

// Only for admin
if (current_user_can('manage_options')) {
    add_action('admin_notices', 'display_user_token');
}
function display_user_token() {
    $user_id = get_current_user_id();
    $auth_token = get_user_meta( $user_id, 'wordpress_access_token', true);
    echo $auth_token;
}

Now, log into the WP dashboard and copy the token. Open an incognito window (this is important, because you cannot queue the API with both the usual WP cookie for authentication and the OAuth access token. You will get an error back if both are present.) Go to http://sitepoint-wp-rest-api.test/wp-json/wp/v2/users/1/?context=edit

You should get back something like this

{
    "code":"rest_not_logged_in",
    "message":"You are not currently logged in.",
    "data": {"status" : 401}
}

Then try http://sitepoint-wp-rest-api.test/wp-json/wp/v2/users/1/?context=edit&access_token=ACCESS_TOKEN

Voila!

You can also try the cURL calls below in order to double check if everything works. The first call should return an error message and the other one should delete the ‘Hello World!’ post from WP.

# this one returns an error
curl -X DELETE http://sitepoint-wp-rest-api.test/wp-json/wp/v2/posts/1/

# this one should really delete the post -
# you can verify it in the WP dashboard
curl -X DELETE http://sitepoint-wp-rest-api.test/wp-json/wp/v2/posts/1/?access_token=<access-token>

It’s important that you pass in the right value of context, depending on it’s value you get different fields back. Check the WP REST API documentation for details.

We also have to make sure that if the user uses the normal wp-login.php without the redirect, they still get redirected to the proper page to retrieve the access token.

If the user is logged in and visits the /login page, the OAuth template code we created will run and the user will get a new access_token.

Add this to your functions.php:

// wp-content/themes/twentysixteen-child/functions.php

add_filter( 'login_redirect', 'ab_login_redirect', 10, 3 );
function ab_login_redirect( $redirect_to, $request, $user ) {
    return site_url() . '/login';
}

We also need to make sure that the cookie gets removed when the user logs out of WordPress.

// wp-content/themes/twentysixteen-child/functions.php

add_action ( 'login_form_logout' , 'ab_cookie_remove_logout' );
function ab_cookie_remove_logout() {
    setcookie('wordpress_access_token', "expired", time() - 3600, '/', preg_replace('#^https?://#', '', rtrim(site_url(),'/')), 0);
    wp_logout();
}

Now, your final functions.php file in the child theme should look like this:

<?php
// wp-content/themes/twentysixteen-child/functions.php

add_action( 'wp_enqueue_scripts', 'theme_enqueue_styles' );

function theme_enqueue_styles() {
    wp_enqueue_style( 'parent-style', get_template_directory_uri() . '/style.css' );
    wp_enqueue_style( 'child-style',
        get_stylesheet_directory_uri() . '/style.css',
        array('parent-style')
    );
}

if (current_user_can('manage_options')) {
    add_action('admin_notices', 'display_user_token');
}
function display_user_token() {
    $user_id = get_current_user_id();
    $auth_token = get_user_meta( $user_id, 'wordpress_access_token', true);
    echo $auth_token;
}

add_filter( 'login_redirect', 'ab_login_redirect', 10, 3 );
function ab_login_redirect( $redirect_to, $request, $user ) {
    return site_url() . '/login';
}

add_action ( 'login_form_logout' , 'ab_cookie_remove_logout' );
function ab_cookie_remove_logout() {
    setcookie('wordpress_access_token', "expired", time() - 3600, '/', preg_replace('#^https?://#', '', rtrim(site_url(),'/')), 0);
    wp_logout();
}

At this point, our API server is ready and it’s time to turn our attention to the Angular app.

The Client

We are going to use Yeoman to generate a boilerplate Angular application. The app will use the ngCookies module to retrieve to cookie and show a simple form, and use the $http module to retrieve and edit posts if the permissions of the logged in user are correct.

Note that you can also include the Angular WP API library in order to do the API calls, but we will use the default $http service for sake of simplicity and to show that we can use anything we want to integrate with WP. You can even use your own custom Ajax calls. Usually, in Angular, you would put the functions for sending requests to the API in a separate service, but for this tutorial we will keep it simple, and do it within the controller.

Create the client VM

We’re going to use a bare Ubuntu 14.04 Server vagrant box from Vagrantcloud, and use this SitePoint tutorial to create an application scaffold.

Go back to the folder where you installed the Scotchbox, in my case this is /boxes/sitepoint-wp-rest-api, and add a new folder, for the Angular app VM.

cd /boxes
mkdir sitepoint-wp-angular-app
cd sitepoint-wp-angular-app

# this will create a Vagrantfile which we will modify a bit and then run vagrant up
vagrant init ubuntu/trusty64

Now open the newly created Vagrantfile, you will see a lot of commented lines. Remove all of that code and replace it with this

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|

    config.vm.box = "ubuntu/trusty64"
    config.vm.network "private_network", ip: "172.31.255.253"
    config.vm.hostname = "app.sitepoint-wp-rest-api.test"
    config.vm.synced_folder ".", "/var/www", :mount_options => ["dmode=777", "fmode=666"]

    # Optional NFS. Make sure to remove other synced_folder line too
    #config.vm.synced_folder ".", "/var/www", :nfs => { :mount_options => ["dmode=777","fmode=666"] }

    config.vm.provider "virtualbox" do |v|
        v.memory = 1024
        v.cpus = 2
    end

end

Basically this is the same Vagrantfile as we used for the WP installation, but we have to change it to use another IP, since 172.31.255.254 is already in use.

Change the source image that is going to be used (the official ubuntu/trusty64 instead of Scotchbox), and last but not least, change the hostname by appending app to the FQDN we used for WP. This will allow us to set a cookie which will work on both – the main domain and the subdomain we just created.

Run vagrant up from the sitepoint-wp-angular-app folder.

When vagrant has finished, run vagrant ssh in order to ssh into the VM we just booted up. We are doing this so that we can generate the Angular app scaffold with Yeoman directly on the server, and then run a developer server with grunt connect. This will make the actual VM serve the app from app.sitepoint-wp-rest-api.test.

We could have done this from a local folder as well, without a second VM, but then we would have to make sure that nothing is running on port 80 on your local machine, which can be a hassle to do. This is a cleaner way and supports multiple setups like this.

Setup Grunt and AngularJS

Now we can use this SitePoint tutorial to scaffold our Angular app. A few preparation steps are required, though.

Go back to your terminal where you have your SSH connection to the bare ubuntu VM, change the directory to /var/www and install npm.

cd ~
curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
sudo apt-get install -y nodejs
cd /var/www

From this point you, can follow the linked tutorial to get a basic, scaffolded app. Run the commands from the tutorial on your VM and not locally, starting from the beginning and stopping once grunt server is run.

Note that you may need to run some of the commands in the tutorial with sudo. Also, when choosing which modules to include, be sure to tick angular-cookies.js, angular-route.js and include Bootstrap, everything else you can answer with no. You may also need to run sudo apt-get install git, in order for bower to work.

Before running grunt server make sure to disable livereload or the VM will try to connect to a browser, won’t find it, and will throw an error. In /var/www/Gruntfile.js change connect:livereload to connect:test.

One final step before we can run grunt server: in the same Gruntfile.js, in the root of the app, find the comment that says // The actual grunt server settings a few lines down, underneath options key, and change the hostname to ‘0.0.0.0’ and the port to 80, and also the port under test key.

The final configuration for connect should now look like this:

// /var/www/Gruntfile.js

// ...
    // The actual grunt server settings
    connect: {
      options: {
        port: 80,
        // Change this to '0.0.0.0' to access the server from outside.
        hostname: '0.0.0.0',
        livereload: 35729
      },
      livereload: {
        options: {
          open: true,
          middleware: function (connect) {
            return [
              connect.static('.tmp'),
              connect().use(
                '/bower_components',
                connect.static('./bower_components')
              ),
              connect().use(
                '/app/styles',
                connect.static('./app/styles')
              ),
              connect.static(appConfig.app)
            ];
          }
        }
      },
      test: {
        options: {
          port: 80,
          middleware: function (connect) {
            return [
              connect.static('.tmp'),
              connect.static('test'),
              connect().use(
                '/bower_components',
                connect.static('./bower_components')
              ),
              connect.static(appConfig.app)
            ];
          }
        }
      },
      dist: {
        options: {
          open: true,
          base: '<%= yeoman.dist %>'
        }
      }
    },
    // ...

Run sudo grunt serve from /var/www folder ( or wherever you scaffolded your app ). It’s important that you run grunt serve from the VM and not locally, because you want the app to be served from the virtual machine, running on a local IP to which the app.sitepoint.local host resolves.

Visit http://app.sitepoint-wp-rest.api.test, and you should see the scaffolded app running! Additionally, if you inspect the cookies on the SPA, you should see the same wordpress_access_token as in your WP site!

Add the post admin functionality

Now we need to add some basic functionality to show WP posts and, depending on user’s capabilities, show or hide the edit post button. In the case where the user can edit, we’ll allow him to update the post title and/or content when Save is pressed.

Let’s start with the HTML. In the app/views/ folder add posts.html. This view will be used to show a list of posts and a form for editing the selected post.

<!-- app/views/posts.html -->
<div class="container">
    <h1>Posts</h1>
    <ul>
        <li ng-repeat="post in posts">
            <a href="{{post.link}}"
               target="_blank">{{post.title.rendered}}</a>
            <a ng-click="editPost(post.id)"
               ng-show="user.capabilities.edit_posts">Edit</a>
        </li>
    </ul>

    <form name="editPostForm" id="editPost" style="display: none;" class="edit">
        <label for="title">Title:</label>
        <input type="text" id="title" ng-model="post.title">
        <label for="content">Content</label>
        <input type="text" id="content" ng-model="post.content"/>
        <a ng-click="updatePost()">Save</a>
    </form>

    <p id="responseMessage"></p>
</div>

This is the app/scripts/app.js file. We’ll remove some default stuff created by the generator and add the route configuration for the posts page.

// app/scripts/app.js

angular
    .module('publicApp', [
        'ngRoute',
        'ngCookies'
    ])
    .config(function ($routeProvider) {
        $routeProvider
            .when('/', {
                templateUrl: 'views/posts.html',
                controller: 'PostsCtrl',
                controllerAs: 'posts'
            })
            .otherwise({
                redirectTo: '/'
            });
    });

Lastly, we need to add the posts controller. Under app/scripts/controllers/ add a new file, posts.js and copy this inside:

// app/scripts/controllers/posts.js

angular.module('publicApp')
  .controller('PostsCtrl', function ($http, $scope, $cookies, $httpParamSerializerJQLike) {
    // ...
  });

Let’s add the necessary functionality. First, we retrieve the API endpoint /users/me in order to get the current user ID. Then, we use that ID to get user capabilities in order to know whether to show or not to show the Edit button.

// app/scripts/controllers/posts.js

angular.module('publicApp')
    .controller('PostsCtrl', function ($http, $scope, $cookies, $httpParamSerializerJQLike) {
        var apiUrl = 'http://sitepoint-wp-rest-api.test/wp-json/wp/v2';

        // Retrieve user permissions
        // First get the current user id
        $http({
            method: 'GET',
            url: apiUrl + '/users/me/?access_token=' + $cookies.get('wordpress_access_token')
        }).then(function successCallback(response) {

            // second API call to get more details about the current user, e.g. capabilities
            $http({
                method: 'GET',
                url: apiUrl + '/users/' + response.data.id + '/?context=edit&access_token=' + $cookies.get('wordpress_access_token')
            }).then(function successCallback(response) {
                console.log(response.data);
                $scope.user = response.data;
            }, function errorCallback(response) {
                console.log(response);
            });
            $scope.user = response.data;
        }, function errorCallback(response) {
            console.log(response);
        });

    });

We also need to retrieve all posts:

// app/scripts/controllers/posts.js

// ...

        // Retrieve all posts
        $http({
            method: 'GET',
            url: apiUrl + '/posts'
        }).then(function successCallback(response) {
            console.log(response.data);
            $scope.posts = response.data;
        }, function errorCallback(response) {
            console.log(response);
        });

// ...

Next, we have to add the function for the Edit button, which will iterate through our posts array and put the selected post title and content into our form for editing.

// app/scripts/controllers/posts.js

// ...

        // Edit post button
        $scope.editPost = function(id) {
            document.getElementById('editPost').style.display = 'block';
            $scope.post.title = '';
            $scope.post.content = '';
            $scope.post.id = id;
            for (var i = 0; i < $scope.posts.length; i++) {
                if ($scope.posts[i].id === id) {
                    $scope.post.title = $scope.posts[i].title.rendered;
                    $scope.post.content = $scope.posts[i].content.rendered;
                }
            }
        };

// ...

We’ll also need a save function that will trigger a POST request to the /posts endpoint, with the changed title and content value, when the Save button is clicked.

// app/scripts/controllers/posts.js

// ...

        // Update post
        $scope.updatePost = function() {
            $http({
                url: apiUrl + '/posts/' + $scope.post.id + '/?context=edit&access_token=' + $cookies.get('wordpress_access_token'),
                method: "POST",
                headers: {
                    'content-type': 'application/x-www-form-urlencoded'
                },
                data: $httpParamSerializerJQLike({
                    title: $scope.post.title,
                    content: $scope.post.content
                })
            }).then(function successCallback(response) {
                console.log(response);

                for (var i = 0; i < $scope.posts.length; i++) {
                    if ($scope.posts[i].id == $scope.post.id) {
                        $scope.posts[i] = response.data;
                    }
                }

                document.getElementById('editPost').style.display = 'none';
                document.getElementById('responseMessage').innerHTML = 'Succesfuly updated post.';

            }, function errorCallback(response) {
                console.log(response);
            });
        };

// ...

Your final posts.js file should look like this.

Before trying out the app, some additional cleanup is needed. Delete the app/scripts/controllers/main.js file, and app/views/main.html. Then go to index.html and change

<div ng-include="'views/main.html'" ng-controller="MainCtrl"></div>

to

<div ng-include="'views/posts.html'" ng-controller="PostsCtrl"></div>

Also, make sure the ng-app value in index.html matches the module name in app.js.

That’s it! You should now be able to login into WordPress as admin (or some other role that can edit posts) and when you go into the Angular app, you will see and edit button next to each post. Clicking the edit button will open the editing form and show a Save button, which will persist the data back to the WordPress DB.

You can continue experimenting on your own and create a menu that looks the same on WP and in the SPA in order to make the transition between the two more seamless, and in case you want to completely password protect the app, when a user without a valid cookie comes along you can redirect them to the WP login.

Using the iframe option

In order to use the iframe and localStorage option for sharing the token that I mentioned earlier, we first need to create a static HTML file in the Angular app root folder.

Let’s name that file tunnel.html and paste in the following markup:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>The Dark Portal</title>
    <script>
        function receiveMessage(event)
        {
            // Do we trust the sender of this message?
            if (event.data.prefix !== "unique_key_sitepoint")

               return;
            for (var key in event.data) {
                if (event.data.hasOwnProperty(key) && key != "unique_key_sitepoint") {
                    localStorage.setItem(key, event.data[key]);
                }
            }
        }

        window.addEventListener("message", receiveMessage, false);
    </script>
</head>
<body>
    <h1>Psst</h1>
</body>
</html>

Now, we need to insert this as an iframe into WordPress. Go back to functions.php under wp-content/themes/twentysixteen-child/ and add this:

// wp-content/themes/twentysixteen-child/functions.php
// ...

add_action( 'genesis_footer', 'ab_share_token_iframe' );
function ab_share_token_iframe() {
    echo '<iframe id="ab-login-iframe" style="display:none"></iframe>';
}

And last but not least, we need to enqueue a script in WordPress which will be posting to the iframe. Let’s enqueue the script and also localize it, in order to pass the auth token to the front-end.

Create a new folder under wp-content/themes/twentysixteen-child/, name it js and then inside create a file main.js with this content (twentysixteen already has jQuery enqueued):

// wp-content/themes/twentysixteen-child/js/main.js

(function($) {
     $(document).ready(function() {
         var win = document.getElementById('ab-login-iframe');
         var payload = {
             oauthToken: abData.accessToken,
             prefix: "unique_key_sitepoint"
         };

         win.src = abData.src;
         win.onload = function() {
             win.contentWindow.postMessage(payload, "*");
         }
     });
 })(jQuery);

Now we enqueue and localize the script with the access_token. Note that we used the JS_APP_URL global here. Head to the main WP configuration file, wp-config.php and add this line (you can do it right under WP_SITEURL:

// wp-config.php

// ...

/** This is new, WP SiteURL and Home globals override */
define('WP_SITEURL', 'http://sitepoint-wp-rest-api.test/');
define('WP_HOME', WP_SITEURL);
define('JS_APP_URL', 'http://app.sitepoint-wp-rest-api.test');

// ...
// wp-content/themes/twentysixteen-child/functions.php
// ...

function ab_localise_scripts() {
    $user_id = get_current_user_id();
    $auth_token = get_user_meta( $user_id, '_access_token', true);
    $data = array(
        'accessToken' => $auth_token,
        'src' => JS_APP_URL . '/tunnel.html'
    );
    wp_localize_script( 'customscripts', 'abData', $data );
}
wp_enqueue_script( 'customscripts', get_stylesheet_directory_uri() . '/assets/js/main.js', array('jquery');
ab_localise_scripts();

Conclusion

And that is how quickly you can have a working API for your SPA! Although we used AngularJS in this tutorial, keep in mind that this setup can be used with any other JS framework as well.

Before using this in production, it’s very important to consider the beta status of the API plugin and also that SSL is a must, otherwise we could expose the access_token to the public, which is basically almost as bad as sending out the username and password.

What do you think? Will you create a custom JS front-end for your next WordPress app? Maybe you’re considering a WP-based API to serve content for an SPA? Let me know in the comments!

Sponsors