Nested routes rendering into same template/outlet breaks on browser back button click

Russell Smith picture Russell Smith · Mar 1, 2013 · Viewed 7.2k times · Source

I have a nested route hierarchy that I need for my application to track user model selections. I'm trying to use a main application template and have each route render into a single outlet on that template. This works as I traverse down the route hierarchy from parent to child.

However once you click the browser back button to go back up the route hierarchy the parent route renderTemplate hook doesn't fire. This results in the child being unhooked from the outlet and nothing rendered back into it.

Here's an example:

App = Ember.Application.create({});

App.Router.map(function(){
    this.resource("animals", function(){
        this.resource("pets", function(){
              this.route('new')
        })
    })
});
App.PetsView = Ember.View.extend({
    templateName : 'wild/pets'
});
App.AnimalsRoute = Ember.Route.extend({
     renderTemplate: function() {
    this.render({
    into: "application",
    outlet : "A"
})
     }});
App.PetsRoute = Ember.Route.extend({
     renderTemplate: function() {
this.render({
    into: "application",
    outlet : "A"
})}});

App.PetsNewRoute = Ember.Route.extend({
     renderTemplate: function() {
this.render({
    into: "application",
    outlet : "A"
})}});

With templates:

<script type="text/x-handlebars" data-template-name="application">
    <h1>{{#linkTo "animals"}}Hello from Ember.js</h1>{{/linkTo}}
      {{outlet A}}
</script>

<script type="text/x-handlebars" data-template-name="animals">
    {{#linkTo "pets"}}This is animals list{{/linkTo}}
</script>
<script type="text/x-handlebars" data-template-name="wild/pets">
    {{#linkTo "pets.new"}}This is pets list{{/linkTo}}
</script>

<script type="text/x-handlebars" data-template-name="pets/new">
    This is pet creation
</script>

And here's a jsfiddle with this code. Click the links to traverse the routes, then click the browser back button and the application template is rendered with nothing hooked into its outlet.

http://jsfiddle.net/Wq6Df/3/

Is there any way to force a re-render, or am I going about this the wrong way?

Answer

Nick Ragaz picture Nick Ragaz · Mar 11, 2013

You're going about it the wrong way.

Ember depends on a hierarchy of nested outlets that match the route hierarchy. So every time you click on a link that takes you to a child route, the child route should render into an outlet within its parent's template. If you always render into the same template and outlet, then Ember won't be able to update the outlets properly when you move back up the route hierarchy. (I hope that makes sense.)

To avoid this problem, a good rule of thumb is to only use the into option for rendering templates that you're managing outside of the route hierarchy. For example, I use it for rendering modal views that don't have a URL and which I tear down manually. Within the view hierarchy, you can almost always avoid using into. For example, if you need to render more than one template with a separate controller, you can use the {{render}} helper within your template instead of calling render in your route.

In this case, the easiest solution is probably to match your route nesting to your template nesting. Your animals/index route and pets are really siblings, not parent-child, and same for pets/list and your pets/new. In fact, this is the default but somewhat hidden behaviour: you should really be using pets/index to render the list instead of the parent pets route.

App = Ember.Application.create({});

App.Router.map(function(){
    this.resource("animals", function(){
        // this.route("index"); at path /animals is implicit
        this.resource("pets", function(){
              // this.route("index"); at path /animals/pets is implicit
              this.route("new")
        })
    })
});

// You don't really need any of these route definitions now;
// I'm including them for clarity
App.AnimalsRoute = Ember.Route.extend();
App.AnimalsIndexRoute = Ember.Route.extend();

App.PetsRoute = Ember.Route.extend();
App.PetsIndexRoute = Ember.Route.extend();
App.PetsNewRoute = Ember.Route.extend();

// Not sure why you need to use a custom template name here, 
// but it should work fine
App.PetsView = Ember.View.extend({
    templateName : 'wild/pets'
});

With templates:

<!-- animals gets rendered into this outlet -->
<script type="text/x-handlebars" data-template-name="application">
    <h1>{{#linkTo "animals"}}Hello from Ember.js</h1>{{/linkTo}}
    {{outlet}}
</script>

<!-- animals/index and pets get rendered into this outlet -->
<script type="text/x-handlebars" data-template-name="animals">
    {{outlet}}
</script>

<!-- no outlet here because animals/index has no child routes -->
<script type="text/x-handlebars" data-template-name="animals/index">
    {{#linkTo "pets"}}This is animals list{{/linkTo}}
</script>

<!-- pets/index and pets/new get rendered into this outlet -->
<script type="text/x-handlebars" data-template-name="wild/pets">
    {{outlet}}
</script>

<script type="text/x-handlebars" data-template-name="pets/index">
    {{#linkTo "pets.new"}}This is pets list{{/linkTo}}
</script>

<script type="text/x-handlebars" data-template-name="pets/new">
    This is pet creation
</script>