Robert Dickert

Personal blog

Set Up Navigation With Iron Router and Bootstrap

| Comments

This post is a continuation of Iron Router: First Steps. You can download the code here.

Meteor’s reactivity is one of its most powerful features but it requires a change in tactics if you are coming from a more traditional framework. Iron Router (IR) was built from the ground up to take advantage of Meteor’s strengths. We will explore some simple patterns to provide navigation and context cues to your user with very little code.

So far, we have learned how to make Meteor respond to routes to render pages. Let’s put that together with Twitter Bootstrap to add some simple navigation. First, add Bootstrap 3:

mrt add bootstrap-3

Once you restart Meteor, you should see that Bootstrap has changed your fonts, and you are ready to begin.

Add a Layout – Router.configure()

We will add Bootstrap’s navbar component for top-level navigation. Since it will live at the top of the page, we would like to have a header region that isn’t re-rendered every time we navigate. Instead of letting Iron Router render our templates directly to document.body, we will give it a layoutTemplate to wrap around our templates. In our javascript:

Specify a layout template
1
2
3
 Router.configure({
   layoutTemplate: 'layout'  //can be any template name
 });

Many of the configuration options that can be set on routes can be set as defaults in Router.configure(), including special-case templates like notFoundTemplate, loadingTemplate, and yieldTemplates. Here, we tell IR to use the template named layout for all routes (this can be overridden in a this.route() specification).

OK, now we need to create our layout:

HTML Layout
1
2
3
4
5
6
7
8
9
10
11
12
<template name="layout">
  <div class="container">
    <header>
      {{> nav }}
    </header>
    {{> yield}}
  </div>
</template>

<template name='nav'>
  Nav goes here...
</template>

All we have done is to set up a template with the name ‘layout’ (as specified in ‘layoutTemplate’–but we could use any name). We added some structure, including a <div> of class “container” so that Bootstrap will center our content. Then we have a <header> element that includes a template called nav with some dummy text, and an IR component, {{> yield}}. The yield component tells IR where to render its output. If you run your app with what we have now, it will again render each page, but the “Nav goes here…” text will remain at the top no matter which route you navigate to. It is possible to have IR render to more than one yield in a template via named yields, but we don’t need that here.

Set Up Bootstrap’s Navbar

Now let’s replace the above nav template to add the bootstrap navbar (modified from bootstrap’s example code)

Bootstrap navbar with inclusions for our customization
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template name='nav'>
  <nav class="navbar navbar-default" role="navigation">
    <div class="container-fluid">
      <!-- Brand and toggle get grouped for better mobile display -->
      <div class="navbar-header">
        <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        {{> navBrand}}
      </div>
      <!-- Collect the nav links, forms, and other content for toggling -->
      <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
        <ul class="nav navbar-nav">
          {{> navItems}}
        </ul>
      </div><!-- /.navbar-collapse -->
    </div><!-- /.container-fluid -->
  </nav>
</template>

This mostly adds bootstrap boilerplate to build the nav. By separating {{> navBrand}} and {{> navItems}}, we can isolate our app-specific markup, making our code more readable and reusable:

Navbar customization
1
2
3
4
5
6
7
8
9
10
11
12
<template name="navBrand">
  <a class="navbar-brand" href="{{ pathFor 'home' }}">IR Demo</a>
</template>

<template name='navItems'>
  <li class="{{ activeIfTemplateIs 'articles' }}">
    <a href="{{ pathFor 'articles'}}">Articles</a>
  </li>
  <li class="{{ activeIfTemplateIs 'about' }}">
    <a href="{{pathFor 'about'}}">About</a>
  </li>
</template>

The links use the {{ pathFor }} helper we discussed in the previous article. If you try the app now, you should get a working navbar that covers three of our templates: home, articles, and about (we’ll get to fullArticle in a moment).

Reactively Set the Navbar’s “active” Class – Router.current()

Bootstrap allows us to show where we are by highlighting a navigation item if its class is set to “active”. You can see in the code above that we have a helper {{ activeIfTemplateIs 'templateName' }} ready and waiting, although it doesn’t do anything yet. We can check which template we are on by looking at the current route controller. IR makes it available via Router.current().

helpers.js
1
2
3
4
5
6
7
8
9
if (Meteor.isClient) {
  Template.navItems.helpers({
    activeIfTemplateIs: function (template) {
      var currentRoute = Router.current();
      return currentRoute &&
        template === currentRoute.lookupTemplate() ? 'active' : '';
    }
  });
}

The helper receives the template variable from our html (e.g., {{ activeIfTemplateIs 'about' }}). We can then get the currently active route controller by by calling Router.current(). This object has a lookupTemplate() method to tell us what template is currently in use (this method name may change in the future, stay tuned). Then we return ‘active’ for the class name if it’s a match, which will trigger the navbar’s highlighting. Router.current() may return null on startup, so we have to guard our lookup with currentRoute &&.

