CodeBrief

Anatomy of a Complex Ember.js App Part I: States and Routes

UPDATE: This is no longer the preferred way of handling routing and layout in Ember. Please read this updated post.

I am long on Ember.js. I previously wrote a brief comparison of some of the more popular javascript frameworks, and Ember came out on top. I am now in the process of incorporating it into GroupTalent in a big way.

Due to the recent origins of Ember, one of its biggest shortcomings is a serious lack of documentation. Its website and some related blog posts focus only on the basics and hello world type examples (read todo in the js mvc world). A major reason for this is that the patterns required for a complex application have not yet been added to the core Ember.js codebase. That is not to say, however, that complex Ember.js applications are not already being built.

With that in mind, I have decided to write a series of posts detailing some of the libraries and patterns I have been using for Ember development. In this post, I cover creating a multi-page application: all that is required to swap out different pages and sub-pages as well as tieing everything in to the browser location and the HTML5 history API. Specifically I discuss two libraries that I have written, Ember Layout and Ember RouteManager. You can also skip ahead and see an example application in action.

Meet The State Manager

A core construct inside of Ember is the notion of states managed by a state manager. This is used in many places inside the Ember.js codebase, including managing view render states. Thus, it is only natural for this construct to be used to manage the composition of views as well.

In fact, Ember already has primitive support for managing views inside it's core. You can do the following:

App.stateManager = Ember.StateManager.create({

  rootElement: '#content',

  section1: Ember.ViewState.create({
    view: App.section1
  }),

  section2: Ember.ViewState.create({
    view: App.section2
  })

});

Setting the stateManager's state to section1 will automatically append the App.section1 view to the element selected by #content. If the state is then set to section2 the old view will be removed and replaced by App.section2.

This is sufficient for extremely basic applications, but breaks down when the demands are more complex. This method is restricted to view hierarchies that are only a single level deep - most often not the case. The canonical example of this is a page with a primary navigation and a sub-page that has its own tabbing system. What is really needed here is a way to dynamically nest views arbitrarily deep.

Layouts

Ember Layout adds an intuitive layout mechanism on top of Ember.js. You can see it in action here. It adds the concept of a handlebars {{yield}} helper which can be used to indicate insertion points of dynamically replaceable content. This should be extremely familiar to anyone with a rails background, but with the added notion of being dynamically replaceable.

Here is a code snippet extracted from the example:

App.main = Em.LayoutView.create({
  templateName: 'main'
});

App.routeManager = Em.RouteManager.create({
  rootLayout: App.main,
  ...
  layoutNesting: App.NavState.create({
    ...
    viewClass: Em.LayoutView.extend({
      templateName: 'layout-nesting',
    }),
    section1: App.SubNavState.create({
      ...
      viewClass: Em.View.extend({
        title: 'Section 1',
        templateName: 'section'
      })
    }),
    ...
  })
});

And the corresponding template code:

<script type="text/x-handlebars" data-template-name="main">
  ...
  <div class="container">
    {{yield}}
  </div>
</script>
<script type="text/x-handlebars" data-template-name="layout-nesting">
  ...
  <div class="sub-container">
    {{yield}}
  </div>
</script>

When the routeManager (i.e. StateManager) goes to the layoutNesting.section1 state, a view instance specified by the viewClass property of the layoutNesting state is inserted into the {{yield}} location in the main template. This newly inserted view also has its own {{yield}} location, which is replaced by the view from the section1 state. Switching to a different state will then automatically reconstruct this view hierarchy.

Routing

At the crux of any single-page web application is a routing system. Although Ember doesn't natively include/endorse any particular routing system, it plays well with almost anything out there. Options include SprouteCore Routing and Sammy.js.

Ideally, however, due to Ember's proclivity towards states, a state-based routing solution seems like the most natural fit. This makes even more sense when taken in the context of the view composition system already being state-based.

Ember RouteManager is exactly that, a state-based routing solution. It introduces an extension of StateManager called RouteManager. Consider the following code (irrelevant parts snipped) from the example:

App.routeManager = Em.RouteManager.create({
  rootLayout: App.main,
  home: App.NavState.create({
    ...
  }),
  layoutNesting: App.NavState.create({
    path: 'layout-nesting',
    ...
    section1: App.SubNavState.create({
      path: 'section1',
      ...
    }),
    section2: App.SubNavState.create({
      path: 'section2',
      ...
    }),
    ...
  }),
  routeParameters: App.NavState.create({
    path: 'route-parameters',
    ...
    items: Em.LayoutState.create({
      ...
    }),
    item: Em.LayoutState.create({
      path: ':itemId', // specify the path to take a parameter
      ...
    })
  })
});

I'm not going to go into detail here, but suffice it to say that each possible route is captured as a leaf node in the state tree. State's can have an optional path property which dictates how the routes map to states in the routeManager. Paths can consist of dynamic parameters (:parameterName), wildcards (*), regular expressions, and static paths. For example, from the above code: setting the browser location to #/routeParameters/5 would map to the routeParameters.item state with the :itemId parameter being populated by 5. Since these states are also layout states, this will also automatically update the view hierarchy as well.

The beauty of this approach is the de-coupling of application state from absolute routes. For some reason, many frameworks enforce a holistic view of the routing system, treating it as a list of mappings from routes to actions. With RouteManager's approach, state definitions are agnostic to the parts of the route corresponding to their parent, and instead only focus on their local part.

Much of the code inside RouteManager is originally based on SproutCore routing. Thus, it can also use the HTML 5 history api by setting the wantsHistory property.

Caveat

The caveat here is that what I am describing is likely to change as Ember.js evolves. The libraries I have presented are still immature and more conceptual than fully formed. Ultimately Ember's core will have a solution for this and I will update this post when that is the case.

Stay tuned for the next post in this series where I cover data persistence.

comments powered by Disqus