Dynamic Navigation With AngularJS and WordPress

Table of Contents:

  1. Introduction
  2. Explained At a High Level
  3. The Architecture
  4. The Factory
  5. Sitemap on Run
  6. Navigation Directive
  7. Directive Scope Variable
  8. Bind Sitemap Data to Navigation
  9. Summary
  10. Other Posts in this Series

Introduction

In part 1 of this AngularJS and WordPress series we wired up a Yeoman AnguarJS app to a WordPress back-end with WP REST API. In part 2 we set up one route, one controller and one view that could dynamically handle any route and fetch pages from our WordPress install.

What good is a web application that can dynamically fetch any page content from our WordPress API if you need to type the URL into the address bar? If we are going to fetch pages dynamically, we need a navigation system that knows what pages to fetch.

In part 3 of this series I am going to show you how I tackled the dynamic navigation problem. I’m not going to lie, this part of the solution isn’t ideal but short of extending the WP REST API Plugin to return navigation objects (which is something I will look into when I have time), this solution is the best I could come up with.

Explained At a High Level

Out of the box, WP REST API doesn’t return WordPress menus. I am certain it can be extended to do so through their extensible plugin architecture. There is a way to modify a theme to allow for child object of a parent page to be listed, but the number of XHR’s I’d have to make to determine what a full navigation would look like would border on ridiculous.

In lieu of this absence of functionality I decide the easiest thing to do would be to store the desired navigation information in a WordPress page, as CSV or JSON. Then I could fetch that page via the API and parse its contents as the appropriate data type. This approach does pose a few challenges and concerns but in the end it does work just fine.

Yes it is a little more manual, less dynamic, but it puts the power to edit this information architecture in the hands of the WordPress maintainers and not on the developers of the code base. It will save you from having to deploy changes every time your boss decides to change the site navigation.

The Architecture

Ultimately I chose a JSON format over CSV as it would allow for finer grain information storage and be more scalable in the future should my data structure change. Also, to kill two birds with one stone, I’m structuring this information as my sitemap so it can serve to fill your navigational and sitemap needs.

  1. We are going to write JSON in a WordPress page. For this to work, you need to install a WordPress plugin called Raw HTML which, when toggled in your post, disables the auto formatting (<p> and <br> tags, and smart quotes) that WordPress inserts into every post and page.
  2. In your WordPress pages section, create an empty page called config.
  3. Add another page called sitemap-json and select config as its parent page. The naming convention of adding -json to the title just made sense to me. It indicates at a glance that I can expect to find JSON data there.
  4. Add the following content to your sitemap-json
    [
    {"name":"Home","path":"/","navigation":false},
    {"name":"Test Page 1","path":"/test-page-1","navigation":true,"children":[
        {"name":"Sub Test 1","path":"/sub-test-1","navigation":true},
        {"name":"Sub Test 2","path":"/sub-test-2","navigation":true}
    ]},
    {"name":"Test Page 2","path":"/test-page-2","navigation":true}
    ]
    

    Fill in the information as it pertains to your site. This structure assumes that you have 5 pages;

    1. a home page,
    2. a Test Page 1,
    3. a Test Page 2,
    4. a Sub Test 1 and
    5. a Sub Test 2.

    While you could technically nest the two sub pages in WordPress, I recommend keeping them all top level. Sure enough someone is going to want to change the information architecture around and if they do — and you’ve relied on pages being nested — then your application will break.

    You’ll note the data is broken up into page objects with 4 main parts…

    • name: The page name as you want it to appear in the navigation.
    • path: The address in your AngularJS application where this particular data will be rendered.
    • navigation: Boolean; true if you want the page to appear in the navigation, false if not.
    • children: an array of additional page objects that you wish to appear nested as sub navigation
  5. In the sidebar, look for the Raw HTML panel and toggle all of the features for good measure.

  6. Publish this page and you’re done in WordPress for now.

The Factory

Chances are we could make use of this sitemap object in our application more than once. At the very least we’ll use it for our navigation system and sitemap page, but we could likely use it for our footer and maybe even our home page. We don’t want to fetch that data every time we need it though — we want to fetch it once and store that data. Sometimes developers do this with $rootScope, but I prefer not to muddy the global namespace.

Instead, we’ll make a sitemap factory with a getter and setter, like this:

  1. In terminal, make a new angular factory:
    $ yo angular:factory sitemapService
    

    This will create a sitemapservice.js in app/scripts/services/.

  2. Open app/scripts/services/sitemapservice.js and replace the contents with this:

    'use strict';
    
    /**
     * @ngdoc service
     * @name myappApp.sitemapService
     * @description
     * # sitemapService
     * Factory in the myappApp.
     */
    angular.module('myappApp')
    .factory('sitemapService', function ($http) {
        var service = {
            get: getSitemap,
            set: setSitemap
        },
        sitemap;
    
        return service;
    
        function getSitemap() {
            return sitemap;
        }
    
        function setSitemap() {
            return $http.get('//yoursite.url/wp-json/wp/v2/pages/?filter[name]=config&filter[name]=sitemap-json')
            .then(getAjaxSuccess)
            .catch(getAjaxError);
    
            function getAjaxSuccess(response) {
                sitemap = JSON.parse(response.data[0].content.rendered);
            }
    
            function getAjaxError(reason) {
                console.log('Error: ', reason);
            }
        }
    });
    

    Let’s have a look at what this factory does:

    1. We set our service dictionary to have a set and get method, so we can use sitemapService.set() and sitemapService.get().
    2. We also declare a sitemap variable.
    3. Those methods reference private functions called getSitemap and setSitemap.
    4. The getSitemap function returns the value of sitemap.
    5. The setSitemap function returns a promise from an $http request to our sitemap-json page in WordPress.
    6. In the resolution to that promise, we set the value of sitemap to the parsed value of response.data[0].content.rendered

Sitemap on run

Now that we have our sitemap factory, we want to run it as soon as possible to make that object available for rendering our navigation.

  1. Open app/scripts/app.js and add a run function, after the initial .config:
    .run(function(sitemapService, $rootScope){
        sitemapService.set()
        .then(function () {
            $rootScope.$broadcast('sitemap:updated', sitemapService.get());
        });
    })
    

    This will make the sitemap-json contents available to the rest of the application via sitemapService.get(), but additionally it will create an event to notify us if or when that value changes.

Navigation Directive

To apply our dynamically retrieved navigation information we should make a new Angular directive to encapsulate the navigation markup and functionality.

  1. In terminal:
    $ yo angular:directive navigation
    

    This will create navigation.js in app/scripts/directives/.

  2. While in terminal, create a new view to hold our navigation markup:

    yo angular:view navigation
    

    This will create navigation.html in app/views/.

  3. Open app/scripts/directives/navigation.js and replace the template property:

    template: '<div></div>',
    

    Replace the above with this:

    templateUrl: './views/navigation.html`
    
  4. Replace the contents of navigation.html by cutting and pasting the following from app/index.html into app/views/navigation.html:
    <div class="header">
        <div class="navbar navbar-default" role="navigation">
            <div class="container">
                <div class="navbar-header">
    
                    <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#js-navbar-collapse">
                        <span class="sr-only">Toggle navigation</span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                    </button>
    
                    <a class="navbar-brand" href="#/">myapp</a>
                </div>
    
                <div class="collapse navbar-collapse" id="js-navbar-collapse">
    
                    <ul class="nav navbar-nav">
                        <li class="active"><a href="#/">Home</a></li>
                        <li><a ng-href="#/about">About</a></li>
                        <li><a ng-href="#/">Contact</a></li>
                    </ul>
                </div>
            </div>
        </div>
    </div>
    
  5. In app/index.html, replace the markup you just cut out with the following:
    <navigation></navigation>
    

    Provided you’ve done everything correctly, you should now see this from your served application:

    yo-angular-navigation-directive-start

    Notice our navigation is now little more than a message that reads “this is the navigation directive”. That originates from app/scripts/directives/navigation.js at line 15. This is an example of DOM manipulation, jQuery style.

  6. Let’s remove line 15 in app/scripts/directives/navigation.js;

    // remove this line
    element.text('this is the navigation directive');
    

    Now your navigation should look right as rain again.

    yo-angular-navigation-directive-middle

Directive Scope Variable

Before we can propagate our navigation markup with real links to real pages we need some values from our sitemapService.

  1. In the arguments list of our callback function of app/scripts/directives/navigation.js, at the sitemapService dependency:
    .directive('navigation', function (sitemapService) {
    
  2. Now in the body of the postLink function, assign the value of sitemapService.get() to a scoped sitemap variable:
    link: function postLink(scope, element, attrs) {
        // NEW CODE
        scope.sitemap = sitemapService.get();
        scope.$on('sitemap:updated', function(event, data) {
            scope.sitemap = data;
        });
    }
    

    I find that I fight against a race condition with directives and the AgularJS digest cycle, where values don’t seem to be ready when I expect them. That’s why we initially set our sitemap data in app.run with a $broadcast event in the promise. Here we are listing to that event in case sitemapService.get() yields nothing at time of compiling.

    If you wanted to you could simply forgo the initial assignment and just wait for the sitemap:updated event to occur. It’s a personal preference for me to do it this way.

Bind Sitemap Data to Navigation

Our applications skeleton is built on Bootstrap and therefore already has pretty great navigation in place. We’ll try to embed our sitemap data directly into the existing markup.

  1. Open app/views/navigation.html and remove all but one of the list items in the unordered list:
    <ul class="nav navbar-nav">
        <li><a ng-href="#/">Contact</a></li>
    </ul>
    
  2. Next we’ll add a little ng-repeat, ng-if and some binding on the remaining list item:
    <ul class="nav navbar-nav">
        <li ng-repeat="nav in sitemap" ng-if="nav.navigation"><a ng-href="{{ nav.path }}">{{ nav.name }}</a></li>
    </ul>
    

    The code we just added does four things:

    1. it will create as many new list items as there objects in our sitemap array (ng-repeat="nav in sitemap"),
    2. if the navigation value in that object is true then will write that list item to the DOM (ng-if="nav.navigation"),
    3. we bind nav.path to ng-href (ng-href="{{ nav.path }}"),
    4. and finally, we bind nav.name to the link text ({{ nav.name }}).

    If you have a look at your app now, you should see something like this:

    yo-angular-navigation-start

    This is the start of our dynamic navigation. But all we have is the top level pages. We’re missing our sub pages, drop navigation, active state, collapse… But we’ll leave that for next time.

Summary

In this article I showed you how to use a WordPress page to serve up some navigation JSON. I showed you how to make a factory to consume that JSON and a navigation directive to put it all together. It’s still not complete but I’ve given you enough to absorb for now.

In future posts I’ll cover the finishing details on navigation, site configuration, page specific configuration, site deployment and more. Build, test, learn.

Other Posts in this Series

 

seydoggy

 

Leave a Reply