Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Where to store transient UI state in Ember 2.0

Tags:

ember.js

Ember 2.0 has gone great lengths towards making everything a component. With routable components coming soon, controllers will probably phased out as well.

Context

However, there is a recurring problem I face when building User interface, which I don't have a satisfying pattern for so far: user interface state.

What am I taking about?

  • Selection state
  • Current focus
  • Folded/unfolded state in some tree display

Basically, any state that is not part of the actual data, yet has to be tracked on an object-by-object basis. In the past, this used to be done with Controllers, acting as proxies to models. This approach is now obsolete. The new approach is Components everywhere, for the better. Component does the bookkeeping, tracks transient state and you get actions back.

A typical pattern I have, though, is with shared state, such as a list with selectable items.

The issue

Building a list component, with the following requirements:

  • Each item can be selected.
  • Selection state changes the DOM (some class bindings).
  • User can draw a rectangle in the list component to select several items at once.
  • Ideally, the whole behavior can be abstracted out as a mixin.

Question: where does the selection flag live?

Attempts

1) Everything is a component.

I make each item a sub-component, say {{my-list-item}}. The component tracks the selection state. Problem: how can the list component update the selection state?

2) Move state out of sub-components.

Put it on the list component. Within a separate state array alongside the item list. Pros: the list has all the state it needs. Cons: it's a nightmare to keep it synced when items are added or removed from the list. And it means the sub-component have no access to the state. Or maybe I could pass it to them as a bound value?

3) Reintroducing proxies.

Coming to think of it, there is a way to share some state: put it on the models. Well, not on the actual models so as not to pollute them with local state, but by setting up an ArrayProxy that will return some ObjectProxy for every item, to hold the state.

Pros: this is the only solution I managed to implement completely. Cons: encapsulation and decapsulation of items is a hassle. Also, after a few layers of being passed around, get and set have to go through 4 ou 5 proxies, which I fear will be a problem with performance.

Also, it does not work well for mixins. Should I want to abstract out some HasSelection mixin, and a HasFoldableItems mixin, and a Sortable mixin, they all need some state.

Back to the drawing board

Is there some better pattern I have not found?

I found the following relevant questions, but that led me nowhere:

  • Ember.js - where should interface state be stored? (2012, suggests something close to my #3 above)
  • Road to Ember 2.0 - High level Ember app structure feedback? (some of the key questions in the list are relevant to this one)
like image 568
spectras Avatar asked Sep 16 '15 23:09

spectras


1 Answers

Great question - I actually went to one of the core ember team members to find out and the answer currently is services. Because the component is best left stateless (for the most part) you can leverage a service to persist this state that doesn't fit into a server persisted model.

Here is a great example that Stef Penner put together showing how you might save a "email draft" in your ember app (that isn't persisted backend)

https://github.com/stefanpenner/ember-state-services

Example component for reference (from the github project above)

export default Ember.Component.extend({
  tagName: 'form',
  editEmailService: Ember.inject.service('email-edit'),
  state: Ember.computed('email', function() {
    return this.editEmailService.stateFor(this.get('email'));
  }).readOnly(),

  actions: {
    save() {
      this.get('state').applyChanges();
      this.sendAction('on-save', this.get('email'));
    },

    cancel() {
      this.get('state').discardChanges();
      this.sendAction('on-cancel', this.get('email'));
    }
  }
});
like image 85
Toran Billups Avatar answered Nov 05 '22 18:11

Toran Billups