Composable components

Miguel Camba

@miguelcamba

@cibernox

miguelcamba.com

LET'S USE THE INTERNET!

Addon selection

  • Browse emberobserver.com
  • Gather ~5 alternatives
  • Stars counting
  • Feature comparison
  • Maintenance tie-break

re·use

To use again, especially after salvaging or special treatment or processing.

(rē-yo͞oz′)

tr.v.

Cost of construction > Cost of reuse

Coeficient of reusability

‎(ノಥ益ಥ)ノ ┻━┻

L

M

A

O

arge

onolith with an

rmy of

ptions

We know better

  • Bindings
  • Actions
  • Blocks
  • Components

Think in the community first

Share your pain with others

Value existing solutions

Look for help. "Solo" heroes die soon.

Think in the API second

Before writing any code

{{select options=options}}

Start with the minimum

{{select options=options selected=foo}}
{{select options=list 
         selected=foo 
         onchange=(action "didChoose")}}

DDAU: Prefer actions over bindings

{{select options=list 
         selected=foo 
         onchange=(action (mut foo))}}
{{my-component
  options=list
  optionLabelPath="foo"
  optionValuePath="bar"
  delay=300
  inputWidth=200
  language="en-US"
  onFooBar="sendEmail"
  boxColor=...
  highlight=...
  ajaxAdapter=...
  initSelection=...
  identityMap=...
  refreshOnHover=...}}

Pretend every option costs you £50

2x if mandatory

