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,682