Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Meteor: Full-Stack Web Application Development

You're reading from   Meteor: Full-Stack Web Application Development Rapidly build web apps with Meteor

Arrow left icon
Product type Course
Published in Nov 2016
Publisher Packt
ISBN-13 9781787287754
Length 685 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Authors (2):
Arrow left icon
Fabian Vogelsteller Fabian Vogelsteller
Author Profile Icon Fabian Vogelsteller
Fabian Vogelsteller
Marcelo Reyna Marcelo Reyna
Author Profile Icon Marcelo Reyna
Marcelo Reyna
Arrow right icon
View More author details
Toc

Chapter 5. Making Our App Versatile with Routing

Since we've made it to this chapter, we should already have a good understanding of Meteor's template system and how data synchronization between a server and clients works. After digesting this knowledge, let's get back to the fun part and make our blog a real website with different pages.

You might ask, "What do pages do in a single-page app?" The term "single page" is a bit confusing, as it doesn't mean that our app consists of only one page. It's rather a term derived from the current way of doing things, as there is only one page sent down from the server. After that, all the routing and paging happens in the browser. There aren't any pages requested from the server itself anymore. A better term here would be "client-side web application," though single page is the current used name.

In this chapter, we will cover the following topics:

  • Writing routes for our static and dynamic pages
  • Changing subscriptions based on routes
  • Changing the title of the website for each page

So let's not waste time and get started by adding the iron:router package.

Note

If you've jumped right into the chapter and want to follow the examples, download the previous chapter's code examples from either the book's web page at https://www.packtpub.com/books/content/support/17713 or from the GitHub repository at https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter4.

These code examples will also contain all the style files, so we don't have to worry about adding CSS code along the way.

Adding the iron:router package

Routes are the URLs of a specific page in our app. In a server-side-rendered app, routes are defined either by the server's/framework's configuration or the folder structure on the server.

In a client-side app, routes are simply paths that the app will use to determine which pages to render.

The steps to perform inside the client are as follows:

  1. The website is sent down to the client.
  2. The JavaScript file (or files) is loaded and parsed.
  3. The router code will check which current URL it is on and run the correct route function, which will then render the right templates.

    Tip

    To use routes in our app, we will make use of the iron:router package, a router specifically written for Meteor, which makes it easy to set up routes and combine them with subscriptions.

  4. To add the package, we cancel any running Meteor instance, go to our my-meteor-blog folder, and type the following command:
    $ meteor add iron:router
    
  5. If we are done with this, we can start Meteor again by running the $ meteor command.

When we go back to the console of our browser, we will see an error saying: Error: Oh no! No route found for path: "/". Don't worry; we will deal with this in the next section.

Setting up the router

In order to use the router, we need to set it up. To keep our code organized, we will create a file called routes.js directly in the root of our my-meteor-blog folder with the following code:

Router.configure({
    layoutTemplate: 'layout'
});

The router configuration allows you to define the following default templates:

layoutTemplate

The layout template will be used as the main wrapper. Here, subtemplates will be rendered in the {{> yield}} placeholder, which has to be placed somewhere in the template.

notFoundTemplate

This template will be rendered if the current URL has no defined route.

loadingTemplate

This template will be shown when subscriptions for the current route are loading.

For our blog, we will just define the layoutTemplate property for now.

Perform the following steps to set up the router:

  1. To create our first route, we need to add the following lines of code to the routes.js file:
    Router.map(function() {
    
        this.route('Home', {
            path: '/',
            template: 'home'
        });
    
    });

    Note

    You can also name the Home route as home (in lowercase). Then we can leave the manual template definition out, as iron:router will look automatically for a template called home for that route.

    For simplicity, we define the template manually to keep all routes consistent throughout the book.

  2. If we now save this file and get back to our browser, we will see the layout template rendered twice. This happens not because iron:router adds layoutTemplate by default to the body of our app, but because we added it manually as well as by using {{> layout}} in index.html, it gets rendered twice.

