URL routing with Marionette

In my last post I compared some basic ways in which Marionette and React make it possible to develop single-page applications (SPAs). In my next posts I’ll compare the ways in which they facilitate URL routing within SPAs. To demonstrate the routing capabilities of the libraries I’ll develop a simple app in each. The app will take the form of a tabbed UI in which each tab can be loaded via the URL. My next post will concentrate on React; this post will concentrate on Marionette.

URL routing

What exactly is URL routing? A Simple Introduction to URL Routing provides the following definition:

URL routing means that you when click on a link, instead of being routed to another page, you stay on the same page and the content changes. When this happens, usually a “hash” will be appended to your current URL so that the user can go directly to the content they need as well as using back and forward buttons in the browser.

As this definition suggests URL routing is important because, at the risk of stating the obvious, content within an SPA is typically loaded on a single page. (This contrasts with traditional server-side applications in which any given user interaction, e.g., a form submission, typically loads a new page from the server.) Without URL routing SPAs break the back and forward buttons in the browser, making apps harder to navigate. In order to address such problems many contemporary client-side libraries and frameworks provide URL routing implementations.

Marionette.AppRouter

Marionette’s URL routing implementation comes in the form of the AppRouter class. Using this class it’s possible to define a pattern (route) that matches a URL and a callback function (route handler) that is invoked whenever a route is matched. AppRouter offers two approaches for defining routes and route handlers:

  1. appRoutes: When using this approach the route handler must be present on a “controller” object provided to the router.
  2. routes: When using this approach the route handler must be present on the router itself.

Demo app with Marionette

The demo app will use the routes approach, in which the route handler must be present on the router. The router will define routes and route handlers such that when a route is matched the the appropriate tab will be loaded into the UI. The code for the app is available on CodePen. Before diving into the implementation details I’ll first provide a brief overview of the requirements, data model and code design.

Requirements

First let’s define some basic requirements for the app:

  1. Visiting the page for the first time should load the first tab.
  2. It should be possible to load tabs by clicking on links.
  3. It should be possible to load tabs by changing the URL hash to match a route. For example a route of “tabs/2” should load the second tab.
  4. It should be possible to load tabs using the browser’s back and forward buttons.
  5. Refreshing the page should preserve the URL and load the corresponding tab. For example if the second tab was loaded prior to refreshing the page, the second tab should still be loaded after refreshing the page.

Data model

The app employs a simple data model to represent the idea of a tab.

id: Number
title: String
description: String
active: Boolean

The model’s id attribute uniquely identifies the tab. The active attribute indicates whether a tab is in an active or inactive state. The title and description attributes are just for display purposes.

Code design

The ticking clock app I developed for my last post demonstrated how a Marionette app can be composed by piecing together a number of the library’s classes. For example it used the LayoutView class to contain nested views, the Region class to contain the layout and the Application class to contain the rest of the code. The app I’ve developed for this post reuses some of these classes and introduces some new ones. The basic building blocks of the app are as follows:

  • loadInitialData
    Function
    Loads the data that provides the content for the UI.
  • Tabs
    Extends Marionette.CollectionView
    Represents a collection of tabs.
  • Tab
    Extends Marionette.ItemView
    Represents an individual tab. Clicking a tab loads the corresponding tab panel and changes the URL, engaging the router.
  • TabContent
    Extends Marionette.CollectionView
    Represents a collection of tab panels.
  • TabPanel
    Extends Marionette.ItemView
    Represents an individual tab panel.
  • Layout
    Extends Marionette.LayoutView
    Serves as a container for the tabs and tab content views.
  • Router
    Extends Marionette.AppRouter
    Defines the route and route handler for the application. A matching route invokes the route handler; the route handler loads the correct tab into the page.
  • App
    Extends Marionette.Application
    Serves as a container for the rest of the application code.

The remainder of this post discusses how these building blocks fit together.

Loading the data

The initial set of data for the app is loaded via the aptly named function loadInitialData.

const loadInitialData = () => {
  const dfd = $.Deferred();
  dfd.resolve(
    new Backbone.Collection(
      [
        {id: 1, title: 'Tab one', description: 'This is tab one.'},
        {id: 2, title: 'Tab two', description: 'This is tab two.'},
        {id: 3, title: 'Tab three', description: 'This is tab three.'}
      ]
    )
  );
  return dfd.promise();
};

The function creates a Backbone collection based on the data model. To mimic asynchronous loading the function uses a jQuery Deferred Object to expose the data. Invoking the deferred’s promise method exposes another deferred method, then, which allows an additional handler to be attached. The handler receives the loaded data as input. The then method itself can be chained to the invocation of loadInitialData.

loadInitialData().then((initialData) => {
  // Use initialData here
});

Creating the tabs

