Higher Order Components

Miguel Camba

@miguelcamba

@cibernox

miguelcamba.com

What does "higher order" mean?

In mathematics and computer science, a higher-order function is a function that does at least one of the following:

  • Takes one or more functions as arguments
  • Returns a function as its result
function buildAdder(inc) { 
  return function(num) { return num + inc; };
}
let add1 = buildAdder(1);

add1(3); // => 4
let add4 = buildAdder(4);

add4(3); // => 7 

How does this apply to my components?

sum(1, 2); // => 3 

Most components are essentially functional

{{fa-icon "clock"}} {{! <i class="fa fa-camera"></i> }}

fn(arg1, arg2, ...) => <UX (DOM + Events)>;

Higher Order Functions

Function(args) -> Function
Component(args) -> Component

Higher Order Components

But components can't "return", can they?

{{#thermomix ingredients=ingredients user=u as |meal|}}
  <span class="take-away-bag">
    {{meal}}
  </span> 
{{/thermomix}}

{{yield}}

{{yield 
  (bake (mix ingredients) "45m") 
}}
<span class="take-away-bag">🍔</span>

Ok, we can return (ish), but how do return a component?

The {{component}} keyword.

Dynamic render

{{component "my-button"}} {{! renders comp named my-button }}

{{component foo}} {{! renders comp whose name is in foo }}
{{yield (component "user-avatar" image=user.pic size="big")
        (component "user-location" showMap=true)}}}}

Closure component creator

{{#user-card user=user as |avatar location|}}
  {{component avatar}}
  {{component location}}
  {{!user-avatar image=user.pic size="big"}}
  {{!user-location showMap=true}}
{{/user-card}}
<img src="/users/1/big.jpg" alt="Ben's avatar"> 
<span>Based in Portland</span><canvas></canvas>
{{#user-card user=user as |avatar location|}}
  {{component avatar size="small"}}
  {{component avatar showMap=false}}
  {{!user-avatar image=user.pic size="small"}}
  {{!user-location showMap=false}}
{{/user-card}}
{{yield (hash 
  avatar=(component "user-avatar" image=user.pic size="big")
  location=(component "user-location" showMap=true)
)}}

{{#user-card user=user as |card|}}
  {{component card.avatar}}
  {{component card.location}}
{{/user-card}}

Shorthand syntax

{{yield (hash 
  avatar=(component "user-avatar" image=user.pic size="big")
  location=(component "user-location" showMap=true)
)}}

{{#user-card user=user as |card|}}
  {{card.avatar}}
  {{card.location}}
{{/user-card}}
{{yield (hash 
  avatar=(component "user-avatar" image=user.pic size="big")
  location=(component "user-location" showMap=true)
)}}

{{#user-card user=user as |card|}}
  {{#card.avatar}}whatever{{/card.avatar}}
  {{card.location}}
{{/user-card}}

This seems complex.

Why would I care?

Oh boy.

Here we go

API DESIGN

API Design is important

  • The least options the better

  • Don't abuse bindings to communicate with your parent

  • Minimize mandatory options with sensible defaults

  • Options are passed to the component that cares about them

  • A well crafted component should be easy to adapt to new uses

Good API charactetistics

The {{x-toggle}} study case

{{x-toggle checked=checked}}

The initial approach: Bindings

<span class="x-toggle">
  <input type="checkbox" class="x-toggle-checkbox"
    checked={{checked}}
    onchange={{action (mut checked) value="target.checked"}}
    id={{uid}}>

  <label class="x-toggle-btn" for={{uid}}></label>
</span>
export default Ember.Component.extend({
  tagName: '',
  uid: Ember.computed(function(){ 
    return `input-${Ember.guidFor(this)}`;
  }
});
export default Ember.Controller.extend({
  checkedObserver: Ember.observer('checked', function(){
    this.saveToServer(this.get('checked'));
  }),
  saveToServer() { /**/ }
});

Challenge: Add autosave

export default Ember.Controller.extend({
  checked: Ember.computed({
    get() { return false; }
    set(_, v) {
      this.saveToServer(v);
      return v;
    }
  }),
  saveToServer() { /**/ }
});
export default Ember.Controller.extend({
  checked: Ember.computed({
    get() { return false; }
    set(_, v) {
      Ember.run.debounce(this, 'saveOnServer', v, 500);
      return v;
    }
  }).
  saveToServer(){ /**/ }
});
<span class="x-toggle">
  <input type="checkbox" class="x-toggle-checkbox"
    checked={{checked}}
    onchange={{action onChange value="target.checked"}}
    id={{uid}}>

  <label class="x-toggle-btn" for={{uid}}></label>
</span>

Solution: DDAU

{{x-toggle checked=checked onChange=(action (mut checked))}}
{{x-toggle checked=checked onChange=(action (mut checked))}}
{{x-toggle checked=checked onChange=(action "save")}}
export default Ember.Controller.extend({
  actions: {
    save(checked) {
      this.set('checked', checked);
      this.saveToServer();
    }
  },
  saveToServer(){ /**/ }
});
export default Ember.Controller.extend({
  actions: {
    save(checked) {
      this.set('checked', checked);
      this.perform('saveToServer');
    }
  },
  saveToServer: task(function*(){ /**/ }).restartable()
});

Challenge: Customize size & color

<span class="x-toggle {{color}} {{size}}">
  <input type="checkbox" class="x-toggle-checkbox"
    checked={{checked}}
    onchange={{action onChange value="target.checked"}}
    id="{{uid}}">

  <label class="x-toggle-btn" for={{uid}}></label>
</span>
{{x-toggle checked=checked 
           onChange=(action (mut checked)) 
           size="small" 
           color="green"}}

Improvement: Sensible defaults

export default Ember.Controller.extend({
  size: 'small',
  color: 'green',

  actions: {
    //...
  }
});



{{x-toggle checked=checked 
           onChange=(action (mut checked))}}

Challenge: Add a labels

<span class="x-toggle {{color}} {{size}}">
  <input type="checkbox" class="x-toggle-checkbox"
    checked={{checked}}
    onchange={{action onChange value="target.checked"}}
    id="{{uid}}">
  <label class="x-toggle-btn" for="{{uid}}"></label>
</span>

{{#if label}}
  <label for="{{uid}}" class="x-toggle-label {{size}}">
    {{label}}
  </label>
{{/if}}


{{x-toggle checked=checked 
           onChange=(action (mut checked))
           label="Enable notifications"}}

Challenge: The label can have colors too

<span class="x-toggle {{color}} {{size}}">
  <input type="checkbox" class="x-toggle-checkbox"
    checked={{checked}}
    onchange={{action onChange value="target.checked"}}
    id="{{uid}}">
  <label class="x-toggle-btn" for="{{uid}}"></label>
</span>

{{#if label}}
  <label for="{{uid}}" 
    class="x-toggle-label {{size}} {{labelColor}}">
    {{label}}
  </label>
{{/if}}


{{x-toggle checked=checked 
           onChange=(action (mut checked))
           label="Enable notifications"
           labelColor="green"}}

Problem: What about two labels?

With different colors

{{#if offLabel}}
  <label class="x-toggle-label {{size}} {{offLabelColor}}"
    role="button" onclick={{action onChange false}}>
    {{offLabel}}
  </label>
{{/if}}

<span class="x-toggle {{color}} {{size}}">
  <input type="checkbox" class="x-toggle-checkbox"
    checked={{checked}}
    onchange={{action onChange value="target.checked"}}
    id="{{uid}}">
  <label class="x-toggle-btn" for="{{uid}}"></label>
</span>

{{#if label}}
  <label for="{{uid}}" 
    class="x-toggle-label {{size}} {{labelColor}}">
    {{label}}
  </label>
{{/if}}

{{#if onLabel}}
  <label class="x-toggle-label {{size}} {{onLabelColor}}"
    role="button" onclick={{action onChange true}}>
    {{onLabel}}
  </label>
{{/if}}



{{x-toggle checked=checked 
           onChange=(action (mut checked))
           color="blue"
           offLabel="Disable notifications"
           offLabelColor="red"
           onLabel="Enable notifications"
           onLabelColor="green"}}

But that's not all

  • Images inside the labels
  • Allow arbitrary classes on labels & switch
  • Change disposition of the labels
  • The least options the better

  • Don't abuse bindings to communicate with your parent

  • Minimize mandatory options with sensible defaults

  • Options are passes to the level that cares about them

  • A well crafted component should be easy to adapt to new usages.

Why is this so hard?

We're working at the wrong level of abstraction

Should we keep components together because they are logically related even if they are presentationally separated?

Or should we separate them into different logicless visual units and wire the logic ourselves outside?

NONE

(or both)

Presentational components

Container components

  • Are concerned with how things look.
  • Rarely have their own state (when they do, it’s UI state rather than data).
  • Are concerned with how things work.
  • Provide the data and behavior to presentational or other container components.

Container Component: {{x-toggle}}

{{yield (hash 
  switch=(component 'x-toggle/switch' 
    checked=checked change=(action "change") uid=uid)
  label=(component 'x-toggle/label' 
    checked=checked change=(action "change") uid=uid)
)}}
export default Ember.Component.extend({
  tagName: '',
  uid: Ember.computed(function(){
    return `input-${Ember.guidFor(this)}`;
  }),
  actions: {
    change(value = !this.get('checked')) {
      this.get('onChange')(value);
    }
  }
});

Presentational: {{x-toggle/switch}}

<span class="x-toggle {{color}} {{size}}">
  <input type="checkbox" class="x-toggle-checkbox"
    checked={{checked}}
    onchange={{action change value="target.checked"}}
    id={{uid}}>
  <label class="x-toggle-switch" for={{uid}}></label>
</span>
export default Component.extend({
  tagName: '',
  color: 'green',
  size: 'small'
});

Presentational: {{x-toggle/label}}

{{yield}}
export default Ember.Component.extend({
  tagName: 'label',
  classNames: ['x-toggle-label'],
  classNameBindings: ['size', 'color'],
  attributeBindings: ['uid:for'],
  click(e) {
    return this.get('change')(this.get('value'));
  }
});
{{#x-toggle checked=val onChange=(action (mut val)) as |t|}}
  {{t.switch}}
{{/x-toggle}}

Flexibility and composability at the right level of abstraction

{{#x-toggle checked=val onChange=(action (mut val)) as |t|}}
  {{t.switch color="red" size="big"}}
{{/x-toggle}}
{{#x-toggle checked=val onChange=(action (mut val)) as |t|}}
  {{t.switch color="red" size="big"}}
  {{#t.label}}Toggle{{/t.label}}
{{/x-toggle}}
{{#x-toggle checked=val onChange=(action (mut val)) as |t|}}
  {{#t.label value=false color="green"}}Disable{{/t.label}}
  {{t.switch color="blue" size="big"}}
  {{#t.label value=true color="red"}}Enable{{/t.label}}
{{/x-toggle}}
{{#x-toggle checked=val onChange=(action (mut val)) as |t|}}
  {{t.switch color="blue" size="big"}}
  {{#t.label value=false color="green"}}Disable{{/t.label}}
  {{#t.label value=true color="red" class="italic"}}
    Enable
  {{/t.label}}
{{/x-toggle}}
{{#x-toggle checked=val onChange=(action (mut val)) as |t|}}
  <td>Notifications</td>
  <td>
    {{#t.label value=false color="green"}}Disable{{/t.label}}
  </td>
  <td>{{t.switch color="blue" size="big"}}</td>
  <td>
    {{#t.label value=true color="red"}}Enable{{/t.label}}
  </td>
{{/x-toggle}}
{{#if hasBlock}}
  {{yield (hash 
    switch=(component 'x-toggle/switch' 
        checked=checked onChange=(action "change") uid=uid)
    label=(component 'x-toggle/label' 
        checked=checked onChange=(action "change") uid=uid)
  )}}
{{else}}
  {{x-toggle/switch checked=checked 
    onChange=(action "change") uid=uid color=color size=size}}
{{/if}}

Without compromising the simplest use case



{{x-toggle checked=checked onChange=(action (mut checked))}}

Decoupling enables component composition

Presentational components are easy to replace

{{yield (hash 
  switch=(component switchComponent checked=checked 
          change=(action "change") uid=uid)
  label=(component labelComponent checked=checked 
          change=(action "change") uid=uid)
)}}
export default Ember.Component.extend({
  tagName: '',
  labelComponent: 'x-toggle/label',
  switchComponent: 'x-toggle/switch',
  uid: Ember.computed(function(){
    return `input-${Ember.guidFor(this)}`;
  }),
  actions: {
    change(value = !this.get('checked')) {
      this.get('onChange')(value);
    }
  }
});

Let users bring their own components

<span class="switch">
  <span class="switch-border1">
    <span class="switch-border2">
      <input id="switch1" type="checkbox" checked />
      <label for="switch1"></label>
      <span class="switch-top"></span>
      <span class="switch-shadow"></span>
      <span class="switch-handle"></span>
      <span class="switch-handle-left"></span>
      <span class="switch-handle-right"></span>
      <span class="switch-handle-top"></span>
      <span class="switch-handle-bottom"></span>
      <span class="switch-handle-base"></span>
      <span class="switch-led switch-led-green">
        <span class="switch-led-border">
          <span class="switch-led-light">
            <span class="switch-led-glow"></span>
          </span>
        </span>
      </span>
      <span class="switch-led switch-led-red">
        <span class="switch-led-border">
          <span class="switch-led-light">
            <span class="switch-led-glow"></span>
          </span>
        </span>
      </span>
    </span>
  </span>
</span>

{{my-cool-switch}}

{{x-toggle checked=checked onChange=(action (mut checked))
  switchComponent="my-cool-switch"}}

{{my-cool-switch}}

Ember Power Project

Create a familiy of reusable UX that can be composed together to create new ones

  • Ember Text Measurer
  • Ember Basic Dropdown
  • Ember Power Select
  • Ember Power Calendar
  • Ember Power Select Typeahead
  • {{paper-menu}}
  • {{paper-autocomplete}}
  • and more

Ember Power * API

{{#basic-dropdown as |dd|}}
  {{#dd.trigger}}Click me{{#dd.trigger}}
  {{#dd.content}}Hello world!{{/dd.content}}
{{/basic-dropdown}}
{{#power-calendar onSelect=(action "save") as |cal|}}
  {{cal.nav}}
  {{cal.days}}
{{/power-calendar}}
{
  props,
  components,
  actions: {
    // functions to interact with the component
  },
  helpers: {
    // utils to display component state
  }
}

Compose Higher Order Component 

Dropdown

Calendar

+

=

Datepicker

{
  uniqueId: <string>,
  disabled: <boolean>,
  isOpen: <boolean>,
  trigger: <Component>,
  content: <Component>,
  actions: {
    close() { ... },
    open() { ... },
    toggle() { ... }
  }
}
{
  uniqueId: <string>,
  selected: <date>,
  loading: <boolean>,
  center: <date>,
  nav: <Component>,
  days: <Component>,
  actions: {
    select() { ... },
    center() { ... },
  }
};

{{dropdown}}

{{calendar}}

{{#basic-dropdown as |dd|}}
  
  
  
  
  
  
  
  
  
  
{{/basic-dropdown}}

{{datepicker}}

{{#basic-dropdown as |dd|}}
  {{#power-calendar selected=selected 
    onSelect=(action 'onSelect') as |cal|}}
    
    
    
    
    
    
   
  {{/power-calendar}}
{{/basic-dropdown}}
{{#basic-dropdown as |dd|}}
  {{#power-calendar selected=selected 
    onSelect=(action 'onSelect') as |cal|}}
    {{yield 
      dd 
      cal 
      
      
      
     }}
  {{/power-calendar}}
{{/basic-dropdown}}
{{#basic-dropdown as |dd|}}
  {{#power-calendar selected=selected 
    onSelect=(action 'onSelect') as |cal|}}
    {{yield (assign
      dd 
      cal 
      
      
      
    )}}
  {{/power-calendar}}
{{/basic-dropdown}}
{{#basic-dropdown as |dd|}}
  {{#power-calendar selected=selected 
    onSelect=(action 'onSelect') as |cal|}}
    {{yield (assign
      dd 
      cal 
      (hash helpers=(hash 
        format-date=(action formatDate selected format))
      )
    )}}
  {{/power-calendar}}
{{/basic-dropdown}}
{{#power-datepicker selected=date 
  onChange=(action "save") as |dp|}}
  
  
  

  
  
  
  
{{/power-datepicker}}
{{#power-datepicker selected=date 
  onChange=(action "save") as |dp|}}
  {{#dp.trigger}}
    
  {{/dp.trigger}}

  {{#dp.content}}
    
    
  {{/dp.content}}
{{/power-datepicker}}
{{#power-datepicker selected=date 
  onChange=(action "save") as |dp|}}
  {{#dp.trigger}}
    <input type="text" value={{execute dp.format-date}}/>
  {{/dp.trigger}}

  {{#dp.content}}
    {{dp.nav}}
    {{dp.days}}
  {{/dp.content}}
{{/power-datepicker}}
{{#power-datepicker selected=date 
  onChange=(action "save") as |dp|}}
  <input type="text" value={{execute dp.format-date}}/>
  {{#dp.trigger}}<img src="...">{{/dp.trigger}}


  {{#dp.content}}
    {{dp.nav}}
    {{dp.days}}
  {{/dp.content}}
{{/power-datepicker}}
{{#power-datepicker selected=date 
  onChange=(action "save") as |dp|}}
  <input type="text" value={{execute dp.format-date}}/>
  {{#dp.trigger}}<img src="...">{{/dp.trigger}}

  {{#dp.content}}
    {{#dp.days weekdayFormat="long" as |day|}}
      <span class="circle">{{day.number}}</span>
    {{/dp.days}}
  {{/dp.content}}
{{/power-datepicker}}

Ember Power Datepicker

And there is more...

Recursive components

{{#options-menu as |menu|}}
  {{#menu.options-list options=options as |opt|}}
    {{opt}}
  {{/menu.options-list}}
{{/options-menu}}
{{#each options as |opt|}}
  {{#if (is-group opt)}}
    {{#menu.options as |nestedOpt|}}
      {{yield nestedOpt}}
    {{/menu.options}}
  {{else}}
    {{yield opt}}
  {{/if}}
{{/each}}

More at Ember Conf

Thanks

Higher Order Components

By Miguel Camba

Higher Order Components

  • 3,678