Communication between cousin components - two-way binding vs. unidirectional?


#1

Trying to find best practices for a component relationship I see a lot in my apps: cousin components that are backed by the same models, where there is a one-to-many relationship between the shared ancestor component and each cousin. E.g.

                 ancestor-component
                         |
             +-----------+------------+
             v                        v
          parent1                  parent2
             +                        +
    +----------------+       +----------------+
    v        v       v       v        v       v
cousinA   cousinB  cousinC cousinA  cousinB  cousinC


Each set of siblings represent a different visualization of the data. Cousins A are backed by the same data, cousins B likewise, etc. I’m looking for best practices in ways to sync the visual state of cousins. E.g. hover on one cousin, and the effect is registered on it’s corresponding cousin.

I’ve found two general patterns: two-way binding using the model, and unidirectional using the data-down, actions-up pattern. E.g., to have corresponding cousin components enter a hover state when either is hovered on:

  1. two-way binding Ember Twiddle
// register an isHoverState on the model
export default Ember.Component.extend({
  classNames: ['child-one'],
  classNameBindings: ['model.isHoverState:hover'],
  
  announceHoverState: Ember.observer('model.isHoverState', function() {
      console.log('cousins hovered:', this.get('model.id'));
  }),
  
  mouseEnter() {
    this.set('model.isHoverState', true);
  },
  
  mouseLeave() {
    this.set('model.isHoverState', false);
  }
});
  1. data down/actions up Ember Twiddle
// send mouseEnter/Leave actions up to the shared ancestor component, 
// and a childOne/TwoHover property down to the other parent component
  // (see the twiddle for all the code)
export default Ember.Component.extend({
  classNames: ['child-two'],
  classNameBindings: ['cousinIsHover:hover', 'selfIsHover:hover'],
  
  cousinIsHover: Ember.computed('childOneHover', function() {
    return this.get('childOneHover') === this.get('model');
  }),
  
  announceCousinHover: Ember.observer('childOneHover', function() {
    if (this.get('childOneHover') === this.get('model')) {
      console.log('cousins hovered:', this.get('model.id'));
    }
  }),
  
  mouseEnter() {
    this.sendAction('mouseEntered', this.get('model'));
    this.set('selfIsHover', true);
  },
  
  mouseLeave() {
    this.sendAction('mouseLeft', this.get('model'));
    this.set('selfIsHover', false);
  }
});

I have some early observations about the pros and cons of each approach, but I wanted to know if anyone who has spent more time in the Ember space (I’m going on two months here), has any input on best practices, antipatterns, or other approaches I didn’t consider here.

Initial thoughts:

two-way binding via model

  • takes ~1/2 the lines of code, and is simpler in some ways to reason about
  • requires you to store view state on the model (bad?)
  • it’s not possible to tell which cousin component set the state (bad?)

unidirectional flow

  • requires a lot more code, particularly with a deep component hierarchy from ancestor to cousin
  • the component signatures become a lot longer as the number of possible states increases (b/c you are passing down more data, and up more actions)
  • I don’t like having to pass the childOneHover model down to all child-two components (and childTwoHover model to all child-one components), and compare it against it’s own model to make sure it’s a match.
  • it is possible to tell which cousin initiated the state change, and thus manage reactions to state change in a much more detailed way. (very good)
  • its dependencies are more explicit, and thus easier to test. (very good)

Any thoughts/feedback? Hope this isn’t too general (it’s very specific to a number of cases I’m working on, but maybe not for others?)


#2

requires you to store view state on the model (bad?)

Bad. There are lots of reasons for that, but a few examples should be enough: what if you reload the model from the server? Or if you clone a model for some reason?

Model is for permanent data, not for transient state.

There is a better pattern: move your state into a state service. I believe there are a few examples on this very forum, and I know for sure I read a blog post on that subject a few months ago.

There is also https://github.com/stefanpenner/ember-state-services that provides an implementation of the concept that’s lightweight yet powerful, I recommend you have a look: even if you don’t use it, it will give you ideas on how to do this kind of things.

If your components need to exchange events instead of/in addition to states, you could also check http://www.thesoftwaresimpleton.com/blog/2015/04/27/event-bus/


#3

Yeah, a state-service would definitely be an improvement over simply storing the state on the model. A pub/sub service works as well, though it’s a little more unruly–potentially allowing anything to modify the state of anything else. Both are essentially a ‘two-way binding’ strategy, though, yes? And both introduce implicit dependencies (the first on the model, the second on the component), and make testing a little bit harder.

The above is not necessarily bad, but it does seem to go against the ‘data-down/actions-up’ unidirectional strategy that is at the heart of Ember 2.x.

But the unidirectional strategy brings its own drawbacks, so I guess I should just pick my poison.