Skip to content

Commit

Permalink
Expression RFC
Browse files Browse the repository at this point in the history
  • Loading branch information
mixonic committed May 17, 2015
1 parent 6815eb2 commit cd74a9b
Showing 1 changed file with 173 additions and 0 deletions.
173 changes: 173 additions & 0 deletions active/0000-expression.md
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.

0 comments on commit cd74a9b

Please sign in to comment.