-
-
Notifications
You must be signed in to change notification settings - Fork 407
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
173 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,173 @@ | ||
- Start Date: 2015-05-17 | ||
- RFC PR: (leave this empty) | ||
- Ember Issue: (leave this empty) | ||
|
||
# Summary | ||
|
||
`Expression` is a new type of object in Ember.js, one that fills the gap | ||
between helpers (pure functions) and components (stateful, dom-managing | ||
instances with a lifecycle). Expressions: | ||
|
||
* Have a single return value, and cannot manage DOM | ||
* Can store and read state | ||
* Have lifecycle hooks analagous to components where appropriate. For | ||
example, an expression call `recompute` at any time to generate a new value. | ||
|
||
To use an expression, define it like any other namespaced factory in an | ||
Ember-CLI app: | ||
|
||
```js | ||
// app/expressions/full-name.js | ||
import Ember from "ember"; | ||
|
||
export default Ember.Expression.extend({ | ||
nameBuilder: Ember.inject.service() | ||
positionalParams: ['firstName', 'lastName'], | ||
value() { | ||
const builder = this.get('nameBuilder'); | ||
return builder.fullName(this.attrs.firstName, this.attrs.lastName); | ||
} | ||
}); | ||
``` | ||
|
||
Use an expression anywhere a subexpression is valid: | ||
|
||
```hbs | ||
{{full-name 'Bigtime' 'Beagle'}} | ||
{{input value=(full-name 'Gyro' 'Gearloose') readonly=true}} | ||
``` | ||
|
||
# Motivation | ||
|
||
Helpers in Ember are pure functions. This make them easy to reason about, but | ||
also overly simplistic. It is difficult to write a helper that recomputes due to | ||
something besides the change of its input, and helpers have no API for | ||
accessing parts of Ember like services. | ||
|
||
Specifically, this addresses many of the concerns in | ||
[emberjs/ember.js#11080](https://github.com/emberjs/ember.js/issues/11080). | ||
Libraries such as [yahoo/ember-intl](https://github.com/yahoo/ember-intl), | ||
[dockyard/ember-cli-i18n](https://github.com/dockyard/ember-cli-i18n), and | ||
[minutebase/ember-can](https://github.com/minutebase/ember-can) will be | ||
provided a viable public API to couple to. | ||
|
||
# Detailed design | ||
|
||
Expressions represent a stream of values generated by calling the `value` hook. | ||
A new value is generated upon each use whether it is triggered by a change | ||
to the passed `attrs`, or via a manual call to `this.recompute`. | ||
|
||
Expressions must have a `-` in their name. | ||
|
||
### Consuming an expression | ||
|
||
Expressions can be consumed anywhere an HTMLBars subexpression can be used. For | ||
example: | ||
|
||
```hbs | ||
{{#if (can-access 'admin')}} | ||
{{link-to 'login'}} | ||
{{/if}} | ||
{{my-login-button isAdmin=(can-access 'admin')}} | ||
<my-login-button isAdmin={{can-access 'admin'}} /> | ||
Can access? {{can-access 'admin'}} | ||
``` | ||
|
||
In all these cases, the expression is considered one-way. That is, it is a | ||
readable value and not two-way bound (toggling `isAdmin` does not change the | ||
expression). | ||
|
||
Let's step through exactly what happens when using an expression like this: | ||
|
||
```hbs | ||
<my-login-button isAdmin={{can-access 'admin'}} /> | ||
``` | ||
|
||
Upon initial render: | ||
|
||
* The expression `can-access` is looked up on the container | ||
* The expression is initialized (`init` is called) | ||
* `this.attrs` is set on the expression. Any `positionalParams` (in this case one | ||
mapping the `"admin"` value) are set. | ||
* The `value` function is called on the expression. | ||
* The expression instance remains in memory | ||
|
||
Upon recompute: | ||
|
||
* Any changes to `attrs` are applied, then the `value()` hooks is called again | ||
to generate the new value. | ||
|
||
Upon teardown: | ||
|
||
* The expressions is destroyed, calling the `destroy` method. | ||
|
||
### Defining an expression | ||
|
||
Expressions are similar in design to components, but do not have DOM or all the | ||
lifecycle hooks of a component. For example: | ||
|
||
```js | ||
// app/expressions/can-access.js | ||
import Ember from "ember"; | ||
|
||
export default Ember.Expression.extend({ | ||
// Same API as components: | ||
session: Ember.inject.service() | ||
positionalParams: ['accessRequest'], | ||
|
||
// Still very similar to components: | ||
currentUser: Ember.computed.reads('session.currentUser'), | ||
recomputeForNewUser: Ember.observes('currentUser', function(){ | ||
this.recompute(); | ||
}), | ||
|
||
// Instead of hooks, just call value | ||
value() { | ||
const currentUser = this.get('currentUser'); | ||
return currentUser.can(this.attrs.accessRequest); | ||
} | ||
}); | ||
``` | ||
|
||
However instead of lifecycle hooks and a template, the `value` function is | ||
called and its return value returned from the subexpression. | ||
|
||
# Drawbacks | ||
|
||
Expressions fill a specific gap in the APIs powering Ember templates. | ||
|
||
|has positional params|has dom|has lifecycle, instance|can control rerender | ||
---|---|---|---|--- | ||
components|Yes|Yes|Yes|Yes | ||
helpers|Yes|No|No|No | ||
expressions|Yes|No|Yes|Yes | ||
|
||
Adding a new concept to Ember is not something we are normally excited about. | ||
It adds the the learning curve of the framework. | ||
|
||
On the other hand, it is plausible that expressions might replace helpers. They | ||
need only a small amount of additional API to be a viable replacement for most | ||
uses, even if they would remain more boilerplate. | ||
|
||
# Alternatives | ||
|
||
It is plausible that helpers might be extended to better perform this role. For | ||
example, helpers could be provided the container directly, or be wrapped in an | ||
object the allows them to be configured in some useful manner. | ||
|
||
# Unresolved questions | ||
|
||
Expressions here are one-way. Is it possible to have a two-way expression (data | ||
flowing upward like a mut)? | ||
|
||
Perhaps there should be hooks in place for the lifecycle, instead of relying on | ||
`init` and `destroy`. | ||
|
||
It has been suggested by @wycats that there should be an simpler way to declare | ||
a recomputation dependency on a non-attr (`session.currentUser` for example). | ||
I'm thinking of this as sugar on top of the patterns described here. | ||
|
||
In this proposal I use `positionalParams`, aligning the API with what already | ||
exists for components. It has been suggested that `this.params` would | ||
suffice. The API should likely remain constant across components and | ||
expressions regardless of which direction we choose. |