Skip to content

Instantly share code, notes, and snippets.

@wycats
Created May 19, 2012 02:22
Show Gist options
  • Star 60 You must be signed in to star a gist
  • Fork 11 You must be signed in to fork a gist
  • Save wycats/2728699 to your computer and use it in GitHub Desktop.
Save wycats/2728699 to your computer and use it in GitHub Desktop.

Recently, we've been working on extracting Ember conventions from applications we're working on into the framework. Our goal is to make it clearer how the parts of an Ember application work together, and how to organize and bootstrap your objects.

Routing

Routing is an important part of web applications. It allows your users to share the URL they see in their browser, and have the same things appear when their friends click on the link.

The Ember.js ecosystem has several great solutions for routing. But, since it is such an important part of most web applications, we've decided to build it right into the framework.

If you have already modeled your application state using Ember.StateManager, there are a few changes you'll need to make to enable routing. Once you've made those changes, you'll notice the browser's address bar spring to life as you start using your app—just by moving between states, Ember.js will update the URL automatically.

In order to make state managers more robust, we are now enforcing some new rules about how they are architected. Most existing state managers will probably not run afoul of these new rules, but if yours does, making changes should be relatively straightforward.

Here are the new rules:

  1. You must have a root state named root.
  2. Once your state manager has left its initial state, it must always reside in a leaf state (a state that has no child states). Interior states (states that have children) may never be the current state.

One other small change: For the state manager that you would like to control the URL, you should subclass Ember.Router instead of Ember.StateManager. This provides a hint to Ember.js about which state manager is the primary manager that should update the URL when its current state changes.

You tell Ember.js what to put in the URL by adding a route property to your states. When that state is entered, the string specified by route will be appended to the URL:

App.Router = Ember.Router.extend({
  root: Ember.State.extend({
    index: Ember.State.extend({
      route: '/',
      redirectsTo: 'calendar.index'
    }),

    calendar: Ember.State.extend({
      route: '/calendar',
      
      index: Ember.State.extend({
        route: '/'
      }),
      
      preferences: Ember.State.extend({
        route: '/preferences'
      })
    }),
    
    mail: Ember.State.extend({
      route: '/mail',
      
      index: Ember.State.extend({
        route: '/'
      }),
      
      preferences: Ember.State.extend({
        route: '/preferences'
      })
    })
  })
});

// If the user navigates to the page with the URL
// www.myapp.com/, you will start in the root.calendar.index state.
// The redirection to the calendar.index state will cause the URL
// to be updated to www.myapp.com/calendar

router.transitionTo('preferences');

// URL => www.myapp.com/calendar/preferences

router.transitionTo('mail.preferences');

// URL => www.myapp.com/mail/preferences

router.transitionTo('index');

// URL => www.myapp.com/mail

Hitting the back button will "just work," because the router has updated the URL as your app navigates through the states, and changing the URL via the back/forward buttons will cause the router to automatically navigate to the matching state.

Dynamic Segments

In real-life routing setups, URLs are often composed of static segments (like mail or posts) and dynamic segments, which represent a particular model. In other words, your URL captures not just the current state, but also the context of the current state.

For example, if you have a blog app that displays posts, you need to know not just that you are displaying a post, but which post in particular.

This information is captured in serialized form in the URL. For example a URL like /posts/1 typically means "show the Post object with an ID of 1."

Dynamic segments automatically extract this information from the URL for you, then inform your current state about what it should be displaying by sending the setupControllers event. This is your opportunity to setup any controllers used by your views to display this object.

For example, imagine the following router for displaying posts:

var router = Ember.Router.create({
  root: Ember.State.extend({
    index: Ember.State.extend({
      route: '/'
    }),
    posts: Ember.State.extend({
      route: '/posts',

      show: Ember.State.extend({
        route: '/:post_id',
        modelType: 'App.Post'

        setupControllers: function(router, post) {
          var postController = router.get('postController');
          postController.set('content', post);
        }
      })
    })
  })
});

// If the user navigates to /, he will start in the
// root.index state.

router.transitionTo('posts.show', post)

// URL => www.myapp.com/posts/1

If the user navigates to www.myapp.com/posts/1, Ember will transition to the root.posts.show state. It will also call App.Post.find(1), and pass that Post object into the setupControllers event.

If the application transitions to root.posts.show, passing in a Post object whose id is 1, Ember will get the id of the Post object, and generate the URL /posts/1. This means that once the application enters a state, the URL it generates will automatically work for future navigations.

Bye Bye Singletons

If you're not sure how to organize and connect your objects, one easy solution is to create global singletons that you stick on your application's namespace and use throughout your application as needed.

This approach has two major problems:

  1. A tremendous amount of application logic is bound up in these interconnected singletons, making testing complicated. When an application uses singletons, high-level integration tests are often the only way to test reliably.
  2. The connections between your objects are defined in an ad-hoc, implicit way, based upon your usage of the singletons. In addition to being poor architecture, it makes it very hard for the framework to provide guidance for what objects belong where.

Now that most Ember.js applications will use a router to encapsulate their behavior, we can use the router as a coordination layer, and apply some conventions to how objects are exposed to it.

Part of this is a new mechanism for initializing your controllers. Previously, you would create a global singleton and bind your views to it:

App.postsController = Ember.ArrayController.create({
  // … your controller here
});
{{#each App.postsController}}
  <h1>{{title}}</h1>
  <p>{{body}}</p>
{{/each}}

Now, your app can automatically instantiate your controller classes and make the instances available on the router.

App.PostsController = Ember.ArrayController.extend({
  // ... your controller here ...
});

App.router = App.Router.create()
App.initialize(App.router);

// App.router now has the controller instances on it:
//   App.router.postsController (instance of App.PostsController)

If the controller is no longer available in the global scope, you may be wondering how you access it from your views. Instead of doing ad-hoc work on the global context, you will now set things up as needed as your router moves through different states.

Let's look at an example that connects views to their controllers using the router.

The first thing we will need is an application controller. The application controller is responsible for the high-level state of your app's UI.

App.ApplicationController = Ember.Controller.extend({
  view: null
});

Note that we're defining a subclass via extend() rather than creating an instance via create(). Again, the application's initialize method will instantiate it and make it available to the router for us as applicationController.

Now that we've got our controller, we'll need to create a root Ember.ContainerView. This view manages the main view. As your application moves through the router, the router's states will control what the root view displays.

For example, when entering the posts.index state, it will set the application controller's view property to an instance of App.PostIndexView. If you transition to the posts.show state, it will replace the application controller's view property with an instance of App.PostView.

Here's what the code for the router looks like. Note the setup of the main ContainerView inside the root state's setupControllers event:

var router = Ember.Router.create({
  root: Ember.State.extend({
    setupControllers: function(router) {
      var applicationController = router.get('applicationController'),
          rootView;
          
      rootView = Ember.ContainerView.create({
        controller: applicationController,
        currentViewBinding: 'controller.view'
      });
      
      rootView.appendTo('#content');
    },

    posts: Ember.State.extend({
      route: '/posts',

      setupControllers: function(router) {
        var postsController = router.get('postsController');
        postsController.set('content', Post.find());
      },

      index: Ember.State.extend({
        route: '/',

        setupControllers: function(router) {
          var postsController = router.get('postsController'),
              appController = router.get('applicationController');

          // make an App.PostIndexView the current "main" view
          appController.set('view', App.PostIndexView.create({
            controller: postsController
          }));
        }
      }),

      show: Ember.State.extend({
        route: '/:post_id',
        modelType: 'App.Post'

        setupControllers: function(router, post) {
          var postController = router.get('postController'),
              appController = router.get('appController');

          // Make an App.PostView the current "main" view
          appController.set('view', App.PostView.create({
            controller: postController
          }));
        }
      })
    })  
  })
});

Lastly, we'll make one small change to the Handlebars template. Instead of relying on the global scope, we'll just point to the controller that was set on the App.PostIndexView instance in the setupControllers event.

{{#each controller}}
  <h1>{{title}}</h1>
  <p>{{body}}</p>
{{/each}}

Nested Views

This is a great solution for when you only have one view to show at a time, but what if you want to display multiple views at once? For example, let's imagine that we want to toggle between displaying trackbacks and comments for a given blog post.

This is what the states might look like:

show: Ember.State.extend({
  router: '/:post_id',

  setupControllers: function(router, post) {
    var applicationController = router.get('applicationController'),
        postController = router.get('postController');

    postController.set('content', post);

    applicationController.set('view', App.PostView.create({
      controller: postController
    }));
  },

  comments: Ember.State.extend(),

  trackbacks: Ember.State.extend()
})

What happens if we enter the comments state? We obviously cannot just change the view property of our applicationController--that would replace the post body with the comments. We just want to change a subset of the view.

To understand what to do, let's first look at the template for App.PostView. It's similar to the template we used above, but we removed the {{each}} helper and added an Ember.ContainerView:

<h1>{{controller.content.title}}</h1>
<p>{{controller.content.body}}</p>
{{view Ember.ContainerView currentViewBinding="controller.view"}}

Now we have a ContainerView that will display the contents of its controller's view property. Remember that controller here is not the applicationController--it's the postController that we set up in the setupControllers event.

The next step is to implement the setupControllers event for the comments and trackbacks state. Remember that the role of a controller is to store the information needed for a view to render a model. In this case, we provide the instance of the model itself. But we also give the controller access to a new view instance (either App.CommentsView or App.TrackbacksView, depending on the state), because it is part of the information necessary for the PostView to render the model correctly.

show: Ember.State.extend({
  router: '/:post_id',

  setupControllers: function(router, post) {
    var applicationController = router.get('applicationController'),
        postController = router.get('postController');

    postController.set('content', post);

    applicationController.set('view', App.PostView.create({
      controller: postController
    }));
  },

  comments: Ember.State.extend({
    setupControllers: function(router) {
      var postController = router.get('postController');

      postController.set('view', App.CommentsView.create());
    }
  }),

  trackbacks: Ember.State.extend({
    setupControllers: function(router) {
      var postController = router.get('postController');

      postController.set('view', App.TrackbacksView.create());
    })
  }
})

You can use this approach to create nested views that map onto your router's hierarchy.

The parent template specifies where to insert its child views (using a ContainerView), and the router specifies which particular view should be inserted.

@krisselden
Copy link

It made it into the dev channel and yes it looks like it.

@polyclick
Copy link

Are there any working routing examples for one of the latest emberjs builds? All jsfddles in this topic are broken.

@jobe451
Copy link

jobe451 commented May 17, 2013

It would be nice to know an example how to put queries to the URL, something like
/cars/model=Prius&yearStart=2006&yearEnd=2008

I am currently experimenting with RC3 and ember-data, I got such a case half way working, however I fail when I try to open such a route by "transitionToRoute". I got such a case working by updating manually the URL. The working part I described here: http://stackoverflow.com/questions/16506634/ember-route-with-dynamic-filter-search-criterias (accepted answer)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment