Logo
ManuelSchoebel

Iron-Router Tutorial

The Iron-Router is a "client and server side router designed specifically for Meteor". Before the Iron-Router there were two router that were most commonly used: Meteor-Router from Tom Coleman and Mini-Pages from Chris Mather. The learnings from those two routers are now combined into the Iron-Router project, created by Chris Mather and Tom Coleman. Also there is no router planned for the Meteor 1.0 release and it is not unrealistic that the Iron-Router will become the standard router package for Meteor. Because of this and also because the Iron-Router helps you a lot it is worth looking into it.

##WHAT IS SO SPECIAL ABOUT A ROUTER FOR METEOR? Meteor works differently than other javascript frameworks like for example angular.js or backbone.js. Because of this, the router also has special requirements. For example: You have a site for path '/posts' where a list of posts will be shown. That means you have to have a subscription for the data of several posts and you have to wait for the data before you can render the template. What do you do while you are waiting for the data? You should show a loading template. Also you want to be able to access the data in your template. All this can be really annoying to implement yourself. The Iron-Router helps you do all of this really easily.

##IN THIS ARTICLE YOU WILL LEARN

  • How to add the Iron-Router to your project
  • How to create routes
  • How to handle data and subscriptions
  • How to create and use multiple layouts
  • How to structure your meteor app with route controllers

So let's start and have a look how to add the Iron-Router.

##ADDING THE IRON-ROUTER TO YOUR METEOR PROJECT The Meteor.js Iron Router is on athmosphere so you can simply add it with mrt.

    mrt add iron-router

##CREATING ROUTES ###A SIMPE ROUTE WITH THE IRON-ROUTER Basically creating a route works as this:

    Router.map(function(){
        this.route(name:String, options:Object);
    });

Within the Router.map function you call the route function and pass the name of the route and options that are optional. Imagine we want to render some static information when you navigate to the path '/about'. For that we have a template called 'about'. With the Iron-Router you can do this in only a few lines:

    // simple route with
    // name 'about' that
    // matches '/about' and automatically renders
    // template 'about'
    Router.map( function () {
      this.route('about');
    });

This creates a route named 'about'. It also automatically matches the path '/about' and renders the template 'about'. Based on the name of the router, the Iron-Router already does a lot of stuff. Of course you can always configure a route exactly as you want and override the default behavior. For example you want a route for your home path '/' but you cannot name your route like this, of course. That means the name of our route and the path will be different:

    // simple route with
    // name 'home' that
    // matches '/' and automatically renders
    // template 'home'
    this.route('home', {
      path: '/'
    });

This way the Iron-Router will render the template 'home' if someone navigate to the path '/'. You can also use the path option to create different complex routes, e. g. with parameters or regular expressions.

###ROUTE PARAMETER

    // complex route with
    // name 'postDetail' that for example
    // matches '/posts/1' or '/posts/hello-world' and automatically renders
    // template 'postDetail'
    this.route('postDetail', {
      // get parameter via this.params
      path: '/posts/:_id'
    });

###OPTIONAL ROUTE PART

    // complex route with
    // name 'authorDetail' that for example
    // matches '/authors/1/edit' or '/authors/1' and automatically renders
    // template 'authorDetail'
    // HINT:
    //// get parameter via this.params
    //// the part '/edit' is optional because of '?'
    this.route('authorDetail', {
      path: '/authors/:_id/edit?'
    });

###ANONYMOUS PARAMETER GLOBBING

    // complex route with
    // name 'authorNotFound' that for example
    // matches '/authors/123' or '/authors/dermambo' and automatically renders
    // template 'authorNotFound'
    // HINT:
    //// Get path of * (e.g. '/authors/are-you/there') from this.params (e.g. this.params = ['are-you/there', hash: undefined])
    //// Create a route like this always at last! The routes are checked in the order you created them.
    //// A route like "path:'/authors/:_id'" would never used if it is created after this one  
    this.route('authorNotFound', {
      path: '/authors/*'
    });

###NAMED PARAMETER GLOBBING

    // complex route with
    // name 'anyfile' that for example
    // matches '/download.txt' or '/me.jpg' and automatically renders
    // template 'anyfile'
    // HINT:
    //// Get path of :file(*) (e.g. '/files/invoice.pdf') from this.params.file (e.g. this.params = ["invoice.pdf", file: "invoice.pdf", hash: undefined] )
    this.route('anyfile', {
      path: '/files/:file(*)'
    });

