- Start Date: 2014-05-06
- RFC PR: (leave this empty)
- Ember Issue: (leave this empty)
The {{action
helper should be improved to allow for the creation of
closed over functions that can be passed between components and passed
the action handlers.
See this example JSBin from @rwjblue for a demonstration of some of these ideas.
Block params allow data to be passed from one component to a downstream component, however there is currently no way to pass a callback to a downstream component.
First, the existing uses of {{action
will be maintained. An action can be attached to an
element by using the helper in element space:
An action can be passed to a component as a string:
// app/components/my-button/component.js
export default Ember.Component.extend({
click: function(){
this.sendAction('on-click');
}
});
Or a default action can be passed:
// app/components/my-button/component.js
export default Ember.Component.extend({
click: function(){
this.sendAction();
}
});
In all these cases, submit
is called on the parent context relative to the scope action
is
attached in. The value "submit"
is attached to the component in the last two as
this.attrs.on-click
or this.attrs.action
, although it is not directly used.
Closure actions are created in a template and may be used in all places a string action name can be used. For example, this current functionality:
Would be written using a closure action as:
The functionality is exactly the same as the string-based action example. How does that happen?
(action "submit")
reads thesubmit
function off the current scope'sactions.submit
property.- It then creates a closure to call that function.
{{action
receives that function as a param. It registers a listener (in this case on click) and when fired calls the closure function.
Consider usage on the calling side. With the current string-based actions:
export default Ember.Component.extend({
click: function(){
this.sendAction(); // submit action
// this.attrs.action is a string
this.attrs.action === "submit";
}
});
With closure actions, the action is available to call directly. The (action
helper
wraps the action in the current context and returns a function:
{{my-component action=(action "submit")}}
export default Ember.Component.extend({
click: function(){
this.sendAction(); // submit action
// this.attrs.action is a function
this.attrs.action(); // submit action, new style
}
});
A more complete example follows, with a controller for context:
// app/index/controller.js
export default Ember.Controller.extend({
actions: {
submit: function(){
// some submission task
}
}
});
// app/components/my-button/component.js
export default Ember.Component.extend({
click: function(){
this.attrs.save();
// for enhanced backwards compat, you may also this.sendAction('save');
}
});
The current system of action bubbling falls down quickly when you want to pass a message through multiple levels of components. A closure based action system helps address this.
Instead of relying on bubbling, a closure action wraps an action from the current context's
actions
hash in a function that will call it on that context. For example:
// app/components/my-button/component.js
export default Ember.Component.extend({
click: function(){
this.attrs.action();
// for enhanced backwards compat, you may also this.sendAction();
}
});
A closure action can also be called by an action handler:
Lastly, closure actions allow for yielding an action to a block. For example:
// app/components/my-form/component.js
export default Ember.Component.extend({
actions: {
reset: function(){
// rollback
}
}
});
With string-based actions, an argument can be passed to the called function. For example:
export default Ember.Component.extend({
actions: {
save: function(model) {
model.save();
}
}
});
Closure actions allow for another opportunity to curry arguments. Arguments set by an element action helper simply add to the end of the arguments list:
// app/index/controller.js
export default Ember.Controller.extend({
actions: {
save: function(model, prefs) {
model.set('prefs', prefs);
model.save();
}
}
});
Multiple arguments can be curried or set at any level.
The target
option may be provided to specify what scope the closure is called
with. For example:
The default target for a closure is always the current scope.
- When routable components land, the current component will be the default target.
- If a controller is the current scope, that controller will also be a default target.
- A route will never be a closure action target. String actions will continue to have their current behavior of bubbling to the route.
A later proposal will determine how actions on a route are passed to a routable component.
Currently {{action
is only used in an element space:
The closure usage is a new, perhaps action
is not the right word. However the two
behaviors are pretty similar in their conceptual behavior.
{{action
in element space attaches an event listener that fires a bubbling action.(action
closes over an action from the current scope so it can be attached via{{action
or passed around and called later.
This confusion should go away as we move to an on-click
event listener pattern,
ala <button on-click={{someClosureAction}}>
.
Additionally, there may be developers who still have {{action someActionName}}
instead
of the quoted version. This is long deprecated, but these apps may see some
unexpected behavior.
Also additionally, some emergent behaviors exist that may not be desired as real APIs. For example, an action being a function means it can be passed directly to event handlers:
{{my-component mouseEnter=(action 'didEnter')}}
The actual API we plan for 2.0 (ideally) is:
{{my-component on-mouse-enter=(action 'didEnter')}}
These behaviors should not be documented, and we should make clear that they rely on behavior that
will be deprecated. A mitigating move is to not proxy actions through to
get
on a component, and only allow them to be accessed on attrs
.
Lastly, default actions may look a bit confusing:
But the quoted string syntax is not being removed.
There is maybe a thing called ref
that solves this same problem. There has also
been discussion of accessing properties on outlet
across all child components
and their layouts, which would allow easy targetting of the top level component.
Interaction with ref
or outlet.
if any..
If {{action
returns a function and {{mut
returns a mutable value, is there a problem
with that inconsistency?