Summary
Ember.js 1.13 will introduce a new API for helpers. Helpers will come in two flavors:
Helpers are a class-based way to define HTMLBars subexpressions. Helpers:
- Have a single return value.
- Must have a dash in their name.
- Cannot be used as a block (
{{#some-helper}}{{/some-helper}}
). - Can store and read state.
- Have lifecycle hooks analogous to components where appropriate. For
example, a helper may call
recompute
at any time to generate a new value (this is akin torerender
). - Are a superset of shorthand helpers, the function-based syntax described below. They can do more, but in many cases a shorthand helper is appropriate.
Shorthand helpers are a function-based way to define HTMLBars subexpressions. Helpers written this way:
- Have all the limitations of regular helpers.
- Have no instance associated with them, cannot store or read state.
- Have no lifecycle hooks. The function is simply re-computed when any input changes.
These improved helpers fill a gap in Ember's current template APIs:
has positional params | has layout (shadow DOM) | can yield template | has lifecycle, instance | can control rerender | |
---|---|---|---|---|---|
components | Yes | Yes | Yes | Yes | Yes |
helpers | Yes | No | No | Yes | Yes |
shorthand helpers | Yes | No | No | No | No |
An example helper:
// app/helpers/full-name.js
import Ember from "ember";
export default Ember.Helper.extend({
nameBuilder: Ember.inject.service(),
compute(params) {
const builder = this.get('nameBuilder');
return builder.fullName(params[0], params[1]);
}
});
An example shorthand helper:
// app/helpers/full-name.js
import Ember from "ember";
export default Ember.Helper.helper(function(params, hash) {
let fullName = params.join(' ');
if (hash.honorific) {
fullName = `${hash.honorific} ${fullName}`
}
return fullName;
});
Helpers can be used anywhere an HTMLBars subexpression is valid:
{{full-name 'Bigtime' 'Beagle'}}
{{input value=(full-name 'Gyro' 'Gearloose') readonly=true}}
{{#if (eq (full-name 'Webbigail' 'Vanderquack') selectedFullName))}}
You have chosen wisely.
{{/if}}
Motivation
Ember.js 1.13 make a private API change that removed the ability to access
application containers. Ember.HTMLBars._registerHelper
was previously passed
the env
object, and this was removed as it is an internal implementation
detail.
Ember's helper API has not kept pace with improvements possible after the introduction of HTMLBars. This has resulted in the community using a variety of private APIs, many of which leak information about the outer context of a helpers invocation as well as the render layer implementation.
The current public API is:
This API is sorely lacking in functionality required by addon authors.
- Has no access to other parts of the app, like services
- Leaks a private API for dealing with blocks
- Results in less efficient helpers due to the Handlebars compatibility layer
- Has poor support for hash arguments
Additionally it remains difficult to write a helper that recomputes due to something besides the change of its input.
Specifically, this RFC addresses many of the concerns in emberjs/ember.js#11080. Libraries such as yahoo/ember-intl, dockyard/ember-cli-i18n, and minutebase/ember-can will be provided a viable public API to couple to.
Detailed design
Helpers must have a dash in their name. In an Ember-CLI app, they can be named
according to the app/helpers/full-name.js
convention (app/full-name/helper.js
in pods mode). For a globals app, naming a helper App.FullNameHelper
is
sufficient.
Definition and lifecycle
A helper is defined as a class inheriting from Ember.Helper
. For
example:
// app/helpers/hello-world.js
import Ember from "ember";
// Usage: {{hello-world}}
export default Ember.Helper.extend({
compute() {
return "Hello Helper World";
}
});
Upon initial render:
- The helper instance is created.
- The
compute
method is called. The return value is outputted where the helper is used. For example in<div class={{some-helper}}></div>
the return value is set to the class.
The compute
function is always called with params
(the bare, ordered
arguments) and hash
(the named arguments). For example:
// app/helpers/greet-someone.js
import Ember from "ember";
// Usage: {{greet-someone 'bob' greeting='say hello'}}
export default Ember.Helper.extend({
compute(params, hash) {
return `Hello ${params[0]}, nice to ${hash.greeting}`;
}
});
Which functions the same as this shorthand:
// app/helpers/greet-someone.js
import Ember from "ember";
// Usage: {{greet-someone 'bob' greeting='say hello'}}
export default Ember.Helper.helper(function(params, hash) {
return `Hello ${params[0]}, nice to ${hash.greeting}`;
});
When the params
or hash
contents change, the compute
method is called
again. The instance of the helper is preserved across rerenders of the parent.
A shorthand helper, having no instance, is called every time a bound
argument changes.
The init
and destroy
methods can be subclassed for setup and teardown.
Consuming a helper
Helpers can be used anywhere an HTMLBars subexpression can be used. For example:
{{#if (can-access 'admin')}}
{{link-to 'login'}}
{{/if}}
{{#if (eq (can-access 'admin') false)}}
No login for you
{{/if}}
<my-login-button isAdmin={{can-access 'admin'}} />
Can access? {{can-access 'admin'}}
Passing a helper to a {{
- invoked component skips the auto-mut
behavior:
{{my-login-button isAdmin=(can-access 'admin')}}
Let's step through exactly what happens when using an helper like this:
<my-login-button isAdmin={{can-access 'admin'}} />
Upon initial render:
- The helper
can-access
is looked up on the container - The helper is identified as a full helper, not a shorthand helper function
- The helper is initialized (
init
is called) - The
compute
function is called on the helper. - The return value from
compute
is passed as anattr
tomy-login-button
. - The helper instance remains in memory.
If the parent scope is rerendered:
- The
compute
function is called again. - The return value from
compute
is passed as anattr
tomy-login-button
.
Upon teardown:
- The helper is destroyed, calling the
destroy
method.
Returning a value
The return value of helper is passed through to where their subexpression is called. For example, given a helper (this one a shorthand helper):
// app/helpers/full-name.js
import Ember from "ember";
export default Ember.Helper.helper(function fullName(params, hash) {
return params.join(' ');
}
The following are effectively the same:
<div data-name={{full-name "Fenton" "Crackshell"}}></div>
<div data-name={{"Fenton Crackshell"}}></div>
{{my-component name=(full-name "Magica" "De Spell")}}
{{my-component name="Magica De Spell"}}
<p>{{full-name "Bentina" "Beakley"}}</p>
<p>{{"Bentina Beakley"}}</p>
An exclusion to this pattern is the following form:
<div {{full-name "Webbigail" "Vanderquack"}}></div>
This is a legacy form of mustache usage. Helpers will throw an exception when used in this manner.
Consuming services and recompute
Helpers are a valid target for service injection. For example:
// app/helpers/current-user-name.js
import Ember from "ember";
export default Ember.Helper.extend({
// Same API as components:
session: Ember.inject.service(),
compute() {
return this.get('session.currentUser.name');
}
});
However consuming a property from a service does not bind the data being
displayed to that property. After {{current-user-name}}
has been computed
and rendered, it will never be invalidated.
For this reason, helpers are granted some control over their computation lifecycle. A helper will recompute when:
- A value passed via the template changes (
params
orhash
) - The
recompute
method is called
For example, this helper checks if the current use has access to a resource type:
// app/helpers/can-access.js
import Ember from "ember";
// Usage {{if (can-access 'admin') 'Welcome, boss' 'Heck no!'}}
export default Ember.Helper.extend({
session: Ember.inject.service(),
onCurrentUserChange: Ember.observes('session.currentUser', function() {
this.recompute();
}),
compute(params) {
const currentUser = this.get('session.currentUser');
return currentUser.can(params[0]);
}
});
Drawbacks
Helpers may superficially appear similar to components, but in practice they have none of the special behavior of components such as managing DOM. The intent of this RFC is that full class-based helpers remain very close to the spirit of a pure function (as in the shorthand). However, despite this intent they are a new concept for the framework.
Alternatives
A previous RFC explored creating a new class called Expressions, which would have more closely modeled the API of components (using positional params, attrs). After discussion and consideration it was clear that a third kind of template API would be very challenging to document and teach well.
Unresolved questions
Perhaps there should be hooks in place for the lifecycle, instead of relying on
init
and destroy
.