Meteor todos sample – friendly URLs

This is the fourth in a x part series about Meteor for beginners.

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,

  1. Todo list link is clicked in the left pane, calls TodosRouter.setList(list_id).
  2. TodosRouter.setList navigates to ~/list_id
  3. Router.main sets Session[list_id] variable.
  4. The template helper “todos” detects Session[list_id] has changed so it re-runs.
  5. 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:

  1. TodosRouter.setList(list_id) to TodosRouter.setList(list_name)
  2. TodosRouter.setList should navigate to ~/list_name
  3. 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.

Advertisement

5 thoughts on “Meteor todos sample – friendly URLs

  1. 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…

    • 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.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s