Since the data is exposed as a Backbone collection the app can use a Marionette CollectionView for rendering the models. From the Marionette docs:

The CollectionView will loop through all of the models in the specified collection, render each of them using a specified childView, then append the results of the child view’s el to the collection view’s el.

As this definition implies, a CollectionView actually consists of two separate views: a parent view for rendering the collection and a child view for rendering each of the individual models. Within the app these views are represented by Tabs and Tab respectively.

const Tab = Marionette.ItemView.extend({
  tagName: 'li',
  getTemplate() {
    return _.template((!this.model.get('active')) ? '<a href="#tabs/<%= id %>"><%= title %></a>' : '<%= title %>');
  }
});

const Tabs = Marionette.CollectionView.extend({
  childView: Tab,
  tagName: 'ul',
  collectionEvents: {
    'change': 'render'
  }
});

The Tabs class identifies the Tab class as its child view using the aptly named childView property. It also declares its root HTML element (UL) with tagName. Finally it uses the collectionEvents property to instruct Marionette to re-render the collection whenever the latter’s change event fires.

Meanwhile the Tab class declares its own root HTML element (LI) with tagName. It also leverages the getTemplate method to decide upon a template to use depending on the state of the model. An individual tab exists in one of two possible states: active or inactive. When the tab is active, the model’s title should be rendered without a hyperlink so that it can’t be clicked; when the tab is inactive, the model’s title should be rendered with a hyperlink so that it can be clicked.

Creating the tab content

The process for creating the tab content resembles the process for creating the tabs: A parent view renders the collection and an associated child view renders each of the individual models. These views are represented in the app by TabContent and TabPanel respectively.

const TabPanel = Marionette.ItemView.extend({
  template: _.template('<div><h2><%= title %></h2><p><%= description %></p></div>'),
  onBeforeAttach() {
    return (!this.model.get('active')) ? this.$el.hide() : null;
  }
});

const TabContent = Marionette.CollectionView.extend({
  childView: TabPanel,
  collectionEvents: {
    'change': 'render'
  }
});

The TabContent class identifies the TabPanel class as its child view and, just like the Tabs class, instructs Marionette to re-render the collection whenever the collection’s change event fires. Unlike the Tabs class, TabContent doesn’t explicitly declare its root HTML element–in this case Marionette uses a default (DIV).

Meanwhile the TabPanel class defines its template and, using the onBeforeAttach lifecycle method, decides whether a given panel should be shown or hidden in the DOM. Just as a tab exists in an active or inactive state, so too do tab panels. When a panel is active it should be shown; when inactive, hidden.

Creating the layout

Since the UI has two main views–represented by Tabs and TabContent–a Marionette LayoutView can be used to contain them. The Marionette docs again:

A LayoutView is a hybrid of an ItemView and a collection of Region objects. They are ideal for rendering application layouts with multiple sub-regions managed by specified region managers.

Sounds ideal! All that remains is to define the class.

const Layout = Marionette.LayoutView.extend({
  template: _.template('<div><h1>URL routing with Marionette</h1></div><div id="tabs-region"></div><div id="tab-content-region"></div>'),
  regions: {
    tabsRegion: '#tabs-region', 
    tabContentRegion: '#tab-content-region'
  },
  initialize(options) { 
    this.tabs = options.tabs; 
    this.tabContent = options.tabContent; 
  }, 
  onShow() { 
    this.tabsRegion.show(this.tabs);
    this.tabContentRegion.show(this.tabContent);
  }
});

The Layout class defines a template with elements corresponding to the regions defined in the regions hash. Then, after the template has been added to the DOM during the onShow lifecycle event, it adds the views to the DOM with region.show(view).

Creating the router

The next step is to define the Router class that will be responsible for loading the correct tab into the UI.

const Router = Marionette.AppRouter.extend({
  routes: {
    "tabs/:tab": "tab",
    "*path": "default"
  },
  initialize(options) {
    this.collection = options.collection;
    this.defaultTab = options.defaultTab;
  },
  tab(tab) {
    const tab_ = --tab;
    this.collection.set(this.collection.map((model, index) => {
      model.set('active', tab_ === index);
      return model;
    }));
  },
  default() {
    this.tab(this.defaultTab);
  }
});

Router’s routes property maps a route (tabs/:tab) to a route handler (changeTab). The route handler works by updating the Backbone collection that provides the data to the UI. Specifically it sets the active attribute to true on the model whose id matches the parameter contained in the route (:tab). Updating the collection in this way causes the collection’s change event to fire, which in turns causes the Tabs and TabContent views to re-render (see Creating the tabs and Creating the tab content).

Creating the application

With both the router and the views defined all that remains is to create the top-level application for containing the rest of the code.

