public
Last active

  • Download Gist
updates.md
Markdown

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.

Looks great, whereabouts can I find this code? I don't see it in the routing branch.

We merged the routing branch into master. Not everything here is implemented yet, but most is, and it will all be very soon.

Great, I've just done a pull and taking a look.

Do you recommend going down the setupControllers route rather than using ViewStates?

Yes. This approach is more robust. It handles nested views and provides a hook for controller setup.

o_0 Hello Paul :D Didn't expect to see you here.

Just keeping up with the times :)

This is awesome!

This is a beautiful approach, time for a couple of rewrites!

I've had a quick look and I love the approach of having all the controllers in the statemanager or Router as long as they are of the convention App.XXXController and then I can retrieve them router.xxxController or router.get('xxxController').

I think the conventions are really going to push ember to the front of the mv* client side frameworks with great code savings.

The only problem I had was getting the setupControllers event to fire on the root state, I had to explicitly transitionTo root in order for it to fire:

router.transitionTo('root');

Do I need to explicitly state this?

@wycats Starting under the Nested Views heading there are two instances where router should be route.

show: Ember.State.extend({
  router: '/:post_id', // should be `route`

I think the conventions are really going to push ember to the front of the mv* client side frameworks with great code savings.

:+1:

Is this all pseudo-code or is this stuff expected to work in 0.9.8?

It largely works on master (1.0) but is only partially implemented in 0.9.8.

You can do it manually:

$('#order_entry').on( 'entered.barcode', function(e) {
  e.preventDefault();
  App.router.send('showPost', { id:this.href });
});

or with Handlebars:

<a {{action showPost}}>Click</a>

I think the Handlebars way is the preferred way, but doing it programmatically works as well.

But I will admit to being a bit hazy on the exact details.

This is a welcome site to Ember, but I can see some routers being very big, throw in the setupControllers methods and this can start becoming very difficult to maintain. Is there a way to be able to break the routes out into smaller, more manageable pieces?

Also, in some of your examples you are repeating this pattern:

{
  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
    }));
  }
}

could be simplified to something like:

{
  router: '/:post_id',
  controller: 'postController',
  view: 'App.PostView'
}

the assumption being that post (second argument of setupControllers) would be set to content on the controller. If need be you could then define setupControllers as a means to handle the process differently.

Why not take it one step further and define the concept of Regions: ContainerViews that act as placeholders for leaf Views. You provide the app with a tree of Regions, and a RegionController manages what is displayed in each Region. Although you would need to provide a Region tree structure at init, you could always dynamically add more later.

Entering the show state, you would set for example
{ articleRegion: [App.PostView, App.CommentsView, App.TrackbacksView], bottomRegion: [App.FooterView] }

If you want a certain a view to be displayed at all-times you set it in a parent state, and subclass all the other states from it. The state of the UI is still well-defined for each route, and therefore consistent. I'm using this approach for an app, and it works quite well.

@evilmarty, you could resume it to something like this :

https://gist.github.com/2787714

@wycats can you comment on the complete vision for this?

There are no examples in the specs or gists I can find with > 1 child route of root. My expectation is that if you were at a route like /posts/35 an appropriate hashchange event should move back up the hierarchy and go to a neighbor route like /comments/19 but this does not seem to be the case. Going down the hierarchy seems to work (ie, /posts/35/comments/19 would work). With transitionTo you can move to any route. Is this incompleteness in the current implementation or some misunderstanding on my part?

See for example: https://gist.github.com/41547193a6219710a3e5

One gotcha I came across. Call App.initialize(router) after all the controller definitions otherwise the controller instances are never created. Really obvious in hindsight but can be easily missed in the Rails sprocket environment.

Yes, App.initialize(router) is ment to be called inside application ready method or something equivalent ( $(function(){}) )

I'm trying to play with this new great feature using the current master branch. I got the following error :

TypeError: 'undefined' is not an object (evaluating 'location.getURL')

what's wrong ?

@jrabary - Try adding location: "hash" while defining the router. I don't think any default location object is set as of now.

