diff --git a/active/0000-expression.md b/active/0000-expression.md new file mode 100644 index 0000000000..9c74997f80 --- /dev/null +++ b/active/0000-expression.md @@ -0,0 +1,174 @@ +- 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 may 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')}} + +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 + +``` + +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.