This is the fourth in a x part series about Meteor for beginners.
- Part One – Leaderboard: Add a button to change sort order
- Part Two – Leaderboard: Add a button to randomise players’ scores
- Part Three – Leaderboard: Add and remove players from the leaderboard
- Part Four – Todos: make urls friendly (this post)
Introduction
I’ve been trying to get up to speed with Meteor lately. Since I don’t have any experience with nodejs it’s been a challenge.
I started by looking at the leaderboard sample, and then the todos sample. At the end of the video the author lays down a challenge – to make the URLs friendly. So instead of http://localhost:3000/bbbeec7c-6749-400b-bc45-4ed744e010b7 it should be http://localhost:3000/Languages. In order to do that I needed to understand how routing works.
When you click on a todo list in the left, this code fires:
Template.lists.events = { 'mousedown .list': function (evt) { // select list Router.setList(this._id); },
which calls:
var TodosRouter = Backbone.Router.extend({ routes: { ":list_id": "main" }, main: function (list_id) { Session.set("list_id", list_id); Session.set("tag_filter", null); }, setList: function (list_id) { this.navigate(list_id, true); } });
The setList function calls navigate, which is Backbone.js’s router function to save the current location in the browser’s history. The “true” parameter also tells the browser to navigate to the url, so if list_id = “1234” the browser will navigate to i.e. localhost:3000/1234.
According to the routes defined here
var TodosRouter = Backbone.Router.extend({ routes: { ":list_id": "main" },
if I navigate to localhost:3000/1234, then “1234” will be passed to the “main” function as the “list_id” parameter. Which takes us to:
var TodosRouter = Backbone.Router.extend({ routes: { ":list_id": "main" }, main: function (list_id) { Session.set("list_id", list_id); Session.set("tag_filter", null); }, setList: function (list_id) { this.navigate(list_id, true); } });
OK, so the only thing that the “main” function does is set two Session variables. What effect does that have? Time to look at some html.
client/todos.html:
<template name="todos"> {{#if any_list_selected}} <div id="items-view"> <div id="new-todo-box"> <input type="text" id="new-todo" placeholder="New item" /> </div> <ul id="item-list"> {{#each todos}} {{> todo_item}} {{/each}} </ul> </div> {{/if}} </template>
That there is a Handlebars template, with name=”todos”. Interestingly, it has a “#each todos”. The data for that is retrieved by a helper function called “todos” (on the Template also called “todos”), i.e. this:
Template.todos.todos = function () { // Determine which todos to display in main pane, // selected based on list_id and tag_filter. var list_id = Session.get('list_id'); if (!list_id) return {}; var sel = {list_id: list_id}; var tag_filter = Session.get('tag_filter'); if (tag_filter) sel.tags = tag_filter; return Todos.find(sel, {sort: {timestamp: 1}}); };
This template (like all Meteor templates) reactively monitors the Session and detects that the value for Session.get(‘list_id’) has changed, so it runs again and returns the todo items to be rendered. Each todo item is then rendered by another Handlebars template called todo_item , as per the {{> todo_item}} in the html.
In summary then,
- Todo list link is clicked in the left pane, calls TodosRouter.setList(list_id).
- TodosRouter.setList navigates to ~/list_id
- Router.main sets Session[list_id] variable.
- The template helper “todos” detects Session[list_id] has changed so it re-runs.
- The Handlebars template named “todos” is re-rendered with the result of the “todos” template helper.
So how do we make the URLs friendly?
Now that we understand how routing works in Meteor, we can take the TodosRouter shown above and change:
- TodosRouter.setList(list_id) to TodosRouter.setList(list_name)
- TodosRouter.setList should navigate to ~/list_name
- Router.main can lookup the correct list_id from the list_name and set the Session[list_id] as before.
So
var TodosRouter = Backbone.Router.extend({ routes: { ":list_name": "main" }, main: function (list_name) { var list = Lists.findOne({name: list_name}); if (list) Session.set("list_id", list._id); Session.set("tag_filter", null); }, setList: function (list_name) { this.navigate(list_name, true); } });
We also need to change all the code which calls setList():
Template.lists.events = { 'mousedown .list': function (evt) { // select list Router.setList(this.name); },
Meteor.subscribe('lists', function () { if (!Session.get('list_id')) { var list = Lists.findOne({}, {sort: {name: 1}}); if (list) Router.setList(list.name); } });
Template.lists.events[ okcancel_events('#new-list') ] = make_okcancel_handler({ ok: function (text, evt) { var id = Lists.insert({name: text}); Router.setList(text);
Finally, for cosmetic reasons, we should change the “lists” template so that the html that’s rendered for the todo list links has the correct href attribute – so that when the user hovers over the link they see the new friendly url.
<a class="list-name {{name_class}}" href="/{{name}}"> {{name}} </a>
See the source code on github.
Did you created the todos app on your own… because i tried replicating the todo functionality, it’s failed. meteor is not loading the css and other client side javascript functionality…
Try creating a todo on your own and let me know…
Hi Murali,
No I only used the todo sample app, i.e:
meteor create –example todos
Matt.
Thanks so much for this. I have finally got my head around routers in meteor thanks to this walk through. No posts on Meteor in ages though – have you given up on it?
Yeah, I started playing with other stuff in my spare time, namely RavenDB and Windows Azure. It seems like Meteor is only good for Single Page Apps and I don’t have much need to write those right now. I’ll probably start playing with Meteor again when once I finish my RavenDB spare time project.
I can’t seem to get the hang of this. If I sent you a GitHub link to my copy of the Todos example, would you be able to quickly change it and send them back? Thanks! @maxbailey on Twitter