Ember.js - where should interface state be stored?
Asked Answered
R

1

6

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.

Retroversion answered 14/9, 2012 at 13:38 Comment(0)
C
4

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);
  }
})
Castalia answered 14/9, 2012 at 16:56 Comment(4)
I think the first part of your answer (about maintaining a on-controller array) is what I was looking for. With the example, though, I think I may not have been clear: I was trying to demonstrate that they have multiple flight searches serialized. In their interface, each search resides in it's own tab, of which there can be many, so they aren't really hierarchical. Another example could be a filtering interface, where an unlimited number of filters could be applied to an input. Storing filters on the controller makes sense, but it would be awesome to have the state reflected in the URL too.Retroversion
It's also possible, of course, (likely, even?) that I'm misinterpreting your reply. In any case, thanks for the answer! I'm going to hold off accepting it for the moment to see if we can come up with a solution to the serialization side of things, but my impression is that at the moment state is either hierarchical and thus serializable with the router, or not, in which case you're on your own.Retroversion
Gabriel - I missed that nuance to your question the first time, but I expanded upon my answer to address it. I think this scenario is at the edge of the conventions provided by ember, but can be easily handled in an app-specific way (like in my expanded example).Castalia
Sorry for taking a while to get back to accept this. I've accepted it now, though I'm not sure how much has changed with the new router. Is this still the recommended approach?Retroversion

© 2022 - 2024 — McMap. All rights reserved.