{{#each users as |user|}}
  {{user.name}}
{{else}}
  <p>No matching results</p>
{{/each}}

Seek familiarity

{{#select options=users selected=foo as |user|}}
  {{user.name}}
{{else}}
  <p>No matching results</p>
{{/select}}

OOP principles apply just as well to components as they do to code

Favor composition over Inheritance

  1. Identify responsability
  2. Divide
  3. Compose

Single responsability principle

Identify

  • The list of options that bypasses overflow rules

What do all selects have in common?

  • Using the trigger toggles the list of options
  • Clicking anywhere else hides the list
  • Clicking an option selects it
  • Navigate with keyboard, highlight on hover, ect...

Divide

<div class="trigger" onclick={{action "toggle"}}>
  {{yield selected}}
</div>
{{#if opened}}
  <div class="dropdown-content">
    {{yield}}
  </div>
{{/if}}

{{ember-basic-dropdown}}

actions: {
  toggle() {
    this.get('opened') ? this.close() : this.open();
  }
},

open() {
  this.set('opened', true);
  this.addClickHandlerToBody();
},

close() {
  this.set('opened', false);
  this.removeClickHandlerFromBody();
}

{{ember-basic-dropdown}}

Compose

{{#dropdown}}
  {{#each options as |option|}}
    {{yield option}}
  {{else}}
    {{yield to="inverse"}}
  {{/each}}
{{else}}
  {{yield selected}}
{{/dropdown}}

{{ember-power-select}}

{{#select options=list selected=selected as |opt|}}
  {{option}}
{{else}}
  <p>No results</p>
{{/select}}

Your app

Repeat

<div class="trigger" onclick={{action "toggle"}}>
  {{yield selected}}
</div>
{{#if opened}}
  {{#ember-wormhole to=destinationElmnt}}
    <div class="dropdown-content">
      {{yield}}
    </div>
  {{/ember-wormhole}}
{{/if}}

{{ember-basic-dropdown}}

actions: {
  toggle() { }
},

open() {
  // ...
  this.listenToResizeScrollAndOrientationEvents();
  run.schedule('afterRender', this, this.reposition);
},

reposition() {
  // Calculate coordinates
}

{{ember-basic-dropdown}}

contentFor: function(type) {
  if (type === 'body-footer') {
    return '<div id="dropdown-wormhole"></div>';
  }
}

{{ember-basic-dropdown}}

{{#select options=list selected=selected as |opt|}}
  {{option}}
{{else}}
  <p>No results</p>
{{/select}}

Your app

{{ember-power-select}}

{{ember-basic-dropdown}}

{{ember-wormhole}}

{{#dropdown}}
  <ul class="select-options">
    {{#each results as |opt|}}
      <li class="select-option {{selected}} {{highlighted}}" 
          onclick={{action "select" opt}} 
          onmouseover={{action "highlight" opt}}>

        {{yield opt}}

      </li>
    {{/each}}
  </ul>
{{else}}
  ...
{{/dropdown}}

Components

inter-comunication

From child to parent

{{my-foo onFoo="didFoo"}}

From child to parent with reply

let reply = this.get('onFoo')(someArg);
this.sendAction('onFoo', someArg);
{{my-foo onFoo=(action "didFoo")}}

From parent to child ??

😱

<div class="trigger" onclick={{action "toggle"}}>
  {{yield selected}}
</div>
{{#if opened}}
  {{#ember-wormhole to=destinationElmnt}}
    <div class="dropdown-content">

{{ember-basic-dropdown}}

    </div>
  {{/ember-wormhole}}
{{/if}}
      {{yield}}
      {{yield (action "close")}}

{{ember-power-select}}

{{#dropdown}}
  <ul class="select-options">
    {{#each results as |opt|}}
      <li class="select-option {{selected}} {{highlighted}}" 
          onclick={{action "select" opt}} 
          onmouseover={{action "highlight" opt}}>

        {{yield opt}}

      </li>
    {{/each}}
  </ul>
{{else}}
  ...
{{/dropdown}}
{{#dropdown}}
          onclick={{action "select" opt}} 
{{#dropdown as |closeDropdown|}}
          onclick={{action "select" opt closeDropdown}} 

{{ember-power-select}}

export default Ember.Component.extend({
  actions: {
    select(selection, closeDropdown, evt) {
      // do stuff ...
      closeDropdown(); // ... and close :-P
    }
  }
});
    select(selection, closeDropdown, evt) {
      // do stuff ...
      closeDropdown(); // ... and close :-P
    }
    select(selection, closeDropdown, evt) {
      // do stuff ...
      this.onchange(selection, closeDropdown);
    }

💥 BOOM!

We've just inverted DDAU 😎

We've just inverted DDAU 😎

AUDD

Bidirectional communication enables a whole new level of composability and customization

{{yield (action "close")}}
{{yield this}}

But you have to be careful

🚫

DANGER

Expose only your API

// helpers/hash.js
import Ember from 'ember';

export default Ember.Helper.helper((_, hash) => hash);
{{yield (hash 
    close=(action "close") 
    open=(action "open") 
    isOpen=opened)}}

Comming in 2.3, but polyfillable now: 

https://github.com/cibernox/ember-hash-helper-polyfill

{{#dropdown}}
  <ul class="select-options">
    {{#each results as |opt|}}
      <li class="select-option {{selected}} {{highlighted}}" 
          onclick={{action "select" opt}} 
          onmouseover={{action "highlight" opt}}>

        {{yield opt}}

      </li>
    {{/each}}
  </ul>
{{else}}
  ...
{{/dropdown}}
{{#dropdown}}
          onclick={{action "select" opt}} 
{{#dropdown as |dropdown|}}
          onclick={{action "select" opt dropdown}} 
export default Ember.Component.extend({
  actions: {
    select(selection, dropdown, evt) {
      // ...
      dropdown.close();
    }
  }
});

You can't cover all use cases.

Face it.

But users can if you let them.

Enable composition for everyone

Identify

{{ember-power-select}}

{{ember-basic-dropdown}}

{{ember-wormhole}}

{{e-p-s/selected}} + {{e-p-s/options}}

{{#dropdown as |dropdown|}}
  <ul class="select-options">
    {{#component optionsComponent
                 options=results 
                 selected=selected 
                 select=(action "select") 
                 highlight=(action "highlight") 
                 dropdown=dropdown as |opt|}}

      {{yield opt}}

    {{/component}}
  </ul>
{{else}}
  {{component selectedComponent selected=selected}}
{{/dropdown}}


    {{#component optionsComponent
                 options=results 
                 selected=selected 
                 select=(action "select") 
                 highlight=(action "highlight") 
                 dropdown=dropdown as |opt|}}

      {{yield opt}}

    {{/component}}


  {{component selectedComponent selected=selected}}
{{#select options=list 
          selected=selected as |opt|}}


  {{option}}

{{else}}
  <p>No results</p>
{{/select}}

Your app

{{#select options=list 
          selected=selected
          optionsComponent="animated-opts" as |opt|}}

  {{option}}

{{else}}
  <p>No results</p>
{{/select}}

Endless possibilities

Don't wait.

All available in 1.13+

Compose for fun and profit

Thanks

Reusable components

By Miguel Camba

Reusable components

  • 9,104