Router's nested resource implementation is extremely limiting


#1

I’ve recently came across a very interesting part of Ember’s router. Our app is very contextual. We have a few core models (resources) that are interacted in different ways depending on how you look at them. Here’s is one example. I’ll express this in routes because it’s easier to understand.

App.Router.map(function() {
    this.resource('companies', function() {
        this.resource('deals', function() {
            this.route('new');
        });
    });
    
    this.resource('contacts', function() {
        this.resource('deals', function() {
            this.route('new');
        });
    });

    this.resources('deals', function() {
       this.route('new');
    });
});

We interact with the Deal model when looking at them through a Company, Contact or simply straight on. Now, through reasons unknown to me, the router is designed to make this difficult to do. The core problem is that the router does not namespace resources. Also there is no way to distinguish how to get to the various new routes. The router creates these routes:

  • CompaniesRoute
  • ContactsRoute
  • DealsRoute
  • DealsNewRoute

There is no mention of a CompaniesDealsNewRoute or a ContactsDealsNewRoute. Actually there is no way to even get to them anyways: there is only deals.new. So how can I create a unique route for these situations? Don’t use resources. Ok, but then I nesting routes is not allowed. This design decision is going to make programming our application extremely difficult. At the moment, I don’t think their is a (decent) work around for this.

Here is the only work around I can think of:

App.Router.map(function() {
    this.resource('companies', function() {
        this.resource('companiesDeals', function() {
            this.route('new');
        });
    });
    
    this.resource('contacts', function() {
        this.resource('contactsDeal', function() {
            this.route('new');
        });
    });

    this.resources('deals', function() {
       this.route('new');
    });
});

This seems very wrong, but it’s the only way to get unique nested resources. At this point, I just want to understand why the decision to disable nesting of resources was made.


#2

Yeah, I think that feels a bit weird, too.

You can undo some of the damage in your routes, and protect your controllers from all this complexity, by using something like this:

App.DealsRoute = Ember.Route.extend({
  // We want to use the DealsController for all our different deals routes.
  controllerFor: function (name, model) {
    if (name.match(/_deals$/) {
      this._super('deals', model);
    } else {
      this._super(name, model);
    }
  }
});

App.CompaniesDealsRoute = App.DealsRoute.extend();
App.ContactsDealsRoute = App.DealsRoute.extend();

I really wish there were a better way to handle this, and I have no idea whether this was a deliberate design decision, or just something the router doesn’t handle yet.


#3

CompaniesDealsNewRoute ? Ouch! Would be nice with proper namespacing IMO. Companies.Deals.NewRoute :slight_smile:

Would be nice if the resources also automatically set up the “real” nested namespaces and reflected this for controllers and views as well IMO. then you could have folder hierarchies that match your namespacing and all is well :slight_smile: Strange why this wasn’t considered!?

It should be possible to have the “string” of previous resources available at each level. The resource could create a new Resource instance, and the function be inserted on this instance as a ResourceBuilder (to build child resources), then the next resource could be nested on the previous resource with a pointer back to the parent resource - composite builder pattern. I sure hope this makes it into router v3+, perhaps at the Ember 1.2 stage :wink:


#4

Any news on this? I’ve been facing this limitation and I can’t not agree on how this was made. It’s frustrating and foten ember is advertised as a javascript framework for large apps, but this proves wrong when you start fighting against the framework when you app grows.


#5

How about something lieke this:

App.Router.map(function() {
    this.resource('companiesDeals', { path: 'companies/deals' }, function() {
      this.route('new');
    });

    this.resource('contactsDeal', { path: 'contacts/deals' }, function() {
      this.route('new');
    });

    this.resources('deals', function() {
       this.route('new');
    });
});

One base controller named App.DealsController, while App.CompaniesDealsController and App.ContactsDealsController extends from App.DealsController.


#6

I agree that if you can write something like this in the router:

App.Router.map(function() {
  this.resource('company', function() {
    this.resource('deals', function() {
      this.route('new');
    });
  });
});

…you’d expect to able to able to write something like this in your templates:

{{#linkTo company.deals.new}}New Deal{{/linkTo}}

…and to define the route like this:

App.CompanyDealsNewRoute = Ember.Route.extend();

Looking into the source, it’s hard to tell if this limitation is by design, or an omission.

In the definition for route we see the parent route’s name explicitly prepended:

if (this.parent && this.parent !== 'application') {
  name = this.parent + "." + name;
}

There is no equivalent in the definition for resource, yet adding those same lines produces the expected behaviour.


#7

(Though breaks a bunch of existing tests)


#8

I think given the current router and this situation, this solution makes the most sense. This gets kind of messy though, because not included here are the routes and paths for companies and contacts.

Coming from the rails world, I’d love to have these resources work similarly where I can have each of these nested resources support multiple dynamic segments, too. Like ‘/company/12/deals/3243’. I have yet to see this brought up on the boards or on StackOverflow. Is there a different way we should be doing this with ember?


#9

@pholloway: Here it is how I’ve done it:

 this.resource("product", { path: 'product' } , function() {
    ...
    this.route("edit", { path: "/:id" });

    this.resource("reviews", { path: ':id/reviews' }, function() {
      ...
      this.route("edit", { path: "/:id" });
      ...
    });
  });

#10

Awesome, thanks so much. There should be an example of this in the Ember Guides.


#11

I agree. It took me some time to figure that out.


#12

Looks like there is a good example, but it’s tucked away in the Template section under Links.


#13

This is indeed really limitating :(. One solution that was given in GitHub was to use dot notation:

this.resource('library', function() {
    this.resource('library.folder', {path: 'folders/:folder_id'});

    this.resource('library.albums', {path: 'albums'}, function() {
      this.resource('library.album', {path: 'albums/:album_id'});
    });

This has the nice side-effect too that templates are organized more logically (so I have pictures.hbs in the sub-folder “library” for instance).

However, it seems there is a problem with this when using needs. If I write needs: “library.albums” it does not work out of the box (I need to explicitly define a LibraryAlbums controller).


#14

I have also struggled with this. It would seem trivial to implement a deeply nested UI with the marketing around ember js. But scratch the surface and to an outsider or newbie it seems a bit complex. I would love to see an explicit guide that shows a deeply nested router example perhaps three or four levels deep. It is not clear to me if this is even possible in the current state of ember.

Below might be a somewhat contrived example but would this even be possible? A hypothetical directory of libraries and the books in each library. The UI would be be tree like where you can drill down inside the nested structure. I can’t seem to find any examples where the resource nesting is more than two deep.

App.Router.map(function() {
    this.resource('libraries', function() {
        this.route('new');

        this.resource('library', {path: ':library_id'}, function() {

            this.resource('books', function() {
                this.route('new');
                this.resource('book', {path: ':book_id'}, function() {

                    this.resource('pages', function() {
                        this.route('new');
                        this.resource('page', {path: ':page_id'}, function() {

                        }); // Page
                    }); // Pages

                }); // Book
            }); // Books

        }); // Library
    }); // Libraries
}); // map

#15

We made this choice originally because chaining names would lead to some very long class names, i.e. App.CompaniesDealsNewController is a pretty long name. In general, I also think super deeply nested routes is a bad idea. It really does get ugly pretty quick.

However, we have had some discussion about this (I think there’s even an open ticket) and I expect we’ll be thinking more about ways to make some of these cases easier.


#16

Ugly is better than impossible. Ember shouldn’t tell me how I should define my urls. That’s my job. Here is the pull request: https://github.com/emberjs/ember.js/pull/3069