###REGULAR EXPRESSIONS

    // complex route with
    // name 'listPostsByDate' that for example
    // matches '/posts/date/2013-03-23' and automatically renders
    // template 'listPostsByDate'
    // HINT:
    //// Get parts of route (e.g. '/posts/date/2013-03-23') from this.params (e.g. this.params = ["2013", "03", "23", hash: undefined] )
    this.route('listPostsByDate', {
      path: /^\/posts\/date\/(\d+)\-(\d+)\-(\d+)/
    });

###GLOBAL NOT FOUND ROUTE

    // complex route with
    // name 'notFound' that for example
    // matches '/non-sense/route/that-matches/nothing' and automatically renders
    // template 'notFound'
    // HINT:
    //// Define a global not found route as the very last route in your router
    //// Also this is different from the notFoundTemplate in your Iron Router
    //// configuration!
    this.route('notFound', {
      path: '*'
    });

###USING THE ROUTE NAMES With the Iron-Router you should never type a path directly. For example in a simple link you could do it like this:

    <nav>
      <a href="/about">About</a>
    </nav>

Create a named route:

    // simple route with
    // name 'about' that
    // matches '/about' and automatically renders
    // template 'about'
    Router.map( function () {
      this.route('about');
    });

And use the handlebars "pathFor" helper:

    <nav>
      <a href="{{ pathFor 'about' }}">About</a>
    </nav>

This will be the same. But you get a huge benefit: If you change the path later on, you will not have to change anything in your template.

The Iron-Router gives us a helper to get the path and one for the entire url:

    <nav>
      <a href="{{ pathFor 'about' }}">About</a>
      <a href="{{ urlFor 'imprint' }}">Imprint</a>
    </nav>

You can also access this in your code:

    // returns '/posts/1'
    Router.routes['postDetail'].path({_id: 1});
    // returns 'http://localhost:3000/posts/1'
    Router.routes['postDetail'].url({_id: 1});

As you can see, if you have a route with a parameter, you have to pass the route parameter as an object attribute. If you want to do this in a handlebars helper, make sure that the needed data is available in your template. We will see how to do this in the next part.