We could also have tested the path '/about' against currentRoute.path. (the reason for choosing template instead of path will be clear below). Router.current() is a reactive data source, so you can use it in template helpers and Deps.autorun() blocks to make reactive updates.

Set up Article Navigation with a Sidebar

If you have updated the code to this point, you will now have a working navbar that shows via highlight if we are currently on the “Articles” or “About” pages (the brand item, which links to home, does not highlight). What about our fullArticle template? Let’s follow a common use case and make the list a sidebar so that when we click on one of the article links on the Articles page, it will display next to the list of articles rather than replacing it. We will need to replace the articles template with the following html:

Replacement for template ‘articles’
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template name = 'articles'>
  <div class='row'>
    <div class="col-sm-4">
      <h2>Articles</h2>
      <ul>
        {{#each articleList}}
          <li>
          <a href="{{ pathFor 'article' }}"> {{title}} </a>
          </li>
        {{/each}}
      </ul>
    </div>
    <div class="col-sm-8">
      {{> fullArticle selectedArticle}}
    </div>
  </div>
</template>

We have added a few bootstrap divs with classes of row and col-sm-4 and so on to format our grid (you can read Bootstrap’s docs if you don’t know how these work). The list of articles will be in a sidebar (note that we now pass articleList instead of this), and the main content area will be populated via {{> fullArticle article}}. The use of articleList and article means we expect these variables to be present in our data context. Let’s redo our routing to accomplish this:

Replacement for previous Router.map()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Router.map(function () {
  this.route('home', {
    path: '/',
  });
  this.route('about');
  this.route('articles', {
    // articles now under `articleList` instead of `this`
    data: {
      articleList: function () {return Articles.find()},
      selectedArticle: {}
    }
  });
  this.route('article', {
    path: '/article/:_id',
    // provide data for both `articleList` and `selectedArticle`
    data: function () {
      return {
        articleList: Articles.find(),
        selectedArticle: Articles.findOne({_id: this.params._id})
      }
    },
    template: 'articles'  //change template target
  });
});

The data function of both the /articles and /article/:_id paths now send both articleList and selectedArticle (which may be empty) for use in rendering. We also set the template for the full article’s route to 'articles' instead of 'fullArticle'.

Now, clicking on an article link will pull up the full article to the right. And since we keyed our {{ activeIfTemplateIs }} helper off of the template rather than the path, the navbar still highlights correctly even though the path changes.

Highlight the Selected Article in the Sidebar

It would also be nice to be able to highlight the selected article in the list on the left. You may have seen this done in the past using Meteor’s Session object, but IR’s reactive data sources make that unnecessary. First, add a selected class for the highlight in your css:

A selected class
1
2
3
.selected {
  background-color: #e7e7e7;
}

Add the class helper in the articles template:

Replacement for the #each block
1
2
3
4
5
{{#each articleList}}
  <li class="{{ maybeSelected }}"> <!-- Add "selected" class (if selected) -->
  <a href="{{ pathFor 'article' }}"> {{title}} </a>
  </li>
{{/each}}

And write your helper in js:

Selected helper
1
2
3
4
5
6
7
8
//make sure you are in the `if (Meteor.isClient)` block
Template.articles.helpers({
  maybeSelected: function () {
    var currentRoute = Router.current();
    return currentRoute &&
      this._id === currentRoute.params._id ? 'selected' : '';
  }
});

How does this work? maybeSelected is called inside an #each helper in the template, which sets the data context to the current record, giving you access that record’s properties, including _id. Meteor sets that context as this in your template helper. We can then compare it with the current route’s id by using Router.current().params._id. The params property gives you access to any named parameters by name, or globs and regexes by index number. This is the :_id portion of the parameter we originally set up in the article route (path: '/article/:_id').

Conclusion

We were able to accomplish a lot with a small amount of code. Notice that we were able to reactively track and refer to our route, template, and selected article without the use of Session.

We touched on the following parts of Iron Router. In javascript string:

Router.configure({
  layoutTemplate: 'template name'
});

Router.map(function () {
  this.route('route name', {
    path: 'path matching string',
    data: function () {return /*cursor or object*/},
    template: 'template name'
  });

Router.current().path
Router.current().template
Router.current().params

And in the templates we covered the following components:

  • {{ pathFor 'route name' }}
  • {{> yield }}

You now know enough to build a single page app quickly and easily. There are more features in Iron Router that we didn’t cover. These include:

  • Router.go() (change route via javascript)
  • The setData() method to change the data context
  • Advanced path parsing, including multiple variables, hash, query, globbing, and regular expressions
  • Server-side routing
  • Hooks (add code to run before, during, and after routing)
  • The action hook and custom rendering
  • Multiple yield regions
  • Route controllers (an object-oriented way to share route configuration between multiple routes)

Look these up when you are ready. Until then, start building your app!

Get the code

You can clone the full code for this post at the github repo for this article.

Comments