Something I have noticed recently with Ember Router is it only allows navigating to leaf routes — routes without child routes.
Now unless I'm doing things incorrectly then this seems like a bug/mistake in design.
Let's take for example something like this:
I have a collection of projects, each project has many collaborators, with this I want to build a UI with a 3 column layout (something like your standard desktop email client) where on the left I have a list of projects, when clicking on a project the middle column shows a list of collaborators, and clicking on a collaborator loads its details into the righthand column.
Now with the routing I want to navigate to /projects/1 when clicking on a project and onto /projects/1/collaborators/23 when clicking on a collaborator.
Here is a router illustrating the first part of nested route:
App.reopen(
Router: Ember.Router.extend(
enableLogging: true
location: 'hash'
root: Ember.Route.extend(
index: Ember.Route.extend(
route: '/'
redirectsTo: 'projects'
)
projects: Ember.Route.extend(
# This route is not routable because it is not a leaf route.
route: '/projects'
connectOutlets: (router) ->
# List projects in left column
router.get('applicationController').connectOutlet('projects', App.projects)
show: Ember.Route.extend(
# This route is routable because it is a leaf route.
route: '/:project_id'
connectOutlets: (router, project) ->
# Render the project into the second column, which actually renders
# a list of collaborators.
router.get('projectsController').connectOutlet('project', project)
)
)
)
)
)
As you'll see Ember doesn't call updateRoute (set the URL) until transitioning to root.projects.show because of this line https://github.com/emberjs/ember.js/blob/master/packages/ember-routing/lib/routable.js#L81
Has anyone else done anything like this? Is there a better way to design this?
The best way I've found to do this is to have a root.projects.index state with a route of "/" and nothing else. This way every page has it's own specific state.
projects: Ember.Route.extend(
# This route is not routable because it is not a leaf route.
route: '/projects'
connectOutlets: (router) ->
# List projects in left column
router.get('applicationController').connectOutlet('projects', App.projects)
index: Ember.Route.extend(
route: "/"
)
show: Ember.Route.extend(
# This route is routable because it is a leaf route.
route: '/:project_id'
connectOutlets: (router, project) ->
# Render the project into the second column, which actually renders
# a list of collaborators.
router.get('projectsController').connectOutlet('project', project)
)
)
N.B. That being said I'm doing a similar thing with a 3 column layout, and am matching the middle and right column to the routes as above and adding the left column in a shared layout to each of the possible middle views.
I think this issue is resolved. I am using ember routes without an index and do not face any leaf state issues. I was browsing why we need an index at all and I landed up here. Is there any other reason for using the index state too?
Related
I'm looking for a way to hook into the router as it transitions to any given leaf resource during app init. In other words, I want to run a little init routine every single time an app is loaded, no matter what the "incoming" url state is.
Given a router such as this:
App.Router.map ->
#resource "donkeys"
#resource "camels"
When the app is loaded (for the first time) with a dangling url such as this:
localhost:9000/#/donkeys
-- or --
localhost:9000/#/camels
I want the same router-level code to be called.
My first guess was to try something like this (this doesn't work):
App.IndexRoute = Ember.Route.extend
activate: ->
App.callTheBeastsOfBurden()
... but it turns out that IndexRoute is not being traversed. I only see the log message Transitioned into 'donkeys'
In ye olde days, there was this "root route" concept that you could hook in to. If I recall, it looked something like this (this is old and "wrong"):
App.Router = Em.Router.extend
root: Em.Route.extend
connectOutlets: ->
App.callTheBeastsOfBurden()
So what's the Router v2 approved method of accomplishing the same?
App.ApplicationRoute = Ember.Route.extend
activate: ->
App.callTheBeastsOfBurden()
I have the following ember router definition:
WZ.Router = Em.Router.extend
enableLogging: true
location: 'hash'
showHome: Ember.Route.transitionTo 'root.index'
root: Em.Route.extend
initialState: 'index'
connectOutlets: (router, event) ->
unless router.get 'initialized'
router.get('applicationController').connectOutlet 'nav', 'navbar'
router.get('homeController').connectOutlet 'bottombar', 'bottombar'
router.set 'initialized', true
index: Em.Route.extend
route: '/'
connectOutlets: (router, event) ->
router.get('applicationController').connectOutlet 'home'
I am using connectOutlets of the root route because I want the navigation outlets to be connected no matter which url the user enters the application.
The problem is that as soon as the router is created, the root connectOutlets fires and this is before the router has had the controllers injected via runInjections.
Everything works if I connect these outlets in a leaf route but that is not what I am after.
If I cannot use the root connectOutlets, how can I best ensure that the navigation outlets are connected no matter which url or route the user enters the app on?
Should we also disallowing connectOutlets to be overriden on a leaf route as it is fairly useless if there are no controllers etc. to connect?
EDIT: I got round this problem by using Ember.run.next:
WZ.Router = Em.Router.extend
enableLogging: true
location: 'hash'
showHome: Ember.Route.transitionTo 'root.index'
root: Em.Route.extend
connectOutlets: (router, event) ->
Ember.run.next #, ->
unless router.get 'initialized'
router.get('applicationController').connectOutlet 'nav', 'navbar'
router.get('homeController').connectOutlet 'bottombar', 'bottombar'
router.set 'initialized', true
index: Em.Route.extend
route: '/'
But this still seems less than ideal. Is this a hole in the Em logic or by design?
I generally consider the root state to be special, so I dont use connectOutlets for this state. In practice this means that the root state (which responds to URL '/'), redirects to a "home" state, inside which I would put your connectOutlets.
The upside of this is that you don't run in to the controllers-not-injected yet problem, but it also means that your "default" URL becomes something like yoursite.com/#/home if not using the History API, and yoursite.com/home if you are.
Not really ideal in either case, but at least you aren't shoehorning Ember.run.next into your connectOutlets, which seems like bad form which might bring up a slew of other issues moving forward. I suppose you wouldn't, but you are now expecting that your controllers will be injected between now and when the next run loop fires... That may or may not be the case...
I have 3 views in my router.
The index view picks viewtwo as the default.
The problem is when I go to viewone or viewthree it goes through the index route! and loads the viewto before then rerouting to whichever I clicked viewone or viewthree and in my setup that causes some glitch as each view does something to the HTML that it then has to undo before leaving but something is not syncing too well (I tested it has something to do with how fast things load and javascript is applied onto it because my glitch only occurs if I click back and forth really fast).
What is the opposite of didInsertElement right before it gets destroyed?
Also why is the viewto getting loaded from the index route?
index: Ember.Route.extend({
route: '/',
redirectsTo: 'viewtwo'
}),
viewone: Ember.Route.extend({
route: '/viewone',
connectOutlets: function( router ) {
....
viewtwo: Ember.Route.extend({
route: '/viewtwo',
connectOutlets: function( router ) {
....
viewthree: Ember.Route.extend({
route: '/viewthree/:item_id',
connectOutlets: function( router, item ) {
....
What is the opposite of didInsertElement right before it gets destroyed?
willDestroyElement. See this page: http://emberjs.com/api/classes/Ember.View.html#event_willDestroyElement
Also why is the viewto getting loaded from the index route?
The reason index loads viewto is because of the redirect, but I think you already knew that. Not sure what you are asking here.
As far as all your routes going through index, you might want to post a fiddle that emulates this problem. /viewthree should not enter index from what you've posted. If you post a fiddle that can reproduce the problem it will help people provide better answers.
Having two routes (comments, trackbacks) nested within post I try to access content of commentsController from trackbacksController after entering the App directly through /posts/1/trackbacks. Unfortunately it seems like the controller is not fully initialized and the content doesn't get loaded.
This is how the Router looks like:
Router = Ember.Router.create
root: Ember.Route.extend
index: Ember.Route.extend
route: '/'
post: Ember.Route.extend
route: '/posts/:post_id'
index: Ember.Route.extend
route: '/'
redirectsTo: 'comments'
comments: Ember.Route.extend
route: '/comments'
connectOutlets: (router) ->
controller = router.get('postController')
controller.connectOutlet 'comments', controller.get('comments')
trackbacks: Ember.Route.extend
route: '/trackbacks'
connectOutlets: (router) ->
controller = router.get('postController')
controller.connectOutlet 'trackbacks', controller.get('trackbacks')
And here is the TrackbacksController:
App.TrackbacksController = App.ArrayController.extend
init: ->
console.log App.router.get('attributesController.content') # : []
#_super()
Is there a best practice to initialize router.commentsController manually to get it's content from trackbacksController? Is there anything wrong concerning my approach?
You can access all controllers from a controller, using its controllers property.
For example, from trackbacksController, you can use this.get('controllers.commentsController')
EDIT: I realize I did'nt get your problem. I think there is some dependencies between the two controllers, and from my point of view, you can't rely on the application initialization order. So in the init method, you will not be able to access other controllers.
But I think you can put an observer in the trackbacksController, which observes controllers.commentsController.content or controller.commentsController.#each, so when the commentsController is populated, you will be notified.
I've been trying to debug this for a while now, and I may have figured out what the problem is, let me explain what is happening by use of a fiddle
Navigate to http://fiddle.jshell.net/ivanvanderbyl/mfqEB/show/#/projects/1, you'll notice the dynamic segment in the url is replaced with null — it should stay as 1
From what I can gather, after routing once, Ember Router then makes a call to update the URL, replacing the dynamic segments with the values it gets from the respective instantiated objects, in this case App.Project.
The problem is that App.Project is not loaded at this point so id is null
Now whether this is a bug or an implementation failure on my part, has anyone else seen this?
Moving connectOutlets from projects.index directly into projects seems to do the trick. Don't ask me why though.
jsFiddle
Router: Ember.Router.extend(
location: 'hash'
enableLogging: true
root: Ember.Route.extend(
listProjects: Ember.Route.transitionTo("projects.index")
showProject: Ember.Route.transitionTo("projects.show")
index: Ember.Route.extend(
route: "/"
redirectsTo: 'projects.index'
)
projects: Ember.Route.extend(
route: '/projects'
connectOutlets: ((router) ->
router.get('applicationController').connectOutlet('projects', App.Project.find())
)
index: Ember.Route.extend(
route: '/'
)
add: Ember.Route.extend(
route: '/new'
)
show: Ember.Route.extend(
route: '/:project_id'
connectOutlets: (router, project) ->
console.log project.get('name')
# router.get('applicationController').connectOutlet('project', project)
)
)
)
)
So after much investigation the correct solution to this is to simply not specify the primary key attribute on the model, in this case I should not have been specifying the ID attribute on the Project model. Ember Data handles this internally.
Thanks to Peter Wagenet for pointing this out on Github.