Meteor is an amazing platform for being able to quickly build web applications. This isn’t a review of it, but I’ll probably get one out at some point.
Meteor’s great and all, but how do you handle multiple pages with it? In it’s templating docs it shows how to use templates inside of other templates, but not how to switch the content/templates. After a good deal of hunting, I found the solution in Meteor’s github wiki however it was lacking any depth of detail so I decided someone ought to post about it.
You can grab the example I made, and thoroughly commented, from github. I’ll go over the important bits here.
I’m not going to go over Meteor or it’s different parts, they have excellent documentation setup for that. I’m just going to be focusing on how to do this multiple page setup.
The tl;dr
By using one or more Block Helpers we can control which template(s) are shown. Block Helpers are, essentially, customizable conditionals. So, we keep track of the page we’re on and change that when links are clicked, stop links’ default behavior, and then put our logic inside the Block Helper. Meteor will handle the rest for us.
HTML
Let’s look at the full .html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | <head> <title>multiple-view-example</title> </head> <body> <nav> <ul> <li><a href="/">Default</a></li> <li><a href="/one">One</a></li> <li><a href="/two">Two</a></li> <li><a href="/three">Three</a></li> </ul> </nav> {{> body}} </body> <template name="body"> {{#page_is "/"}} {{> default}} {{/page_is}} {{#page_is "/one"}} {{> one}} {{/page_is}} {{#page_is "/two"}} {{> two}} {{/page_is}} {{#page_is "/three"}} {{> three}} {{/page_is}} </template> <template name="default"> This is the default page. </template> <template name="one"> This is page one. <ul> <li><a class="subnav" href="/foo">Subpage foo</a></li> <li><a class="subnav" href="/bar">Subpage bar</a></li> <li><a class="subnav" href="/baz">Subpage baz</a></li> </ul> {{#sub_is "/foo"}} {{> foo}} {{/sub_is}} {{#sub_is "/bar"}} {{> bar}} {{/sub_is}} {{#sub_is "/baz"}} {{> baz}} {{/sub_is}} </template> <template name="foo"> Sub page foo </template> <template name="bar"> Sub page bar </template> <template name="baz"> Sub page baz </template> <template name="two"> This is page two. </template> <template name="three"> This is page three. </template> |
Let’s break that down into bite-sized bits and pieces. First, the body:
5 6 7 8 9 10 11 12 13 14 15 | <body> <nav> <ul> <li><a href="/">Default</a></li> <li><a href="/one">One</a></li> <li><a href="/two">Two</a></li> <li><a href="/three">Three</a></li> </ul> </nav> {{> body}} </body> |
All we’re doing in here is setting up the navigation and then referencing the template named body.
17 18 19 20 21 22 23 24 25 26 27 28 29 30 | <template name="body"> {{#page_is "/"}} {{> default}} {{/page_is}} {{#page_is "/one"}} {{> one}} {{/page_is}} {{#page_is "/two"}} {{> two}} {{/page_is}} {{#page_is "/three"}} {{> three}} {{/page_is}} </template> |
In that template, we’re making use of our main Block Helper (which I’ll get to later on). It’s like a customized if conditional. Whether or not the page is equal to the string we’re passing it, it will then show us whatever is inside the block – in this case other templates.
The rest of the templates are almost all super simple (just text), so I won’t bother going over them. The only one which deserves some attention is the template named one, which showcases how it’s possible to handle subpages with this approach as well.
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | <template name="one"> This is page one. <ul> <li><a class="subnav" href="/foo">Subpage foo</a></li> <li><a class="subnav" href="/bar">Subpage bar</a></li> <li><a class="subnav" href="/baz">Subpage baz</a></li> </ul> {{#sub_is "/foo"}} {{> foo}} {{/sub_is}} {{#sub_is "/bar"}} {{> bar}} {{/sub_is}} {{#sub_is "/baz"}} {{> baz}} {{/sub_is}} </template> |
Javascript
We’ve got some content, a subnav, and then another Block Helper which is specific to this template. All things we’ve previously discussed – so let’s not linger on them and instead move on to the javascript.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | if ( Meteor.is_client ) { var callbacks = { '/': function() { }, '/one': function() { Session.set( 'sub', '/foo' ); }, '/two': function() { }, '/three': function() { } }, noop = function(){}; function getCallback( data ) { return callbacks.hasOwnProperty( data ) ? callbacks[ data ] : noop; } Session.set( 'page', '/' ); Template.body.page_is = function( data, options ) { if ( Session.equals( 'page', data ) ) { setTimeout( getCallback( data ), 0 ); return options.fn( this ); } return options.inverse( this ); }; Template.one.sub_is = function( data, options ) { if ( Session.equals( 'sub', data ) ) { return options.fn( this ); } return options.inverse( this ); }; Meteor.startup( function() { $( document ).on( 'click', function( e ) { if ( e.target.nodeName === 'A' ) { var $this = $( e.target ); if ( $this.hasClass( 'subnav' ) ) { Session.set( 'sub', $this.attr( 'href' ) ); } else { Session.set( 'page', $this.attr( 'href' ) ); } return false; } } ); } ); } if ( Meteor.is_server ) { Meteor.startup( function() { return; } ); } |
Yikes! I know, I know – it seems like a lot. Let’s break it down so that we can digest it. Since there’s nothing in the server bit, we’ll only be going over what’s in the Meteor.is_client section.
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | var callbacks = { '/': function() { }, '/one': function() { Session.set( 'sub', '/foo' ); }, '/two': function() { }, '/three': function() { } }, noop = function(){}; |
Alright, so we’ve got our variable declarations. We’re storing references to callbacks that will happen on the main pages as well as an empty function in case someone stumbles onto a page without a callback assigned to it.
19 20 21 | function getCallback( data ) { return callbacks.hasOwnProperty( data ) ? callbacks[ data ] : noop; } |
This is our helper function which returns either the callback associated with the page or the empty function.
23 | Session.set( 'page', '/' ); |
Alright, here we’re making use of Meteor’s Session object and setting the default page. This is what will first show up every time you visit the site.
25 26 27 28 29 30 31 | Template.body.page_is = function( data, options ) { if ( Session.equals( 'page', data ) ) { setTimeout( getCallback( data ), 0 ); return options.fn( this ); } return options.inverse( this ); }; |
This is the main Block Helper. It simply checks if page in Session is equal to the data we’re checking against (the string from the templates in our .html). If it is, we set the callback to run as soon as possible (so that it will occur after our return) and then return options.fn(this) which will return whatever is inside the main block. The other return of options.inverse(this) is the do whatever is in the {{else}} block, which I didn’t put into the example.
Our subpage Block Helper is pretty much the exact same thing as our main Block Helper, but it doesn’t have any callback in it.
40 41 42 43 44 45 46 47 48 49 50 51 52 53 | Meteor.startup( function() { $( document ).on( 'click', function( e ) { if ( e.target.nodeName === 'A' ) { var $this = $( e.target ); if ( $this.hasClass( 'subnav' ) ) { Session.set( 'sub', $this.attr( 'href' ) ); } else { Session.set( 'page', $this.attr( 'href' ) ); } return false; } } ); } ); |
Meteor.startup() is the equivalent to $(document).ready() for jQuery. Inside of this, we’re capturing all click events on the entire document. We do this so that we can utilize a single handler for all elements, including those added dynamically later on. If the target is a link tag, we handle it by setting either sub or page in Session with it’s href and then returning false which is the same as doing e.preventDefault() and e.stopPropagation().