##HANDLE DATA AND SUBSCRIPTIONS WITH THE IRON-ROUTER ###DATA CONTEXT AS AN OBJECT In the examples above, we wanted to render a list of posts in a template. For that we need to have data - a list of posts. Let us imagine this is our template:

    <template name="posts">
      <h2>A List Of Blog Posts</h2>
      {{#each posts}}
        <h4>{{ title }}</h4>
        <p>{{ text }}</p>
      {{/each}}
    </template>

In our router we can now specify the data that is available in our template:

    // Data context as an object
    this.route('posts', {
      path: '/posts',
      data: {
        posts: [
          {
            title: 'Did you know that...',
            text: 'If you yelled for 8 years, 7 months and 6 days, you would have produced enough sound energy to heat up one cup of coffee.'
          },
          {
            title: 'Hello World',
            text: 'Hi, i am new here!'
          }
        ]
      }
    });

We simply specify the 'data' option as an object. You can pass an object like this directly or you can pass a function.

###DATA CONTEXT AS A FUNCTION Let us say we want to render the postDetail template that also shows the _id parameter of the route itself:

    <template name="postDetail">
      <h1>{{ title }}</h1>
      <p><i>[ID: {{ _id }}]</i></p>
      <p>{{ text }}</p>
      <a href="{{ pathFor 'oldData' }}">old data example</a>
    </template>

We now have to access the route parameter and pass it to the data of our template.

    // Data context as a function
    this.route('postDetail', {
      path: '/posts/:_id',
      data: function (){
        _id  = this.params._id;
        templateData = {
          _id: _id,
          title: 'Did you know that...',
          text: 'If you yelled for 8 years, 7 months and 6 days, you would have produced enough sound energy to heat up one cup of coffee.'
        };
        return templateData;
      }
    });

As we can see, we define the 'data' option as a function. In this function we can access the route parameters simply with 'this.params' and pass it to the templateData object we return in this data-function.

###DATA CONTEXT FROM A COLLECTION Until now, we just passed some static data to the template. But what we really want to do is to pass data from our collections. We now have a template in which we want to list different authors. The template looks like this:

    <template name="authors">
      <h1>List of all authors</h1>
      {{#each authors}}
        <a href="{{ pathFor 'authorDetail' }}">{{ name }}</a>
      {{/each}}
    </template>

Our goal is to get our list of authors out of the database. In order to do this we use the 'Authors' collection:

    // Data context from a collection
    this.route('authors', {
      data: function() {
        templateData = { authors: Authors.find({}) };
        return templateData;
      }
    });

In this case we can access our authors in the template because the Iron-Router handles the data context of the template for us automatically. In this special case we do not even need the data option to be a function. You could also simply pass the object directly like this:

    // Data context from a collection
    this.route('authors', {
      data:  { authors: Authors.find({}) };
      }
    });

Now we have our authors listed and if we click on one author we will see the author detail page. Lets say we opened a authors profile that does not exist, how can we handle the case when there is no data? The Iron-Router also helps us with that because you can specify a 'notFoundTemplate':

    // Data context from a collection with "notFound" template
    // HINT:
    //// If data is 'null' the notFoundTemplate will be automatically rendered
    //// Also this is only for data and NOT for bad url paths. Should be called noDataFoundTemplate :-)
    this.route('authorDetail', {
      path: '/authors/:_id',
      notFoundTemplate: 'authorNotFound',
      data: function() {
        return Authors.findOne({_id: this.params._id});
      }
    });

The Iron-Router will automatically render the specified 'notFoundTemplate' if the data object is 'null'. Because 'Authors.findOne(...)' returns null if there is nothing found, it works out really easily. One other problem we have to think of when dealing with data is, how we handle waiting for subscriptions. And of course the Iron-Router also helps with this a lot!

###WAITING FOR A SUBSCRIPTION What we typically want to do is to render something that indicates 'loading'. We can do this in the global router configuration and tell the Iron-Router what template it should render while we are waiting for one or multiple subscriptions to be ready.

    // Global router config
    Router.configure({
      loadingTemplate: 'loading'
    });

Now the Iron-Router knows what to do while waiting. But we also have to tell the Iron-Router on what exactly it has to wait and we can specify this with the 'waitOn' option in the route options like this:

    // Wait for a subscription before rendering
    // HINT:
    //// waitOn can return any object with a reactive ready() function
    //// you could also return an array with those objects e.g. [Authors.find(), Posts.find()]
    this.route('authors', {
      waitOn: function() {
        return Meteor.subscribe('authorList');
      },
      data: function () {
        templateData = { authors: Authors.find() };
        return templateData;
      }
    });

What happens here is, that the Iron-Router gets a subscription handle as the 'waitOn' option. The Iron-Router now calls the ready() function of the handle. Since it is a reactive data source, the ready function gets called again when the state changes. If the ready() function returns 'true' the Iron-Router knows, the data is arrived and we can go on now. The next that happens is, that the data object is created and the template get rendered. If the ready function returns false, the Iron-Router will render the specified 'loadingTemplate' from the global router configuration. Much like the 'data' option the 'waitOn' option can also be an object, an array of objects or a function that returns either an object or an array of objects. If you have an array of objects, the Iron-Router checks every object to be ready. If one isn't ready the 'loadingTemplate' will be rendered.

##LAYOUT AND RENDERING Since now we always rendered the template that matches the name of the route. Of course we can easily configure a different template that should be rendered instead of the one that the Iron-Router automatically looks for by the name of the route:

    // Specify a different template than 'home'
    this.route('home', {
      path: '/',
      template: 'myCustomHomeTemplate'
    });

In this case the Iron-Router will not render the 'home' template but instead it will render the 'myCustomHomeTemplate' for us.

###SPECIFY A LAYOUT One thing that is very important and makes our lives much easier are layouts. In Meteor we only render and rerender parts of the dom and not always the whole thing. As an example we want to have a layout that specifies a menu, a footer and an area where the main content is rendered in. That could look like this:

    <template name="complexLayout">
      <div class="left">
        {{> yield region="menu"}}
      </div>
 
 
      <div class="main">
        {{> yield}}
      </div>
      <div class="bottom">
        {{> yield region="footer"}}
      </div>
    </template>

In this template are three areas that the Iron-Router renders in. One 'Main-Yield' and two 'Named-Yields' or 'Regions' with the names 'menu' and 'footer'. The names are important if we want to specify what template we want to render where. We now want that the Iron-Router to use our layout and to render the template 'myMenu' to the yield with the name 'menu' and the template 'myFooter' to the yield named 'footer'.

    this.route('home', {
      path: '/',
      layoutTemplate: 'complexLayout',
      yieldTemplates: {
        'myMenu': {to: 'menu'},
        'myFooter': {to: 'footer'}
      }
    });

We achieve this with the route options 'layoutTemplate' and 'yieldTemplates'. The Iron-Router will now render as we like and also render the 'home' template to the unnamed main yield.

###OVERRIDE THE ROUTE FUNCTION Normally, the Iron-Router does everything automatically for us if we just specify the options. But if we want to have some special logic and more control we can also create our own function that is used when the route matches the path. For that we use the route option named 'action' which is a function:

    // Use a custom action function instead of automatic rendering
    this.route('home', {
      path: '/',
      layoutTemplate: 'complexLayout',
      action: function() {
        // this is an instance of RouteController
        // access to:
        //  this.params
        //  this.wait
        //  this.render
        //  this.stop
        //  this.redirect
 
 
        // render yieldTemplates
        this.render('myMenu', {to: 'menu'});
        this.render('myFooter', {to: 'footer'});
        this.render('home');
      }
    });

If we navigate to the path '/' the Iron-Router will call our own action function where we now have access to all we need to wait, react on route parameters or do what ever we like to do. That means we have the flexibility to write really less code and let the Iron-Router do the magic or take control our selves completely where necessary.

If your project is getting bigger, the examples above will get unhandy. If you specify your routes in one file, like I often do, you will not want to specify all the 'waitOn', 'data', custom 'action' functions and more all in this file. This is where we use the RouteControllers to structure our app better.

##Iron Router Global Configuration It is better to use the global Iron Router configuration to specify a notFoundTemplate, a layoutTemplate and the loadingTemplate rather than doing this in a route itself:

    Router.configure({
      layoutTemplate: 'layout',
      notFoundTemplate: 'notFound',
      loadingTemplate: 'loading'
    });

Also since a newer version of the Iron-Router it is important for the loading template to be rendered, to add a 'onBeforeHook' like this:

    Router.onBeforeAction('loading');

If you do not know what before-hooks are, check out my other blog post about filters, before and after hooks.

##STRUCTURE WITH CONTROLLERS A RoutController can basically do everything you could also specify in a route, so you can specify what layout to use, what to render or on what subscriptions the Iron-Router should wait for the given route. First we specify which controller for a specified path is to use:

    this.route('myDocuments', {
      path: '/documents',
      controller: 'MyDocumentsController'
    });

The controller then could look like this:

    MyDocumentsController = BaseController.extend({
      layoutTemplate: 'baseLayout',
      yieldTemplates: {
        'userMenu': {to: 'menu'}
      },
 
 
      waitOn: function() {
        return Meteor.subscribe('waitingFor');
      },
 
 
      data: function () {
        data = { waitingFor : AllwaysLate.find() };
        return data;
      },
 
 
      onBeforeAction: function(){
 
      },
 
 
      onAfterAction: function(){
 
 
      },
 
      action: function(){
        this.render();
      }
    });

As you can see, the attributes of the controller are looking pretty much the same as the route options. If you specify everything directly in the route itself, the Iron-Router also uses a RouteController, so it is pretty much the same internally. As you can see i extended the 'MyDocumentsController' from a controller named 'BaseController' which is NOT the controller from Iron-Router. I always create something like this in case i need functions that all of my controllers have in common. For a simple 'BaseController' you can just inherit from Iron-Routers 'RouteController' like this:

    BaseController = RouteController.extend({
      // specify stuff that every controller should have
    });

There is much more to the Iron-Router like server-side rendering, before and after hooks and more. But for this basic tutorial we are done! If you have questions or feedback just get in touch via twitter/Manuel_Schoebel.

©️ 2024 Digitale Kumpel GmbH. All rights reserved.