To prevent the double appearance of the layout template, we need to remove the {{> layout}} helper from the <body> tag inside our index.html file.

When we check out the browser, we will now see the layout template rendered only once.

Switching to a layout template

Even though we passed a template to our Home route using template: home, we are not rendering this template dynamically; we are just showing the layout template with its hardcoded subtemplates.

To change this, we need to replace the {{> home}} inclusion helper inside our layout template with {{> yield}}.

The {{> yield}} helper is a placeholder helper provided by iron:router, where route templates get rendered.

After doing this, when we check out the browser, we shouldn't see any change, as we are still rendering the home template, but this time dynamically. Then we proceed as follows:

  1. In order to see whether this is true, we will add a not found template to our app, by adding the following template to our layout.html file after the layout template:
    <template name="notFound">
      <div class="center">
        <h1>Nothing here</h1><br>
        <h2>You hit a page which doesn't exist!</h2>
      </div>
    </template>
  2. Now we need to add the notFoundTemplate property to the Router.configure() function of route.js:
    Router.configure({
        layoutTemplate: 'layout',
        notFoundTemplate: 'notFound'
    });

When we now navigate to http://localhost:3000/doesntexist in our browser, we will see the notFound template being rendered instead of our home template:

Switching to a layout template

If we click on the Home link in the main menu, we will get back to our front page, as this link was navigating to "/". We have successfully added our first route. Now let's move on to create the second route.

Adding another route

Having a front page doesn't make a real website. Let's add a link to our About page, which has been in our drawer since Chapter 2, Building HTML Templates.

To do this, just duplicate the Home route and change the values to create an About route, as follows:

Router.map(function() {
    
    this.route('Home', {
        path: '/',
        template: 'home'
    });
    this.route('About', {
        path: '/about',
        template: 'about'
    });
});

That's it!

Now, when we go back to our browser, we can click on the two links in the main menu to switch between our Home and About pages, and even typing in http://localhost:3000/about will bring us straight to the corresponding page, as shown in the following screenshot:

Adding another route

Moving the posts subscription to the Home route

In order to load the right data for each page, we need to have the subscription in the routes instead of keeping it in the separate subscriptions.js file.

The iron:router has a special function called subscriptions() , which is ideal for that purpose. Using this function, we can reactively update subscriptions belonging to a specific route.

To see it in action, add the subscriptions() function to our Home route:

this.route('Home', {
    path: '/',
    template: 'home',
    subscriptions
: function(){
        return Meteor.subscribe("lazyload-posts", Session.get('lazyloadLimit'));
    }
});

The Session.setDefault('lazyloadLimit', 2) line from the subscriptions.js file needs to be placed at the start of the routes.js file and before the Router.configure() function:

if(Meteor.isClient) {
    Session.setDefault('lazyloadLimit', 2);
}

This has to wrapped inside the if(Meteor.isClient){} condition, as the session object is only available on the client.

The subscriptions() function is reactive like the Tracker.autorun() function we used before. This means it will rerun and change the subscription when the lazyloadLimit session variable changes.

In order to see it working, we need to delete the my-meteor-blog/client/subscriptions.js file, so there are not two points where we subscribe to the same publication.

When we now check the browser and refresh the page, we will see the home template still shows all the example posts. Clicking on the lazy-load button increases the number of posts listed, though this time everything happens through our reactive subscriptions() function.

Note

The iron:router comes with more hooks, which you can find as a short list in the Appendix.

To complete our routes, we only need to add the post routes, so we can click on a post and read it in full detail.

Setting up the post route

To be able to show a full post page, we need to create a post template, which can be loaded when the user clicks on a post.

We create a file called post.html inside our my-meteor-blog/client/templates folder with the following template code:

<template name="post">
  <h1>{{title}}</h1>
  <h2>{{description}}</h2>

  <small>
    Posted {{formatTime timeCreated "fromNow"}} by {{author}}
  </small>
  
  <div class="postContent">
    {{#markdown}}
{{text}}
    {{/markdown}}
  </div>
</template>

This simple template displays all the information of a blog post and even reuses our {{formatTime}} helper we created earlier in this book from our template-helper.js file. We used this to format at the time the post was created.

We can't see this template yet, as we first have to create a publication and route for this page.

Creating a single-post publication

In order to show the full post's data in this template, we need to create another publication that will send the complete post document to the client.

To do so, we open our my-meteor-blog/server/publication.js file and add the following publication:

Meteor.publish("single-post", function(slug) {
  return Posts.find({slug: slug});
});

The slug parameter, which has been used here, will be later provided from our subscription method so that we can use the slug parameter to reference the correct post.

Note

A slug is a document title, which is formatted in a way that is usable in a URL. Slugs are better than just appending the document ID to the URL, as they are readable and understandable by visitors. They are also an important part of a good SEO.

So that we can use slugs, every slug has to be unique. We will take care of that when we create the posts.

Assuming that we pass the right slug such as my-first-entry, this publication will send down the post containing this slug.

Adding the post route

In order for this route to work, it needs to be dynamic because every linked URL has to be different for each post.

We will also render a loading template until the post is loaded. To start, we add the following template to our my-meteor-blog/client/templates/layout.html:

<template name="loading">
  <div class="center">
    <h1>Loading</h1>
  </div>
</template>

Additionally, we have to add this template as the default loading template to our Router.configure() call in the routes.js:

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

We then add the following lines of code to our Router.map() function to create a dynamic route:

this.route('Post', {
    path: '/posts/:slug',
    template: 'post',

    waitOn: function() {
        return Meteor.subscribe('single-post', this.params.slug);
    },
    data: function() {
        return Posts.findOne({slug: this.params.slug});
    }
});

The '/posts/:slug' path is a dynamic route, where :slug can be anything and will be passed to the routes functions as this.params.slug. This way we can simply pass the given slug to the single-post subscription and retrieve the correct document for the post matching this slug.

The waitOn() function works like the subscriptions() function, though will automatically render loadingTemplate, we set in the Router.configure() until the subscriptions are ready.

The data() function in this route will set the data context of the post template. We basically look in our local database for a post containing the given slug from the URL.

Note

The findOne() method of the Posts collection works like find(), but returns only the first found result as a JavaScript object.

Let's sum up what happens here:

  1. The route gets called (through a clicked link or by reloading of the page).
  2. The waitOn() function will then subscribe to the right post identified by the given slug parameter, which is a part of the URL.
  3. Because of the waitOn() function, the loadingTemplate will be rendered until the subscription is ready. Since this will happen very fast on our local machine, so we probably won't see the loading template at all.
  4. As soon as the subscription is synced, the template gets rendered.
  5. The data() function will then rerun, setting the data context of the template to the current post document.

Now that the publication and the route are ready, we can simply navigate to http://localhost:3000/posts/my-first-entry and we should see the post template appear.

Linking the posts

Although we've set up the route and subscription, we can't see it work, as we need the right links to the posts. As each of our previously added example posts already contains a slug property, we just have to add them to the links to our posts in the postInList template. Open the my-meteor-blog/client/templates/postInList.html file and change the link as follows:

<h2><a href="posts/{{slug}}">{{title}}</a></h2>

Finally, when we go to our browser and click on the title of a blog post, we get redirected to a page that shows the full post entry, like the entry shown in the following screenshot:

Linking the posts

Creating a single-post publication

In order to show the full post's data in this template, we need to create another publication that will send the complete post document to the client.

To do so, we open our my-meteor-blog/server/publication.js file and add the following publication:

Meteor.publish("single-post", function(slug) {
  return Posts.find({slug: slug});
});

The slug parameter, which has been used here, will be later provided from our subscription method so that we can use the slug parameter to reference the correct post.

Note

A slug is a document title, which is formatted in a way that is usable in a URL. Slugs are better than just appending the document ID to the URL, as they are readable and understandable by visitors. They are also an important part of a good SEO.

So that we can use slugs, every slug has to be unique. We will take care of that when we create the posts.

Assuming that we pass the right slug such as my-first-entry, this publication will send down the post containing this slug.

Adding the post route

In order for this route to work, it needs to be dynamic because every linked URL has to be different for each post.

We will also render a loading template until the post is loaded. To start, we add the following template to our my-meteor-blog/client/templates/layout.html:

<template name="loading">
  <div class="center">
    <h1>Loading</h1>
  </div>
</template>

Additionally, we have to add this template as the default loading template to our Router.configure() call in the routes.js:

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

We then add the following lines of code to our Router.map() function to create a dynamic route:

this.route('Post', {
    path: '/posts/:slug',
    template: 'post',

    waitOn: function() {
        return Meteor.subscribe('single-post', this.params.slug);
    },
    data: function() {
        return Posts.findOne({slug: this.params.slug});
    }
});

The '/posts/:slug' path is a dynamic route, where :slug can be anything and will be passed to the routes functions as this.params.slug. This way we can simply pass the given slug to the single-post subscription and retrieve the correct document for the post matching this slug.

The waitOn() function works like the subscriptions() function, though will automatically render loadingTemplate, we set in the Router.configure() until the subscriptions are ready.

The data() function in this route will set the data context of the post template. We basically look in our local database for a post containing the given slug from the URL.

Note

The findOne() method of the Posts collection works like find(), but returns only the first found result as a JavaScript object.

Let's sum up what happens here:

  1. The route gets called (through a clicked link or by reloading of the page).
  2. The waitOn() function will then subscribe to the right post identified by the given slug parameter, which is a part of the URL.
  3. Because of the waitOn() function, the loadingTemplate will be rendered until the subscription is ready. Since this will happen very fast on our local machine, so we probably won't see the loading template at all.
  4. As soon as the subscription is synced, the template gets rendered.
  5. The data() function will then rerun, setting the data context of the template to the current post document.

Now that the publication and the route are ready, we can simply navigate to http://localhost:3000/posts/my-first-entry and we should see the post template appear.

Linking the posts

Although we've set up the route and subscription, we can't see it work, as we need the right links to the posts. As each of our previously added example posts already contains a slug property, we just have to add them to the links to our posts in the postInList template. Open the my-meteor-blog/client/templates/postInList.html file and change the link as follows:

<h2><a href="posts/{{slug}}">{{title}}</a></h2>

Finally, when we go to our browser and click on the title of a blog post, we get redirected to a page that shows the full post entry, like the entry shown in the following screenshot:

Linking the posts

Adding the post route

In order for this route to work, it needs to be dynamic because every linked URL has to be different for each post.

We will also render a loading template until the post is loaded. To start, we add the following template to our my-meteor-blog/client/templates/layout.html:

<template name="loading">
  <div class="center">
    <h1>Loading</h1>
  </div>
</template>

Additionally, we have to add this template as the default loading template to our Router.configure() call in the routes.js:

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

We then add the following lines of code to our Router.map() function to create a dynamic route:

this.route('Post', {
    path: '/posts/:slug',
    template: 'post',

    waitOn: function() {
        return Meteor.subscribe('single-post', this.params.slug);
    },
    data: function() {
        return Posts.findOne({slug: this.params.slug});
    }
});

The '/posts/:slug' path is a dynamic route, where :slug can be anything and will be passed to the routes functions as this.params.slug. This way we can simply pass the given slug to the single-post subscription and retrieve the correct document for the post matching this slug.

The waitOn() function works like the subscriptions() function, though will automatically render loadingTemplate, we set in the Router.configure() until the subscriptions are ready.

The data() function in this route will set the data context of the post template. We basically look in our local database for a post containing the given slug from the URL.

Note

The findOne() method of the Posts collection works like find(), but returns only the first found result as a JavaScript object.

Let's sum up what happens here:

  1. The route gets called (through a clicked link or by reloading of the page).
  2. The waitOn() function will then subscribe to the right post identified by the given slug parameter, which is a part of the URL.
  3. Because of the waitOn() function, the loadingTemplate will be rendered until the subscription is ready. Since this will happen very fast on our local machine, so we probably won't see the loading template at all.
  4. As soon as the subscription is synced, the template gets rendered.
  5. The data() function will then rerun, setting the data context of the template to the current post document.

Now that the publication and the route are ready, we can simply navigate to http://localhost:3000/posts/my-first-entry and we should see the post template appear.

Linking the posts

Although we've set up the route and subscription, we can't see it work, as we need the right links to the posts. As each of our previously added example posts already contains a slug property, we just have to add them to the links to our posts in the postInList template. Open the my-meteor-blog/client/templates/postInList.html file and change the link as follows:

<h2><a href="posts/{{slug}}">{{title}}</a></h2>

Finally, when we go to our browser and click on the title of a blog post, we get redirected to a page that shows the full post entry, like the entry shown in the following screenshot:

Linking the posts

Linking the posts

Although we've set up the route and subscription, we can't see it work, as we need the right links to the posts. As each of our previously added example posts already contains a slug property, we just have to add them to the links to our posts in the postInList template. Open the my-meteor-blog/client/templates/postInList.html file and change the link as follows:

<h2><a href="posts/{{slug}}">{{title}}</a></h2>

Finally, when we go to our browser and click on the title of a blog post, we get redirected to a page that shows the full post entry, like the entry shown in the following screenshot:

Linking the posts

Changing the website's title

Now that we have the routes of our posts working, we are only missing the right titles being displayed for each page.

Sadly, <head></head> is not a reactive template in Meteor, where we could let Meteor do the work of changing titles and meta tags.

Note

It is planned to make the head tag a reactive template, but probably not before version 1.0.

To change the document title, we need to come up with a different way of changing it, based on the current route.

Luckily, iron:router has the onAfterAction() function, which can also be used in the Router.configure() function to run before every route. In this function, we have access to the data context of the current route, so we can simply set the title using native JavaScript:

Router.configure({
    layoutTemplate: 'layout',
    notFoundTemplate: 'notFound',

    onAfterAction: function() {
        var data = Posts.findOne({slug: this.params.slug});

        if(_.isObject(data) && !_.isArray(data))
            document.title = 'My Meteor Blog - '+ data.title;
        else
            document.title = 'My Meteor Blog - '+ this.route.getName();
    }
});

Using Posts.findOne({slug: this.params.slug}), we get the current post of the route. We then check whether it's an object; if so, we add the post title to the title metatag. Otherwise, we just take the route name.

Doing this in Router.configure() will call the onAfterAction for every route.

If we now take a look at our browser's tab, we will see that the title of our website changes when we move throughout the website:

Changing the website's title

Tip

If we want to make our blog cooler, we can add the mrt:iron-router-progress package. This will add a progress bar at the top of our pages when changing routes. We just need to run the following command from our app's folder:

$ meteor add mrt:iron-router-progress

Summary

That's it! Our app is now a fully working website with different pages and URLs.

In this chapter, we learned how to set up static and dynamic routes. We moved our subscriptions to the routes so that they change automatically, based on the route's needs. We also used slugs to subscribe to the right posts and displayed them in the post template. Finally, we changed our website's title so that it matches the current route.

To learn more about iron:router, take a look at its documentation at https://github.com/EventedMind/iron-router.

You can find this chapter's code examples either at https://www.packtpub.com/books/content/support/17713 or on GitHub at https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter5.

In the next chapter, we will take a deeper look at Meteor's session object.

You have been reading a chapter from
Meteor: Full-Stack Web Application Development
Published in: Nov 2016
Publisher: Packt
ISBN-13: 9781787287754
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image