Skip to content

Instantly share code, notes, and snippets.

@simonihmig
Last active December 23, 2016 19:02
Show Gist options
  • Save simonihmig/f4cada751b89b192f608bcb83ed1c22a to your computer and use it in GitHub Desktop.
Save simonihmig/f4cada751b89b192f608bcb83ed1c22a to your computer and use it in GitHub Desktop.
DDAU for public properties with common behaviour

Patterns for common behaviour of Ember components without violating DDAU

DDAU (data down action up) is the common pattern for creating ideomatic components in modern Ember.js. This prevents (unexpectedly) changing your application state in components, and enables you to put your state management logic to where it belongs: the component/controller/service that "owns" that state.

But what to do if you want to enable controlling some specific part of your component for some rare cases where this is needed, but have a default common behaviour (just UI related) for controlling that property where this is not needed in most cases.

Let's look at this with an example: we want to create a component for a simple dropdown button. It has a open property that controls the state of its dropdown menu being shown: when you click on the button that property is set to true which renders the dropdown menu, and when you click again outside of it it will set it to false and thus hide the menu. So far so good, this is the default behaviour you would expect, and that is fine as long as this property is declared as private.

But what if you want to enable your application to programmatically control opening or closing the dropdown menu. At first this seems easy, just declare the open property as public and create a binding to that property in your component invocation:

{{drop-down open=someOtherProperty}}

But then when the default behaviour remains in place, with Ember's two-way bindings changing the open property within the component because of some click events will propagate that change to someOtherProperty, so changing your application state within the dropdown and thus violating the DDAU principle.

So our problem in a nutshell: we want the component to have its default behaviour for 99% of the cases were we do not need full control, but allow this control for some rare cases without violating DDAU. And a common pattern agreed upon by the Ember community for solving that problem seems to be missing, or at least I have not been able to identify one so far.

So here are my candidates...

A) Pure DDAU

The component does not set the public properties at all, thus all property bindings effectively become one-way.

export default Ember.Component.extend({
  open: false,
  onChange: () => {},
  _open() {
    this.get('onChange')(true);
  }
});
{{drop-down open=someOtherProperty onChange=(action (mut someOtherProperty))}}

Pros:

  • Purest DDAU approach

Cons:

  • delegates the handling of the open state to the user of the component, even when not required
  • requires a lot of boilerplate for each component use

B) Apply default behaviour only when not controlling property

The component checks wheather the open property is used, and if so the user of the component takes over control, otherwise the default behaviour is applied. This is similar to what https://github.com/jquense/uncontrollable does for React components.

export default Ember.Component.extend({
  open: false,
  onChange: () => {},
  _open() {
    if (this.attrs.open === undefined) {
      this.set('open', true);
    }
    this.get('onChange')(true);
  }
});
Uncontrolled: {{drop-down}}
Controlled: {{drop-down open=someOtherProperty onChange=(action (mut someOtherProperty))}}

Pros:

  • Allows a controlled (matches case A) and an uncontrolled (default behaviour, no boilerplate required) way of using the component

Cons:

  • Just by using the open property it looses its default behaviour, which might be unexpected by the user

C) Provide default behaviour with an overridable action

The default behaviour is implemented in the default action, that can be overriden.

export default Ember.Component.extend({
  open: false,
  onChange: (state) => this.set('open', state),
  _open() {
    this.get('onChange').call(this, true);
  }
});
Uncontrolled: {{drop-down}}
Controlled: {{drop-down open=someOtherProperty onChange=(action (mut someOtherProperty))}}
Two-way binding, violates DDAU: {{drop-down open=someOtherProperty}}

Pros:

  • Allows a controlled (matches case A) and an uncontrolled (default behaviour, no boilerplate required) way of using the component
  • Probably quite easy to reason about
  • Allows two-way binding by not using a custom action handler, if that is what the user desires (is this a pro or rather a con? :))

Cons:

  • Can accidentally violate DDAU if user binds to open property but forgets to add custom action handler
  • If the user wants to make use of the onChange action for some other purposes than controlling the UI state, he/she is still forced to control the UI state
  • If he/she forgets to do so the component will accidentally loose its expected behaviour

D) Disable default behaviour by action's return value

The default behaviour remains in place as long as you do not return false from the action, which signals that you want to take over control.

export default Ember.Component.extend({
  open: false,
  onChange: () => {},
  _open() {
    if (this.get('onChange')(true) !== false) {
      this.set('open', true);
    }
  }
});
Uncontrolled: {{drop-down}}
Controlled: {{drop-down open=someOtherProperty onChange=(action "dropdown")}}

Pros:

  • Allows a controlled (matches case A) and an uncontrolled (default behaviour, no boilerplate required) way of using the component
  • Allows two-way binding by not returning false, if that is what the user desires (is this a pro or rather a con? :))
  • Does not accidentally loose its expected behaviour

Cons:

  • Can violate DDAU if user binds to open property but forgets to return false
@fsmanuel
Copy link

fsmanuel commented Dec 23, 2016

@simonihmig It should be easy to make it a computed property:

function readsWithDefault(dependentKey, defaultValue) {
  return computed(dependentKey, {
    get() {
      return getWithDefault(this, dependentKey, defaultValue);
    },
    set(key, value) {
      return value;
    }
  });
}

export default Ember.Component.extend({
  open: readsWithDefault('isOpen', false)
});

@cibernox
Copy link

@simonihmig your concerns about leaking are valid. The component should nullify the remote controller in the willDestroy hook to avoid that. The reason for this approach is that there is more actions than open/close, some of which like reposition is not something you can trigger changing a property.

I needed a way of triggering actions from the outside, and since I had that, I put everything there. Although I do provide an initiallyOpened=true/false option to render the dropdown open in the first place, but it never updates again, from that point on the component holds its own state.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment