Is there a reason we can not have nested routes?

I spoke with Ryan Florence a bit yesterday about this topic so I know I’m not crazy in thinking that it should be possible. I have an app that has 3 layers of nested templates:

App.Router.map(function() {
  this.resource('docs', function() {
    this.resource('basics', function() {
      this.route('index');
      this.route('building_an_lti_app');
      this.route('post_parameters');
      this.route('other_resources');
    });
  });

The pages these are linking to are all static content pages. The docs template provides a nav bar at the top and the basics template offers a nav list on the side and is embedded in the docs template.

After talking with Ryan for a bit, I understand that the point of a resource is that it represents data. However, in this example, I am not dealing with data.

Is there a reason we don’t have the ability to have nested routes? The concern I have here is my links now have to reference the resource closest to the route, which removes the docs completely from the link.

For example:

{{#linkTo 'docs.basics.post_parameters'}}Click Me{{/linkTo}}

does not work when

{{#linkTo 'basics.post_parameters'}}Click Me{{/linkTo}}

does.

I know this topic has been discussed prior, and there are workarounds for this… however is there a reason we don’t support nested routes? And if not, is this something we can add in the future?

Thanks!

The real issue is when you have a second set of nested routes that have the same “resource” name, even though its a not resource, its just a view hierarchy you want.

this.resource('docs', function() {
  this.resource('intro', function() {
    this.route('a');
  });
});

this.resource('tutorials', function() {
  this.resource('intro', function() {
    this.route('a');
  });
});

Since resource resets the namespace, you can’t have an intro template for both docs and tutorials.

It would be nice to have nested routes so the namespaces don’t get reset, then you’d have ‘docs/intro.hbs’ and ‘tutorials/intro.hbs’.

Or, provide some configuration options to specify which objects to map too and break convention.

There’s been some talk about possibly making this.route() nestable, without flattening out the naming conventions. Seems like this in conjunction with making it possible to configure the expected generated class names would be a big win…

4 Likes

@machty from watching your presentation on the router facelift at the Boston meetup, this question was brought up and you distinguished between using nested routes vs. “unroutable substates” (i.e. modals, tabs, etc.) Is it the goal of Ember to make these substates routable? And if so would nested routes be the approach to tackle this problem?

I agree that it would be nice to be able to nest route calls. For now people can achieve the same by prefixing similar named routes with the parent route’s name. E.g. docs.intro and tutorials.intro:

this.resource('docs', function() {
  this.resource('docs.intro', function() {
    this.route('a');
  });
});

this.resource('tutorials', function() {
  this.resource('tutorials.intro', function() {
    this.route('a');
  });
});

This works as expected. The name of the controller for tutorials.intro.a will be App.TutorialsIntroAController, and its template will be tutorials/intro/a.

4 Likes

Nice, I didn’t know about that trick.

@ianstarz Nestable this.route() would mainly be used to append to the existing namespace, rather than resetting it the way this.resource() does.

The syntax for non-routeable substates still needs to be discussed, as well as its implementation, but @lukemelia and I tossed around some possible ideas, such as

this.route('post', { states: ['saving', 'otherstate'] });  

// which might be shorthand for
this.route('post', function() {
  this.state('saving');
  this.state('otherstate');
});

(Luke, apologies if I’ve butchered this)

But I think the idea is that you’ll have a /post URL which corresponds to the default state of the post route, and you can still do things like transitionTo('post'), but when, say, the user clicks the save button, you can transitionTo('post.saving') without changing any URLs, and the PostSavingRoute can handle events fired at that time, which you can use to, say, prevent transitions, put up loading spinners, etc., until the post has finished saving.

This could also be used for modals too. Keep in mind that launching a modal from a Route is something you can do today, it’s just that if you have modals with deeply nested outlets and so on, you can’t reuse the lovely router DSL to sketch out your modal, connect all the expected controllers, inject views into outlets, etc… So adding nestable this.route and this.state would get us closer to that goal.

@machty glad you and @lukemelia have been discussing this. Big +1 from me, as I feel like more than half the time in my applications I’m hand rolling state management for these types of substates. It would be cool if we could get these substates into the route somehow, as well. Obviously, /post/saving would not work with nested routes. But maybe something like /post-saving or /post_saving or some other convention? Going with the opinion that everything should be routable, it would be nice if you could share a page that retained “unroutable” substate.

@ianstarz IMO, everything should have a URL. Except things that shouldn’t have a URL. The transient state of saving a post is an example of something that shouldn’t have its own URL (What would it even mean to visit that URL? Would you save the post? With what data? When would it exit the route?)

It would certainly be useful to have the app behave contextually based on the fact that the post is being saved. My view is that URL-less substates would be an excellent way to do that elegantly.

@lukemelia Haha, I didn’t even realize what I was saying (was thinking about something I’ve been working on instead.) Yeah definitely the transient saving state you would never route to. I see the distinction you guys are drawing now. Definitely think that nested routes and substates are going to save a lot of people a lot of work. As long as you are a believer of routing everything you can, I trust you guys will come up with good solutions. I’ll keep an eye out for your proposals—would love to follow that work (and help out if at all possible.)

I see the point in sub states, but isn’t there easier ways to achieve something like preventing transitions while a record is saving? Using sub states you would need to define this behavior for all routes that need this behavior. Example:

App.Router.map(function() {
  this.resource('post', {path: '/posts/:post_id'}, function() {
    this.state('saving');
  });
  this.resource('category', {path: '/category/:category_id'}, function() {
    this.state('saving');
  });
});

App.PostRoute = Ember.Route.extend({
  events: {
    save: function() {
      var self = this;
      this.transitionTo('post.saving');
      this.get('model').commit().then(function() {
        self.transitionTo('post');
      });
    }
});

App.PostSavingRoute = Ember.Route.extend({
  events: {
    willTransition: function() {
      //Do some crazy logic to prompt the user if he wants to discard changes
    }
  }
});

App.CategoryRoute = Ember.Route.extend({
  events: {
    save: function() {
      var self = this;
      this.transitionTo('post.saving');
      this.get('model').commit().then(function() {
        self.transitionTo('post');
      });
    }
});

App.CategorySavingRoute = Ember.Route.extend({
  events: {
    willTransition: function() {
      //Do some crazy logic to prompt the user if he wants to discard changes
    }
  }
});

Notice how the logic in App.PostSavingRoute and App.CategorySavingRoute is the exact same.

You might as well define an App.SaveAwareRoute that your post and category routes extend from:

App.Router.map(function() {
  this.resource('post', {path: '/posts/:post_id'});
  this.resource('category', {path: '/category/:category_id'});
});

App.SaveAwareRoute = Ember.Route.extend({
  events: {
    willTransition: function() {
      if (!this.controller.get('isSaving')) {
        //Bubble transition if we're not saving
        return true;
      }
      //Do some crazy logic to prompt the user if he wants to discard changes
    }
  }
});

App.PostRoute = App.SaveAwareRoute.extend({
  events: {
    save: function() {
      var self = this;
      this.set('isSaving', true);
      this.get('model').commit().then(function() {
        this.set('isSaving', false);
      });
    }
  }
});

App.CategoryRoute = App.SaveAwareRoute.extend({
  events: {
    save: function() {
      var self = this;
      this.set('isSaving', true);
      this.get('model').commit().then(function() {
        this.set('isSaving', false);
      });
    }
  }
});

This way the “prompt user before transitioning if saving” logic is centralized in one place. And your App.Router.map code looks more lean.

In which other cases would you use sub states?

This is a start that will lead to this.state, would appreciate your comments/ideas: https://github.com/emberjs/ember.js/pull/3069

@machty This is definitely a good addition. I don’t see any reason why it shouldn’t be allowed to nest this.route calls, and newcomers often stumble over this issue.

Does this mean that the only difference between this.resource and this.route will be that the former resets the path name?

@seilund correct. It’s also paving the way to route-less substates, which I’m working on now.

By adding a this.state() method or how?

Yeah that’s what I had in mind, at least at first.

Excuse my naiveté, but does this approach have any unanticipated consequences when using Ember Data? Just curious. Have been trying to grok the best way to do deeply nested resources.

Not that I know of. But I haven’t used it extensively. As long as your “object routes” have /:model_id segments in them, single models will automatically be loaded no matter that name of the route.

Ryan, thanks for this post. I have been struggling quite a bit trying to understand why my nested routes weren’t working as expected. But your comment “resource resets the namespace” now makes a lot of sense. It would be nice if this was explicitly documented in the user guide. Perhaps an updated screen cast talking about this issue would help a lot of new users trying to get their heads around nested routing. Your screencast at http://ember101.com/videos/007-nested-routes is quite good but the nuance in this thread really helps clarify the issue.

I also had the requirement to have simple nested routes in the path. For example think about an administration area of a fairly complex area and you just want to separate aspects by corresponding routes like

/settings/application/i18n
/settings/application/currencies
/settings/users/defaults
/settings/users/authentication
...

Having repeatedly problems to model such cases brought to to the point that I only use resource based routes because I don’t need to think about how and whether I can achieve what I want.