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()
Related
I'm using Ember 1.0 and Ember-Data 1.0Beta. I'm trying to pass a dynamic segment to a route like so:
#resource 'organization', path: 'organizations/:organization_id', ->
#route 'edit'
Then in my edit route:
Whistlr.OrganizationEditRoute = Ember.Route.extend
model: (params) ->
#store.find('organization', params.organization_id)
Unfortunately, the params hash turns up empty. When I inspect it in the console, it's just a simple {}. In turn, params.organization_id is null. This happens even when the URL looks correct: "/organizations/1/edit`
This closely resembles the setup in the Ember guides. What could I be getting wrong?
The reason for this is the dynamic segment (:organization_id) is part of the organization resource and not the edit resource. This means that only OrganizationRoute will have access to the params.organization_id.
However, if you need the model in your OrganizationEditRoute you can use modelFor to access it.
Whistlr.OrganizationEditRoute = Ember.Route.extend
model: (params) ->
#modelFor('organization')
This lets the OrganizationRoute load the model from it's dynamic segment and then the OrganizationEditRoute can just simply access that model like so.
The router of my application looks like this (it's CoffeeScript):
App.Router.map () ->
#resource 'conversations', { path: '/' } ->
#resource 'conversation', { path: ':conversation_id' }
#route 'new'
So, in my app, I have paths like /new, /1, /2, etc.
I would like to detect a transition from /1 to /2 to make some initializations in my view (basically, put the focus on a textarea field). Unfortunately, as /1 and /2 use the same route, it seems nearly impossible to detect that transition.
I tried by using didInsertElement in the view (as described here) or by observing currentPath in the controller (as described here). It works fine if I go from /new to /1 (different routes) but not if I go from /1 to /2.
I found this gist suggesting to use the StateManager but it seems outdated (and I'm not sure it's really what I need).
What do you suggest me to do?
EDIT
It seems that setupController is called every time so I decided to overload it like this:
App.ConversationRoute = Ember.Route.extend {
setupController: (controller, model) ->
controller.set 'model', model
# do something here?
}
And I want the init method in my view to be called:
App.ConversationView = Ember.View.extend {
init: ->
#$('form textarea').focus()
}
But I still can't figure out how to make these two things work together (it's a problem because I read that the controller is not supposed to know about the view).
Thank you for your help!
Use the didInsertElement view hook and an observer.
App.ConversationView = Ember.View.extend
didInsertElement: ->
#focusOnTextarea()
modelChanged: (->
#focusOnTextarea()
).observes('controller.model')
focusOnTextarea: ->
#$('form textarea').focus()
In the case of going from /1 to /2, the route and view are not changing. Ember does the least amount of work possible. There's no need to re-render the view, so it doesn't. But this tripped me up too, and I think it's a big gotcha.
Also, if you override init in your view, make sure to call #_super().
Note: the model hook is only called when landing on a page to deserialize the URL, not when transitioning from another page and changing the model instance.
Route#model is your friend here. It will receive a params hash containing information from the URL on every route change (even when changing just which instance of a class is being viewed) In your case,
App.ConversationRoute = Ember.Route.extend {
model: (params) ->
App.Conversation.find params.conversation_id
setupController: (controller, conversation) ->
// you have the correct Conversation object
The guides have more examples.
The didInsertElement method of the view is the best method if you need to instantiate something on your view. If you need to have the controller do something when the template loads, you can put the call in the setupController method of your route:
App.FirstRoute = Ember.Route.extend({
setupController: function(controller){
controller.onload();
}
});
Here's a jsfiddle with a full example:
http://jsfiddle.net/jgillick/Q5Kbz/
This will be called each time that route is loaded. Try the jsfidle. Click along to the second template and then use your browser's back button. The onload should fire again.
Also, fun fact, you can use the deactivate method as an unload, to do anything you need to that controller when the user navigates away from that route:
App.FirstRoute = Ember.Route.extend({
deactivate: function(){
this.controller.unload();
}
});
Unrelated
One thing to note, (not directly related to your question) if you set the model on the controller in the setupController method, it will overwrite your controllers content property each time that route is loaded. To prevent this, put a conditional around the assignment:
setupController: function(controller, model) {
controller.onload();
if (model && (!controller.content || Ember.keys(controller.content).length === 0)) {
controller.set('content', model);
}
}
Using the non-async router, we could expect redirect on a route to be called only after resolving promises on from the model function. That's no longer the case.
How can something like this be implemented today?
App.ClientRoute = Ember.Route.extend
model: (params) ->
App.Client.findById params.client_id
redirect: ->
unless #modelFor 'client'
#transitionTo 'clients'
As of RC6, you would implement this like so:
App.ClientRoute = Ember.Route.extend
model: (params) ->
App.Client.findById params.client_id
afterModel: (resolvedModel)->
unless resolvedModel
#transitionTo 'clients'
those are two Gists from the developer of the new async router of Ember.js which will explain the new behaviour and show you some examples:
https://gist.github.com/machty/5723945
https://gist.github.com/machty/5647589
Hope they'll help you - I'm reading and following through at the moment and I think everything is really well explained ;)
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...
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.