Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Add improved actions RFC
  • Loading branch information
mixonic committed May 7, 2015
1 parent b372664 commit a7e59bc
Showing 1 changed file with 359 additions and 0 deletions.
359 changes: 359 additions & 0 deletions active/0000-improved-actions.md
@@ -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?

0 comments on commit a7e59bc

Please sign in to comment.