const App = Marionette.Application.extend({
  initialize(options) {
    this.rootElement = options.rootElement;
    this.defaultTab = options.defaultTab;
    this.collection = options.collection;
  },
  onBeforeStart() {
    this.router = new Router({
      collection: this.collection,
      defaultTab: this.defaultTab
    });
    this.layout = new Layout({
      tabs: new Tabs({
        collection: this.collection
      }),
      tabContent: new TabContent({
        collection: this.collection
      })
    });
    this.region = new Marionette.Region({
      el: this.rootElement
    });
  },
  onStart() {
    Backbone.history.start();
    this.region.show(this.layout);
  }
});

The App class employs three methods, all native to Marionette Application:

  1. initialize: This method simply saves references to any parameters passed into it. The rootElement parameter corresponds to the id attribute of the HTML element in which the layout view is embedded: <div id="app"></div>.
  2. onBeforeStart: This method instantiates several of the classes required by the application, including Router, Layout, Tabs and TabContent. Notice how the collection loaded by loadInitialData is passed into Router, Tabs and TabContent. Since all three classes reference the same collection, all three classes receive Router’s updates to the collection, the updates being necessary for re-rendering.
  3. onStart: This method sets the initial state of the app. Specifically it monitors for “hashchange” events via Backbone.history.start() and adds the layout view to the DOM.

The App class is instantiated after the deferred object created by loadInitialData has been resolved:

loadInitialData().then((initialData) => {
  const app = new App({
    rootElement: '#app',
    defaultTab: '1',
    collection: initialData
  });
  app.start();
});

Acceptance testing

To test that the app meets the requirements stated above, export the CodePen to a ZIP, unzip the archive and open index.html in a browser.

Conclusion

Such requirements would not be as easy to implement without AppRouter, Marionette’s implementation of URL routing. In my next post I’ll attempt to recreate the exact same app with the exact same behaviors using React. Not that AppRouter made it too hard but let’s see if React makes it any easier!

Comparing Marionette and React

The JavaScript landscape is changing rapidly. If you’re anything like me you’ll know that trying to keep current amidst the churn can be frustrating. We can but try, however. To this end I’ve been learning about React and thinking about how it compares to another UI library I’ve used in the past: the Backbone-based library, Marionette.js.

Why React?

  1. React is relatively well-liked. In the 2016 State of JavaScript “Frameworks” survey 3044 respondents claimed to have used React before, 92% of whom claimed they would use it again. React achieved the highest satisfaction rating, Vue finishing a close second with 89%. Vue had only a fraction of React’s users, however: Only 577 respondents claimed to have used it before. Angular 2 (1092/65%), Ember (850/48%) and Angular (3391/47%) lagged significantly behind React and Vue in one or both regards.
  2. Marionette is relatively obscure. In the same survey Marionette was mentioned by just 41 respondents. No data was given for whether these respondents had used it before or would use it again. Meanwhile Backbone, upon which Marionette is based, acquired a relatively low satisfaction rating: Although a relatively high number of users (2077) claimed to have used it before, only about one-third (32%) claimed they would use it again.
  3. React is more library than framework. Unlike full-blown JavaScript frameworks such as Angular, React is mainly a view library. In this sense it it tempting to see React as a more direct replacement for Marionette, much of whose value in my opinion comes from the views it provides. In principle I tend to prefer libraries over frameworks, chiefly because the latter tend to be more prescriptive in terms of how apps should be constructed.

In summary React appears to be a pretty safe choice that should provide a natural progression from Marionette. Let’s put that to the test by creating an app. We’ll implement a simple ticking clock (featured in the React docs) doing it in both Marionette and React so we can compare.

Creating an application

When creating a JS app it’s nice to have a single object that serves as an entry point to the rest of the UI. Both Marionette and React embrace this concept: Marionette via a dedicated Application class; React via a top-level Component that serves as a parent to other, “child” components. So let’s begin implementing our ticking clock by creating a simple application.

Requirements

  • Output “The time according to {library} is:” to the page

Code

Marionette implementation

To implement the requirement in Marionette:

  1. We created a callback function (onStart) for adding a view containing the output to the DOM. This function runs when the application starts. It first creates a region for showing the view, then creates the view, then shows the view in the region.
  2. We created an instance of the Application class.
  3. We attached an event listener (“start”) to the application instance, passing it the callback function defined in #1.
  4. We started the application via the “start” method on the application instance.

React implementation

To implement the requirement in React:

  1. We created a component for adding the output to the DOM. In this case the component is a functional component but as we’ll see later it could also have been a class component.
  2. We added the output to the DOM via ReactDOM.render. This method basically just merges our component with the DOM, similar to Marionette’s region.show method in #1 above.

Analysis

