Is there an official story for where interface state (as opposed to persisted model state) should live in an Ember.js app?
In the "Responding to User-initiated Events" part of the Router docs, there's an example of delegating click events to a photo's "showPhoto" method, but having a model "show" itself seems like an undesirable mixing of concerns.
I understand that in many cases state should be stored in the router so that the interface state is represented in the URL and is restored if you refresh the page or send the url to someone. But what about non-hierarchical state, such as the list of items selected on a page?
Ideally that type of state would be serialized as query/hash params (eg: http://www.hipmunk.com/flights/QSF-to-NYC#!dates=Sep15,Sep16p1;kind=flight&locations=QSF,YYZ&dates=Sep15,Sep23~tab=1 ) but as far as I know, the router doesn't offer that functionality, does it?
At BackboneConf, Jeremy Ashkenas said that the right way to do that in Backbone was to just store the state on the model too (he had an example of a model with a "selected" field).
But I believe Tom Dale said he didn't think that was a good idea, and not how it should be done in Ember. Unfortunately I don't remember him mentioning how it should be done.
If you want state to be routable (i.e. reachable via a url), then it needs to be serializable and deserializable via ember's router. If state is transient and not routable, then it is probably best kept on the controller.
If you need to represent complex interface state across multiple models (say, for selecting items in a list), consider maintaining a controller-specific array of objects that wrap underlying data models. I think it's hackish to represent view state directly on models, especially if those models are used across multiple views.
For the example you provided, you might do something like this to hook up a complex route:
Ember.Route.extend({
route: "flights/:cities/dates/:dates",
serialize: function(router, context){
return {cities: context.get('cities'),
dates: context.get('dates')};
},
deserialize: function(router, params){
// return a context object that will be passed into connectOutlets()
return {cities: params.cities,
dates: params.dates};
},
connectOutlets: function(router, context) {
// pass the context from deserialize() in as the content of a FlightController
router.get('applicationController').connectOutlet('flight', context);
}
})
Note that you could also use a route such as "flights?cities=:cities&dates=:dates" but the above is probably cleaner and more SEO-friendly.
Expanded upon after Gabriel's comments: If you want to maintain an array of searches, each of which resides in its own tab, I'd recommend keeping the data for those searches in an application-level array (e.g. App.currentUser.activeSearches). My reasoning is that you don't want to have to recreate this data every time a user switches tabs. Instead, the router would retrieve this data in deserialize()
and then pass it as the context to connectOutlets()
. The view and controller to represent this data should be quickly re-constructed based upon this object when switching tabs. Let me extend my example from above:
Ember.Route.extend({
route: "flights/:cities/dates/:dates",
serialize: function(router, context){
return {cities: context.get('cities'),
dates: context.get('dates')};
},
deserialize: function(router, params){
// find or create a "Search" object that contains the filters and results,
// which will be passed into connectOutlets()
return App.currentUser.findOrCreateSearch({cities: params.cities,
dates: params.dates});
},
connectOutlets: function(router, context) {
// pass the context (a search object) from deserialize() in as the content of a FlightController
router.get('applicationController').connectOutlet('flight', context);
}
})