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
- Identify responsability
- Divide
- 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:
{{#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,401