We can see that React makes life a little easier for getting an app off the ground. While Marionette introduces several different concepts right off the bat (Applications, Regions and Views), React introduces just one (Component). React also appears to do things a little more automatically, e.g., no need to officially start the application or create a listener for the start event. React’s syntax also ends up being cleaner and more terse.

Character count

  • Marionette: 296
  • React: 121

Nesting views

Another essential feature of a JS view library is the ability to nest one view within another view. Nested views reflect the treelike structure of the DOM tree and help to encapsulate functionality within clearly defined modules. Both Marionette and React embrace the concept of nested views: Marionette via its dedicated LayoutView class; React via components. So let’s enhance our fledgling app to illustrate how nested views work in each case.

Requirements

  • Add a view for displaying the title
  • Add a view for displaying the clock

Code

Marionette implementation

To implement the requirements in Marionette:

  1. We added two regions to our LayoutView: one for the title, one for the clock. The LayoutView is a special type of view provided by Marionette distinctly for layout purposes.
  2. We created a callback function (onShowLayoutView) for adding views to the regions defined in the layout view. This function runs after the layout view has been added to the DOM.
  3. We attached an event listener (“show”) to the layout view, passing it the callback function defined in #2. Waiting for the parent view to be added to the DOM before attempting to add the child views ensures Marionette will not try to add the latter before the former is ready.

React implementation

To implement the requirements in React:

  1. We created two user-defined components (AppHeader and AppBody) to represent the nested views.
  2. We rendered the nested components in the parent component using the latter’s “render” method.

Analysis

React again seems to make things easier for us. Marionette introduces yet another concept (ItemView); React allows us to reuse an existing one (Component). Marionette makes us do more work (e.g., we have to manually instantiate all new objects); React does this work for us (we can simply define a component declaratively and React will create it for us). React’s syntax again ends up being cleaner and more terse.

CHARACTER COUNT

  • Marionette: 639
  • React: 252

Managing state

The concept of state as it applies to JS apps is admittedly rather new to me: With hindsight I suppose it’s just one of those things I’ve been doing with JS/jQuery/Backbone/Marionette without really thinking too much about how I’m doing it.

To be going on with let’s just define managing state as managing change. In the context of an app the source of change could be manual or automatic. For example:

  • A user enters a value in a text box (manual)
  • A timer updates a value every so often (automatic)

What these cases have in common is that something happens in the app and the app has to respond somehow. In other words, the app must undergo a change in state.

Once again let’s illustrate this by way of our app, this time by implementing the ticking behavior of the clock.

Requirements

  • Store the date so that it can be updated
  • Set a timer to update the date every second
  • Display the date (and redisplay it whenever it’s updated)
  • Clear the timer if the component is ever removed from the page

Code

Marionette implementation

To implement the requirements in Marionette:

  1. We created an ItemView to represent the clock.
  2. We created a Backbone model to store the date, passing the model into the view.
  3. We created a timer (setInterval) for updating the date and set it to update the model’s date attribute every second. We did this within view’s constructor function (initialize).
  4. We created an Underscore template for displaying the date, saving it to the view’s template property.
  5. We instructed Marionette to re-display the view whenever the model’s date attribute changes. For this we used the view’s modelEvents property.
  6. We instructed Marionette to clear the timer if the view is ever removed from the DOM.

React implementation

To implement the requirements in React:

  1. We created a component to represent the clock.
  2. We stored the date in the component’s local state.
  3. We created the timer for updating the date and set it to update the component’s local state every second. We did this in the component’s componentDidMount lifecycle method. This method is called after the component has been rendered to the DOM.
  4. We created a JSX expression for displaying the date, outputting it right there in the component’s render method.
  5. We instructed React to clear the timer if the component is ever removed from the DOM. We did this in the component’s componentWillUnmount lifecycle method.

Analysis

Marionette and React are a little more in line with each other here. There are a similar number of steps involved in implementing this functionality in either framework. The only major difference appears to be related to re-displaying the date. We have to instruct Marionette explicitly to listen for changes to the model so that it knows when to re-display the view. React, on the other hand, propagates changes in state to the component via props.

CHARACTER COUNT

  • Marionette: 976
  • React: 552

Conclusion

Clearly this comparison only touches on the basics of Marionette and React. Equally clearly our ticking clock app is simplistic to say the least. Nonetheless I think the exercise serves to illustrate some interesting differences between the two libraries, differences that for the most part come down in favor of React.

  • I like React’s declarative nature. React seems to do a bit more of the grunt work for us than Marionette. React allows us to declare components; Marionette makes us create objects.
  • I like React’s functional components. Functional code, characterized by pure functions and immutable data, in theory produces more reliable results than object-oriented code. React leans toward the former; Marionette, the latter.
  • I like React’s state management. With React, changes in state are automatically propagated to components via props. With Marionette, again there may be some manual work involved depending on your use case.