-
-
Notifications
You must be signed in to change notification settings - Fork 408
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
359 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,359 @@ | ||
- Start Date: 2014-05-06 | ||
- RFC PR: (leave this empty) | ||
- Ember Issue: (leave this empty) | ||
|
||
# Summary | ||
|
||
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](http://emberjs.jsbin.com/rwjblue/223/edit?html,js,output) | ||
for a demonstration of some of these ideas. | ||
|
||
# Motivation | ||
|
||
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. | ||
|
||
# Detailed design | ||
|
||
First, the existing uses of `{{action` will be maintained. An action can be attached to an | ||
element by using the helper in element space: | ||
|
||
```hbs | ||
{{! app/index/template.hbs }} | ||
{{! submit action will hit immediate parent }} | ||
<button {{action "submit"}}>Save</button> | ||
``` | ||
|
||
An action can be passed to a component as a string: | ||
|
||
```hbs | ||
{{! app/index/template.hbs }} | ||
{{my-button on-click="submit"}} | ||
``` | ||
|
||
```js | ||
// app/components/my-button/component.js | ||
export default Ember.Component.extend({ | ||
click: function(){ | ||
this.sendAction('on-click'); | ||
} | ||
}); | ||
``` | ||
|
||
Or a default action can be passed: | ||
|
||
```hbs | ||
{{! app/index/template.hbs }} | ||
{{my-button action="submit"}} | ||
``` | ||
|
||
```js | ||
// 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. | ||
|
||
### Creating closure actions | ||
|
||
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: | ||
|
||
```hbs | ||
<button {{action "submit" on="click"}}>Save</button> | ||
``` | ||
|
||
Would be written using a closure action as: | ||
|
||
```hbs | ||
<button {{action (action "submit") on="click"}}>Save</button> | ||
``` | ||
|
||
The functionality is exactly the same as the string-based action example. | ||
How does that happen? | ||
|
||
* `(action "submit")` reads the `submit` function off the current scope's | ||
`actions.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: | ||
|
||
```hbs | ||
{{my-component action="submit"}} | ||
``` | ||
|
||
```js | ||
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")}} | ||
``` | ||
|
||
```js | ||
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: | ||
|
||
```js | ||
// app/index/controller.js | ||
export default Ember.Controller.extend({ | ||
actions: { | ||
submit: function(){ | ||
// some submission task | ||
} | ||
} | ||
}); | ||
``` | ||
|
||
```hbs | ||
{{! app/index/template.hbs }} | ||
{{my-button save=(action 'submit')}} | ||
``` | ||
|
||
```js | ||
// 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'); | ||
} | ||
}); | ||
``` | ||
|
||
### Hole punching with a closure-based action | ||
|
||
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: | ||
|
||
```hbs | ||
{{! app/index/template.hbs }} | ||
{{my-form submit=(action 'submit')}} | ||
``` | ||
|
||
```hbs | ||
{{! app/components/my-form/template.hbs }} | ||
{{my-button on-click=submit}} | ||
``` | ||
|
||
```hbs | ||
{{! app/components/my-button/template.hbs }} | ||
{{my-button action=on-click}} | ||
``` | ||
|
||
```js | ||
// 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: | ||
|
||
```hbs | ||
{{! app/index/template.hbs }} | ||
{{my-form submit=(action 'submit')}} | ||
``` | ||
|
||
```hbs | ||
{{! app/components/my-form/template.hbs }} | ||
{{my-button on-click=submit}} | ||
``` | ||
|
||
```hbs | ||
{{! app/components/my-button/template.hbs }} | ||
<button {{action on-click}}></button> | ||
``` | ||
|
||
Lastly, closure actions allow for yielding an action to a block. For example: | ||
|
||
```hbs | ||
{{! app/index/template.hbs }} | ||
{{my-form save=(action 'submit') as |submit reset}}} | ||
<button {{action submit}}>Save</button> | ||
{{! ^ goes to my-form's save attr property, which | ||
is the submit action on the outer scope }} | ||
<button {{action reset}}>Reset</button> | ||
{{! ^ goes to my-form }} | ||
<button {{action "cancel"}}>Cancel</button> | ||
{{! ^ goes to outer scope }} | ||
{{/my-form}} | ||
``` | ||
|
||
```hbs | ||
{{! app/components/my-form/template.hbs }} | ||
{{yield attrs.save (action 'reset')}} | ||
``` | ||
|
||
```js | ||
// app/components/my-form/component.js | ||
export default Ember.Component.extend({ | ||
actions: { | ||
reset: function(){ | ||
// rollback | ||
} | ||
} | ||
}); | ||
``` | ||
|
||
### Currying arguments with a closure-based action | ||
|
||
With string-based actions, an argument can be passed to the called function. For | ||
example: | ||
|
||
```hbs | ||
<button {{action "save" model}}></button> | ||
``` | ||
|
||
```js | ||
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: | ||
|
||
```hbs | ||
{{! app/index/template.hbs }} | ||
{{my-component save=(action "save" model)}} | ||
``` | ||
|
||
```hbs | ||
{{! app/components/my-component/template.hbs }} | ||
<button {{action attrs.save prefs}}></button> | ||
``` | ||
|
||
```js | ||
// 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. | ||
|
||
### Re-targeting the scope of a closure action | ||
|
||
The `target` option may be provided to specify what scope the closure is called | ||
with. For example: | ||
|
||
```hbs | ||
{{! app/index/template.hbs }} | ||
<my-component on-click={{action "save" model target=someComponentInstance}}></my-component> | ||
``` | ||
|
||
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. | ||
|
||
# Drawbacks | ||
|
||
Currently `{{action` is only used in an element space: | ||
|
||
```hbs | ||
<button {{action "booyah"}}>Fire</button> | ||
``` | ||
|
||
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: | ||
|
||
```hbs | ||
{{my-button action=(action 'action')}} | ||
{{! ^ this is valid }} | ||
``` | ||
|
||
But the quoted string syntax is not being removed. | ||
|
||
# Alternatives | ||
|
||
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. | ||
|
||
# Unresolved questions | ||
|
||
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? |