From 4a8c44e35f39a8d6a4a89d2961255fd302edaea4 Mon Sep 17 00:00:00 2001 From: Matthew Beale Date: Wed, 6 May 2015 20:26:40 -0400 Subject: [PATCH] Add improved actions RFC --- active/0000-improved-actions.md | 278 ++++++++++++++++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 active/0000-improved-actions.md diff --git a/active/0000-improved-actions.md b/active/0000-improved-actions.md new file mode 100644 index 0000000000..0eae6853cf --- /dev/null +++ b/active/0000-improved-actions.md @@ -0,0 +1,278 @@ +- 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 }} + +``` + +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 + +``` + +would be written using a closure action as: + +```hbs + +``` + +With the current string-based actions: + +```hbs +{{my-component action="submit"}} +``` + +```js +export default Ember.Component.extend({ + click: function(){ + this.attrs.action === "submit"; + this.sendAction(); // submit action + } +}); +``` + +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(){ + typeof this.attrs.action === "function"; + this.sendAction(); // submit action + this.attrs.action(); // submit action + } +}); +``` + +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/components/my-button/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 bubbleing 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 }} + +``` + +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}}} + + {{! ^ goes to my-form's save attr property, which + is the submit action on the outer scope }} + + {{! ^ goes to my-form }} + + {{! ^ goes to outer scope }} +{{/my-form}} +``` + +```hbs +{{! app/components/my-form/template.hbs }} +{{yield save (action 'reset')}} +``` + +```js +// app/components/my-form/component.js +export default Ember.Component.extend({ + actions: { + reset: function(){ + // rollback + } + } +}); +``` + +# Drawbacks + +Currently `{{action` is only used in an element space: + +```hbs + +``` + +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 an element space attaches an action from the current scope to an event on the element, +* `(action` closes over an action from the current scope so it can be attached as an action later. + +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?