App.router = Ember.Router.create({
    location: "hash",
    root: Ember.State.extend({})
})

So does this render ghempton/ember-routemanager obsolete?

I wonder if there is any chance of an update? The code seems to have changed quite a lot since this gist with setupControllers now redundant.

@GaruavShetty1016 - I'm having the same issue, so thanks for that lead. Your suggestion gets me a different error, though: "Uncaught TypeError: Object hash has no method 'setURL'". So I'm wondering if I'm referencing the hash properly.

@dagda1 Here's a working jsFiddle example of the latest changes.

@pjmorse - the code for the router has changed a lot since this article was written so I think taking a look at @jbrown's example should clear up a lot of things.
@jbrown - thanks for making the jsfiddle example, you beat me to it :) .
The latest code also has an implementation of the html5 history api. We can use that in place of the hash implementation by using 'history' in place of 'hash' for the value of location. I have not tried it out yet though.

@GauravShetty1016 @jbrown thanks, both of you. Looks like I'd better pull up to ember-latest.

@jbrown's gist doesn't seem to work with 0.9.8.1, and being new to Ember.js, I'm not sure where to go for more information about this.

It works with the version of ember-latest currently at https://github.com/downloads/emberjs/ember.js/ember-latest.js (as in, as of when I posted this comment) but not with the ember.js I built from git a few days after that was uploaded, so it's clear that this is a moving target (or "very unstable API") at the moment. I'm using it because I don't see a better option, but I would recommend either staying with a fixed version or grabbing a commit which works and "freezing" your project to that. Unless you have time to refactor your app every few days.

@pjmorse - this is just for a prototype, so the link you posted will work fine. Thank you.

@jbrown, in your fiddle (http://jsfiddle.net/justinbrown/C7LrM/10/), why are you not using the leaf states that @wycats uses in his example? also, could you default to a substate of profile using redirectsTo: 'photos'? doesn't seem to work if i add it to your fiddle: http://jsfiddle.net/davidpett/8jpv8/

Does this support translated routes for internationalized apps?

E.g. can the route string be somehow set dynamically from locale files? Also it would be cool when using Ember with Rails routing would not have to be specified twice...

Well, I assume in general with Rails one would simply specify very basic routes from within Rails and the rest in Ember? So there wouldn't be much duplication? Wonder if locale files from Rails could be used to lookup route translations. As a more general question: does Ember has any support for I18n already?

Updated the example to work against ember-latest: http://jsfiddle.net/C7LrM/59/

now this works with embers latest http://jsfiddle.net/C7LrM/86/

is there a statement to what we should rely on when developing or should we still use stateManager until release/stable?

What is the expected behavior of Ember.Route.transitionTo ? In this jsfiddle http://jsfiddle.net/TnuEn/17/ when one click on viewProfile, the state remain in root.profile. I expect it to be root.profile.index and then transits directly to root.profile.posts . If I enter the url directly the transition is correct. Is it a bug ? Is it related to the new navigateAway callback ?

Hey, I just updated to the version from the jsfiddle. I've been using the Router, but getting some weird behaviour so I hoped updating might sort it out, unfortunately I'm now getting the following Warning and Errors:

WARNING: Computed properties will soon be cacheable by default. To enable this in your app, set ENV.CP_DEFAULT_CACHEABLE = true. vendor.js:54720
Uncaught TypeError: Object function () { return initMixin(this, arguments); } has no method 'finishPartial' vendor.js:42905
Uncaught Error: Cannot find module "app"

Any clue what might be going here?

@mtadhg what browser + version are you using?

Hey Kselden, I'm on Chrome.

There is a V8 bug that made it into Chrome's dev channel that will seriously mess with Ember.

(obj[key] = fn) === obj instead of (obj[key] = fn) === fn

http://code.google.com/p/v8/issues/detail?id=2226

am I right in saying we need a controller/view pair e.g. HomeController/HomeView for every outlet we want to connect?

@kselden I'm not sure I follow, that seems to affect Chrome Canary?! Are you saying this could be the cause of my problem?

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

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

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)

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.