Some background. We have a client that needs to use a Wordpress backend with a single-page-app frontend. For the frontend, we are using Ember.js. For the backend, Wordpress is serving more as a CMS than a blog. This means the ember app needs more than just a stream of posts, but also a dynamic menu that it pulls from Wordpress and then nests correctly for display. The Wordpress setup involves three plugins:

  • JSON REST API
  • JSON REST API Menu routes
  • ACF to WP API (this is not used yet. stay tuned.)

Finally, for Wordpress you might need to set up Cross-Origin Resource Sharing. This should get the json served up in a reliable way. It is not, however, being formatted in the way that Emberjs would like. Ember expects JSON APIs to be built using the format found here.

Ember would like one object returned from http://somesite/api/menus/2 to look like this:

{
  "menus": {
  "id": "2",
  "title": "First Menu Item",
  ...
  }
}

An array of objects returned from http://somesite/api/menus might look like this:

{
  "menus": [{
  "id": "16",
  "title": "First Menu Item",
  ...
  }, {
  "id": "2"
  "title": "Second Menu Item",
  ...
  }]
}

Wordpress' JSON REST API presents some challenges to our expectations here.

First, JSON REST API with the Menu plugin provides JSON that looks like (wordpress-site.com/wp-json/menus):

[
  {
    "term_id": "2",
    "name": "Primary Navigation",
    "slug": "primary-navigation",
    "term_group": "0",
    "term_taxonomy_id": "2",
    "taxonomy": "nav_menu",
    "description": "",
    "parent": "0",
    "count": "10",
    "ID": "2",
    "meta": {
      "links": {
        "collection": "http://wordpress-site.com/wp-json/menus/",
        "self": "http://wordpress-site.com/wp-json/menus/2"
      }
    }
  }
]

This supports multiple menus. To get the menu we want, we have to reference the particular menu we would like to get which, in our case is the menu with an ID of 2. The menu we need lives here: (wordpress-site.com/wp-json/menus/2). That response looks like this:

{
  "ID": 2,
  "name": "Primary Navigation",
  "slug": "primary-navigation",
  "description": "",
  "count": 10,
  "items": [
    {
      "ID": 37,
      "order": 1,
      "parent": 0,
      ...
    },
    {
      "ID": 52,
      "order": 2,
      "parent": 37,
      ...
    }
  ],
}

There are a couple of challenges here. First, there is no root object in the JSON. Second, the menu items are nested in the menu response. Finally, the relationship between parent/child menu items isn't structural - the children have a "parent" attribute that references the ID of their parent item. If the parent attribute is 0 there is no parent, otherwise the parent can be nested at any level. Item1 might have a parent: 0, item2 has parent: 1, item3 has parent: 2, and on and on. For the sake of an easy example, say we want our markup structured like the Bourbon/Refils menu found here.

%ul#navigation-menu
  %li.nav-link.more
    %a{:href => "#"} Parent Item (Item1)
    %ul.submenu
      %li
        %a{:href => "#"} Child Item (Item2)
      %li.more
        %a{:href => "#"} Child Item (Item3)
        %ul.submenu
          %li
            %a{:href => "#"} Grandchild Item (Item4 with parent:3)
          %li
            %a{:href => "#"} Grandchild Item (Item5 with parent:3)

The first thing to do with an ember app is set up the models and routes. First the models.

/app/models/menu.js

import DS from 'ember-data';

export default DS.Model.extend({
  name: DS.attr('string'),
  slug: DS.attr('string'),
  description: DS.attr('string'),
  count: DS.attr('number'),
  items: DS.hasMany('item')
});

/app/models/item.js

import DS from 'ember-data';

export default DS.Model.extend({
  order: DS.attr('number'),
  parent: DS.attr('number'),
  title: DS.attr('string'),
  url: DS.attr('string'),
  attr: DS.attr('string'),
  target: DS.attr('string'),
  classes: DS.attr('string'),
  xfn: DS.attr('string'),
  description: DS.attr('string'),
  object_id: DS.attr('number'),
  object: DS.attr('string'),
  type: DS.attr('string'),
  type_label: DS.attr('string'),
  hasParent: function(){
    if(this.get('parent') === 0){
      return false;
    } else {
      return true;
    }
  }.property('parent')
});

We added an attribute that determines if the menu has a parent item. I'm not certain that this is the best place for this, but it keeps things simple for now and I take the "get it working then refactor" approach.

Now that we have the basic structure of our models setup we can move on to hooking our models to the proper routes. I am not certain how the final product will be structured, but for the sake of producing a functioning app, I have attached the second menu object to the pages route which is accessed at the root path.

/app/router.js

import Ember from 'ember';
var Router = Ember.Router.extend({
  location: AppNameWhatever.locationType
});
Router.map(function() {
  this.resource('pages', { path: '/'}, function(){
    // children routes go here
  });
});
export default Router;

/app/routes/pages.js

export default Ember.Route.extend({
  model: function() {
    // remember our primary menu is the menu object with ID: 2
    return this.store.find('menu', 2);
  }
});

Next, we can implement our templates and views and give this stuff a place live for everyone to see. First the pages template:

/app/templates/pages.hbs

{{view 'menu'}}
{{outlet}}

Now, the primary-navigation template that we will attach to the {{view 'menu'}} which we just called.

/app/templates/primary-navigation.hbs

<ul id="navigation-menu">
  {{#each item in items}}
    {{#unless item.hasParent}}
      <li class='nav-link' {{bind-attr data-id=item.id}}>
        <a href='#'>
          {{item.title}}
        </a>
      </li>
    {{/unless}}
  {{/each}}
  {{#each item in items}}
    {{#if item.hasParent}}
      <li class='children child-items' {{bind-attr data-id=item.id data-parent=item.parent}}>
        <a href='#'>
          {{item.title}}
        </a>
      </li>
    {{/if}}
  {{/each}}
</ul>

I am not really convinced that this approach is the best way to accomplish the task of creating a nested menu from flat JSON. However, it works; and again, I want to get it working then refactor. I am creating <li>s with all the Items that don't have parents - this serves as the first level of the navigation. Then, I do the same thing with all Items that have parents.

In the View, I will use jquery to arrange the items properly. This is not the only possible approach to this, but having considered a few alternatives, I feel like this is the quickest, most straightforward approach. I am giving each parent LI a data-id attribute and setting it equal to the item.id. The children get the same data-id attribute as well as a data-parent attribute that is set to the value of item.parent (this corresponds to the item.id of the parent object).

Now the view. This is the view that we are referencing in our pages template.

/app/views/menu.js

import Ember from 'ember';
export default Ember.View.extend({

This tells the view what template to use. In our case, we're using the primary-navigation template that we just created.

templateName: 'primary-navigation',

Now we want to run some jquery to organize the menu. For this we'll use the didInsertElement event which is called when the element of the view is inserted into the DOM.

didInsertElement : function(){
    this._super();
    Ember.run.scheduleOnce('afterRender', this, function(){
      // this sorts all the children and places them in their proper place with respect to their parents
      $('li.child-items').each(function(){
        var thisParentID = $(this).data('parent');
        var thisContent = $(this).removeClass('child-items');
        var parentLi = $(this).closest('#navigation-menu').find("[data-id='" + thisParentID + "']");
        if(parentLi.hasClass('more')){
          console.log('its already got it');
        } else {
          parentLi.addClass('more');
        }
        console.log(parentLi); 
        if(!(parentLi.has("ul").length)){
          parentLi.append("<ul class='submenu'></ul>");
        }
        thisContent.detach().appendTo(parentLi.find('ul').first());
      });

      // this is for the navigation styles. This is the nav script from refils.bourbon.io
      var menu = $('#navigation-menu');
      var menuToggle = $('#js-mobile-menu');
      var signUp = $('.sign-up');

      $(menuToggle).on('click', function(e) {
        e.preventDefault();
        menu.slideToggle(function(){
          if(menu.is(':hidden')) {
            menu.removeAttr('style');
          }
        });
      });

      // underline under the active nav item
      $(".nav .nav-link").click(function() {
        $(".nav .nav-link").each(function() {
          $(this).removeClass("active-nav-item");
        });
        $(this).addClass("active-nav-item");
        $(".nav .more").removeClass("active-nav-item");
      });
    });
  }

});

That is it for the structure. Now that we have all of that in place, we could throw together some fixtures and see this work. But, let's go ahead and hook up to Wordpress's JSON API.

First, we need to tell the adapter where to look.

/app/adapters/application.js

import DS from "ember-data";
export default DS.RESTAdapter.extend({
  host: 'http://wordpress-site.com',
  namespace: 'wp-json'
});

Now, we can set up the serializer where we will deal with the funky JSON.

/app/serializer/application.js

import DS from 'ember-data';

export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {

Ember expects 'id' rather than 'ID'. So I am going to explicitly set the primary key to 'ID'.

primaryKey: 'ID',

Then, we can deal with our missing root element when extracting arrays from the JSON.

  extractArray: function(store, type, payload) {
    var payloadTemp = {};
    payloadTemp[type.typeKey] = payload;
    return this._super(store, type, payloadTemp);
  },

Here we are dealing with JSON without the root element when extracting objects.

  extractSingle: function(store, type, payload, id) {
    var payloadTemp = {};
    payloadTemp[type.typeKey] = [payload];
    return this._super(store, type, payloadTemp, id);
  },

Finally, we want to tell ember to look for the Items embedded in the menu object.

  attrs: {
    items: { embedded: 'always' }
  }
});

That's it. Your menu should be working. And this should lay the groundwork for getting everything else hooked up. Of course, I welcome corrections of style or substance.

Brandon is a software developer. He was one of Isotope 11's first employees - now he's back with us. Brandon loves working with Ruby and Javascript.