@miguelcamba
@cibernox
miguelcamba.com
<button type="button" class="btn btn-{{size}}">
{{yield}}
</button>
{{#my-btn size="big"}}Submit{{/my-btn}}
<button type="button" class="btn btn-big">
Submit
</button>
Components === Functions
Attributes === Arguments
Avoid implicit contracts with closure actions
export default Ember.Component.extend({
tagName: 'button'
click(e) {
this.sendAction('onClick', e);
}
});
export default Ember.Controller.extend({});
{{#my-btn onClick="foobar"}}Submit{{/my-btn}}
export default Ember.Controller.extend({});
{{#my-btn onClick=(action "foobar")}}
Submit
{{/my-btn}}
{{#my-btn onClick="foobar"}}
Submit
{{/my-btn}}
{{#my-btn onClick=(action "foobar")}}
Submit
{{/my-btn}}
<button onclick={{save}} disabled={{disabled}}>
{{yield}}
</button>
export default Ember.Controller.extend({
saving: true,
actions: {
save() {
this.set('saving', true);
this.get('model').save().
finally(() => this.set('saving', false));
}
}
});
{{#my-btn save=(action "save") disabled=saving}}
Submit
{{/my-btn}}
export default Ember.Component.extend({
tagName: '',
saving: false,
actions: {
save() {
this.set('saving', true);
return this.get('save')(). // returns a promise
finally(() => this.set('saving'));
}
}
});
<button onclick={{action "save"}} disabled={{saving}}>
{{yield}}
</button>
export default Ember.Controller.extend({
actions: {
save() { return this.get('model').save(); }
}
});
export default Ember.Component.extend({
tagName: '',
save: task(function * () {
yield this.get('save')();
})
});
<button onclick={{perform save}} disabled={{save.isRunning}}>
{{yield}}
</button>
{{my-btn save=(action "save")}}
Submit
{{/my-btn}}
this.sendAction("foo", arg1, arg2);
this.get('foo')(arg1, arg2);
Yield your API
Typically, components invoke actions on their parents
That's why it's called DDAU (Actions Up)
The best we can do is to answer to those actions returning values from them.
{{#conversations-list users=user as |conv user|}}
{{user-avatar user=user}}
{{chat-bubble msg=conv.lastMsg}} at {{conv.lastMsg.time}}
{{chat-textbox conversation=conv}}
{{/conversation-list}}
|conv user|
user
conv conv
conv
{{#if open}}
{{#ember-wormhole to="#foo"}}
{{yield (action "close")}}
{{/ember-wormhole}}
{{/if}}
export default Ember.Component.extend({
classNames: 'my-modal',
actions: {
close() {
this.set('open', false);
}
}
});
{{#my-modal as |close|}}
<p>Lorem ipsum</p>
<button onclick={{close}}>Cancel</button>
<button onclick={{action "save"}}>Submit</button>
{{/my-modal}}
|close|
onclick={{close}}
|close open resize isDismisable|}}
aka "The remote controller" pattern
{{yield
(action "close")
(action "open")
(action "resize")
isDisimisable
}}
{{#my-modal as |close open changeSize canDismiss|}}
<button onclick={{open}}>Open<button>
{{#if canDismiss}}
<button onclick={{close}}>Close<button>
{{/if}}
<button onclick={{changeSize}}>Resize<button>
{{/my-modal}}
{{yield (hash
close=(action "close")
open=(action "open")
resize=(action "resize")
isDisimisable=isDisimisable
)}}
{{#my-modal as |modal|}}
<button onclick={{modal.open}}>Open<button>
{{#if modal.isDismisable}}
<button onclick={{modal.close}}>Close<button>
{{/if}}
<button onclick={{modal.resize}}>Resize<button>
{{/my-modal}}
|modal|
onclick={{modal.open}}
modal.isDismisable
onclick={{modal.close}}
onclick={{modal.resize}}
{{yield (hash
open=(action "open")
)}}
{{#my-modal as |modal|}}
{{!-- what is modal.open? A flag? An action? --}}
{{/my-modal}}
{{yield (hash
isOpen=isOpen
actions=(hash
open=(action "open")
)
)}}
{{#my-modal as |modal|}}
{{#unless modal.isOpen}}
<button onclick={{modal.actions.open}}>Open<button>
{{/unless}}
{{/my-modal}}
export default Component.extend({
isOpen: false,
actions: {
open() { this.set('open', true); }
}
});
{{yield (hash
isOpen=isOpen
actions=(hash
open=(action "open")
)
)}}
If you have a public API, tell everyone.
{{my-datepicker
date=birthday
onChange=(action (mut birthday))
onKeyDown=(action "handleKeyboard")
}}
export default Ember.Controller.extend({
actions: {
handleKeyboard() {
// Open on enter? How can I do that?
}
}
});
<input onkeydown={{onKeyDown}}/>
So, pass your public API on your actions so you don't need to
export default Ember.Controller.extend({
actions: {
handleKeyboard(api, e) {
if (e.keyCode === 13) {
api.actions.open();
}
}
}
});
{{#with (hash
isOpen=isOpen
action=(hash open=(action "open")) as |api|}}
<input onkeydown={{action onKeyDown api}}>
{{/with}}
Starting from the end
{{my-component
onChange=(action (mut birthday)) // function(value) { ... }
onKeyDown=(action "handleKey") // function(event) { ... }
move=(action "moveFromTo")}} // function(from, to) { ... }
{{my-component
onChange=(action (mut birthday)) // function(value, e, api) { ... }
onKeyDown=(action "handleKey") // function(e, api) { ... }
move=(action "moveFromTo")}} // function(from, to, e, api) { ... }
function(arg1, arg2, ... argN-2, event, api)
function(arg1, arg2, ... argN-2, event, api)
function(arg1, arg2, ... argN-2, event, api)
function(arg1, arg2, ... argN-2, api, event)
This is easier
This is more flexible
He also get to play games some times
{{#my-modal as |modal|}}
<button onclick={{modal.open}}>Open<button>
{{!-- in here you can access the public API --}}
{{/my-modal}}
{{!-- but what about here? --}}
{{#my-modal register=(action (mut modal)) as |modal|}}
...
{{/my-modal}}
export default Ember.Component.extend({
init() {
this._super();
if (this.get('register')) {
this.get('register')(this.get('publicAPI'));
}
},
publicAPI: Ember.computed(function() {
return {
isOpen: false,
actions: {
open: () => this.send('open')
}
}
})
});
openComponent() {
let fakeClick = new MouseEvent('click');
let button = this.$('.my-component-open-button').get(0);
button.dispatchEvent(fakeClick);
},
highlightOption(obj) {
let event = new MouseEvent('mouseover');
let selector = `.my-component-item:contains("${obj.title}")`;
let button = this.$(selector).get(0);
button.dispatchEvent(event);
}
When all fails, do it yourself.
Never use e.preventDefault() to hijack your component's default behaviour.
instead.
e.preventDefault()
return false;
export default Ember.Component.extend({
actions: {
handleKeyboard(api, e) {
let action = this.get('onKeyDown');
if (action && action(e, api) === false) {
return;
}
// otherwise, do the usual thing...
}
}
});
{{my-component onKeyDown=(action "customThing")}}
{{my-datepicker value=date}}
{{my-datepicker/input value=value onClick=(action "open")}}
{{#if isOpen}}
{{#my-datepicker/popup}}
{{my-datepicker/navbar foo=bar}}
{{my-datepicker/calendar month=month}
{{my-datepicker/hour-selector}}
{{/my-datepicker/popup}}
{{/if}}
{{component inputComponent value=value onClick=(action "open")}}
{{#if isOpen}}
{{#component popupComponent}}
{{component navbarComponent foo=bar}}
{{component calendarComponent month=month}
{{component hourComponent}}
{{/component}}
{{/if}}
{{component inputComponent
{{#component popupComponent
{{component navbarComponent
{{component calendarComponent
{{component hourComponent}}
{{/component}}
{{my-datepicker value=date calendarComponent="custom-calendar"}}
calendarComponent="custom-calendar"
{{#basic-dropdown as |dropdown|}}
This is the content
{{else}}
<button>Click to open</button>
{{/basic-dropdown}}
{{#basic-dropdown as |dropdown|}}
{{!-- Content is the first block? WUT? --}}
{{else}}
<button>Click to open</button>
{{!-- No access to the yielded API here --}}
{{!-- How does that button open component? Magic? --}}
{{/basic-dropdown}}
{{#basic-dropdown
ariaLabel="foo"
disabled=disabled
horizontalPosition="right"
verticalPosition="top"
destination="#placeholder"
renderInPlace=true
triggerClass="my-trigger"
dropdownClass="my-dropdown"
...
as |dropdown|}}
{{#basic-dropdown as |api|}}
{{#basic-dropdown/trigger
class="my-trigger"
ariaLabel="foo"
disabled=disabled}}
{{!-- Access to yielded args --}}
{{!-- The user gets to decide the layout --}}
{{/basic-dropdown/trigger}}
{{!-- I can even add extra markup here!! --}}
{{#basic-dropdown/content
class="my-dropdown"
destination="#placeholder"
renderInPlace=true
horizontalPosition="right"
verticalPosition="top"}}
{{!-- Access to yielded args too --}}
{{/basic-dropdown/content}}
{{/basic-dropdown}}
{{#basic-dropdown as |api isOpen onFocusIn onFocusOut onKeyDown|}}
{{#basic-dropdown/trigger api=api
isOpen=isOpen
onFocusIn=onFocusIn
onFocusOut=onFocusOut
onKeyDown=onKeyDown}}...{{/basic-dropdown/trigger}}
{{#basic-dropdown/content api=api
isOpen=isOpen
onFocusIn=onFocusIn
onFocusOut=onFocusOut}}...{{/basic-dropdown/content}}
{{/basic-dropdown}}
{{yield (hash
trigger=(component "basic-dropdown/trigger"
isOpen=isOpen
open=(action "open")
onKeyDown=(action "onKeyDown")
onFocusIn=(action "onFocusIn")
onFocusOut=(action "onFocusIn")
)
content=(component "basic-dropdown/content"
isOpen=isOpen
open=(action "open")
onFocusIn=(action "onFocusIn")
onFocusOut=(action "onFocusIn")
)
isOpen=isOpen
actions=(hash open=(action "open") ...)
)}}
{{#basic-dropdown as |dropdown|}}
{{#dropdown.trigger}}...{{/dropdown.trigger}}
{{#dropdown.content}}...{{/dropdown.content}}
{{/basic-dropdown}}
{{#basic-dropdown as |dropdown|}}
{{#dropdown.trigger class="my-trigger"}}
...
{{/dropdown.trigger}}
{{#dropdown.content
horizontalPosition="right"
renderInPlace=true}}
...
{{/dropdown.content}}
{{/basic-dropdown}}