@miguelcamba
@cibernox
miguelcamba.com
In mathematics and computer science, a higher-order function is a function that does at least one of the following:
function buildAdder(inc) {
return function(num) { return num + inc; };
}
let add1 = buildAdder(1);
add1(3); // => 4
let add4 = buildAdder(4);
add4(3); // => 7
sum(1, 2); // => 3
{{fa-icon "clock"}} {{! <i class="fa fa-camera"></i> }}
fn(arg1, arg2, ...) => <UX (DOM + Events)>;
Function(args) -> Function
Component(args) -> Component
{{#thermomix ingredients=ingredients user=u as |meal|}}
<span class="take-away-bag">
{{meal}}
</span>
{{/thermomix}}
{{yield
(bake (mix ingredients) "45m")
}}
<span class="take-away-bag">🍔</span>
{{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)}}}}
{{#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}}
{{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}}
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
{{x-toggle checked=checked}}
<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() { /**/ }
});
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>
{{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()
});
<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"}}
export default Ember.Controller.extend({
size: 'small',
color: 'green',
actions: {
//...
}
});
{{x-toggle checked=checked
onChange=(action (mut checked))}}
<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"}}
<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"}}
{{#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"}}
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.
{{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);
}
}
});
<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'
});
{{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}}
{{#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}}
{{x-toggle checked=checked onChange=(action (mut checked))}}
{{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);
}
}
});
<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>
{{x-toggle checked=checked onChange=(action (mut checked))
switchComponent="my-cool-switch"}}
Create a familiy of reusable UX that can be composed together to create new ones
{{#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
}
}
{
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() { ... },
}
};
{{#basic-dropdown as |dd|}}
{{/basic-dropdown}}
{{#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}}
{{#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}}