Skip to content

Commit

Permalink
[FEATURE] Enable fn helper by default. (#17965)
Browse files Browse the repository at this point in the history
[FEATURE] Enable fn helper by default.
  • Loading branch information
rwjblue authored Apr 25, 2019
2 parents 10dc21d + ab381ee commit c21c417
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 22 deletions.
73 changes: 73 additions & 0 deletions packages/@ember/-internals/glimmer/lib/helpers/fn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,79 @@ import { InternalHelperReference, INVOKE } from '../utils/references';
import buildUntouchableThis from '../utils/untouchable-this';

const context = buildUntouchableThis('`fn` helper');

/**
@module ember
*/

/**
The `fn` helper allows you to ensure a function that you are passing off
to another component, helper, or modifier has access to arguments that are
available in the template.
For example, if you have an `each` helper looping over a number of items, you
may need to pass a function that expects to receive the item as an argument
to a component invoked within the loop. Here's how you could use the `fn`
helper to pass both the function and its arguments together:
```app/templates/components/items-listing.hbs
{{#each @items as |item|}}
<DisplayItem @item=item @select={{fn this.handleSelected item}} />
{{/each}}
```
```app/components/items-list.js
import Component from '@glimmer/component';
import { action } from '@ember/object';
export default class ItemsList extends Component {
@action
handleSelected(item) {
// ...snip...
}
}
```
In this case the `display-item` component will receive a normal function
that it can invoke. When it invokes the function, the `handleSelected`
function will receive the `item` and any arguments passed, thanks to the
`fn` helper.
Let's take look at what that means in a couple circumstances:
- When invoked as `this.args.select()` the `handleSelected` function will
receive the `item` from the loop as its first and only argument.
- When invoked as `this.args.selected('foo')` the `handleSelected` function
will receive the `item` from the loop as its first argument and the
string `'foo'` as its second argument.
In the example above, we used `@action` to ensure that `handleSelected` is
properly bound to the `items-list`, but let's explore what happens if we
left out `@action`:
```app/components/items-list.js
import Component from '@glimmer/component';
export default class ItemsList extends Component {
handleSelected(item) {
// ...snip...
}
}
```
In this example, when `handleSelected` is invoked inside the `display-item`
component, it will **not** have access to the component instance. In other
words, it will have no `this` context, so please make sure your functions
are bound (via `@action` or other means) before passing into `fn`!
See also [partial application](https://en.wikipedia.org/wiki/Partial_application).
@method fn
@for Ember.Templates.helpers
@public
@since 3.11.0
*/

function fnHelper({ positional }: ICapturedArguments) {
let callbackRef = positional.at(0);

Expand Down
8 changes: 4 additions & 4 deletions packages/@ember/-internals/glimmer/lib/helpers/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,16 @@ import { CachedReference, referenceFromParts, UPDATE } from '../utils/references
```handlebars
{{get person factName}}
<button {{action (action (mut factName)) "height"}}>Show height</button>
<button {{action (action (mut factName)) "weight"}}>Show weight</button>
<button {{action (fn (mut factName)) "height"}}>Show height</button>
<button {{action (fn (mut factName)) "weight"}}>Show weight</button>
```
The `{{get}}` helper can also respect mutable values itself. For example:
```handlebars
{{input value=(mut (get person factName)) type="text"}}
<button {{action (action (mut factName)) "height"}}>Show height</button>
<button {{action (action (mut factName)) "weight"}}>Show weight</button>
<button {{action (fn (mut factName)) "height"}}>Show height</button>
<button {{action (fn (mut factName)) "weight"}}>Show weight</button>
```
Would allow the user to swap what fact is being displayed, and also edit
Expand Down
24 changes: 11 additions & 13 deletions packages/@ember/-internals/glimmer/lib/helpers/mut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { INVOKE, UPDATE } from '../utils/references';
To specify that a parameter is mutable, when invoking the child `Component`:
```handlebars
<MyChild @childClickCount={{action (mut totalClicks)}} />
<MyChild @childClickCount={{fn (mut totalClicks)}} />
```
or
Expand All @@ -37,20 +37,20 @@ import { INVOKE, UPDATE } from '../utils/references';
Note that for curly components (`{{my-component}}`) the bindings are already mutable,
making the `mut` unnecessary.
Additionally, the `mut` helper can be combined with the `action` helper to
Additionally, the `mut` helper can be combined with the `fn` helper to
mutate a value. For example:
```handlebars
<MyChild @childClickCount={{this.totalClicks}} @click-count-change={{action (mut totalClicks))}} />
<MyChild @childClickCount={{this.totalClicks}} @click-count-change={{fn (mut totalClicks))}} />
```
or
```handlebars
{{my-child childClickCount=totalClicks click-count-change=(action (mut totalClicks))}}
{{my-child childClickCount=totalClicks click-count-change=(fn (mut totalClicks))}}
```
The child `Component` would invoke the action with the new click value:
The child `Component` would invoke the function with the new click value:
```javascript
// my-child.js
Expand All @@ -61,25 +61,23 @@ import { INVOKE, UPDATE } from '../utils/references';
});
```
The `mut` helper changes the `totalClicks` value to what was provided as the action argument.
The `mut` helper changes the `totalClicks` value to what was provided as the `fn` argument.
The `mut` helper, when used with `action`, will return a function that
sets the value passed to `mut` to its first argument. This works like any other
closure action and interacts with the other features `action` provides.
As an example, we can create a button that increments a value passing the value
directly to the `action`:
The `mut` helper, when used with `fn`, will return a function that
sets the value passed to `mut` to its first argument. As an example, we can create a
button that increments a value passing the value directly to the `fn`:
```handlebars
{{! inc helper is not provided by Ember }}
<button onclick={{action (mut count) (inc count)}}>
<button onclick={{fn (mut count) (inc count)}}>
Increment count
</button>
```
You can also use the `value` option:
```handlebars
<input value={{name}} oninput={{action (mut name) value="target.value"}}>
<input value={{name}} oninput={{fn (mut name) value="target.value"}}>
```
@method mut
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { EMBER_GLIMMER_FN_HELPER } from '@ember/canary-features';
import { Component } from '../../utils/helpers';
import { RenderingTestCase, moduleFor, runTask } from 'internal-test-helpers';

import { set } from '@ember/-internals/metal';
import { HAS_NATIVE_PROXY } from '@ember/-internals/utils';
import { DEBUG } from '@glimmer/env';
import { RenderingTestCase, moduleFor, runTask } from 'internal-test-helpers';
import { Component } from '../../utils/helpers';

if (EMBER_GLIMMER_FN_HELPER) {
moduleFor(
Expand Down Expand Up @@ -131,7 +132,14 @@ if (EMBER_GLIMMER_FN_HELPER) {
}, /You must pass a function as the `fn` helpers first argument, you passed null/);
}

'@test asserts if the provided function accesses `this` without being bound prior to passing to fn'() {
'@test asserts if the provided function accesses `this` without being bound prior to passing to fn'(
assert
) {
if (!HAS_NATIVE_PROXY) {
assert.expect(0);
return;
}

this.render(`{{stash stashedFn=(fn this.myFunc this.arg1)}}`, {
myFunc(arg1) {
return `arg1: ${arg1}, arg2: ${this.arg2}`;
Expand All @@ -146,6 +154,21 @@ if (EMBER_GLIMMER_FN_HELPER) {
}, /You accessed `this.arg2` from a function passed to the `fn` helper, but the function itself was not bound to a valid `this` context. Consider updating to usage of `@action`./);
}

'@test there is no `this` context within the callback'(assert) {
if (DEBUG && HAS_NATIVE_PROXY) {
assert.expect(0);
return;
}

this.render(`{{stash stashedFn=(fn this.myFunc this.arg1)}}`, {
myFunc() {
assert.strictEqual(this, null, 'this is bound to null in production builds');
},
});

this.stashedFn();
}

'@test can use `this` if bound prior to passing to fn'(assert) {
this.render(`{{stash stashedFn=(fn (action this.myFunc) this.arg1)}}`, {
myFunc(arg1) {
Expand Down
2 changes: 1 addition & 1 deletion packages/@ember/canary-features/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const DEFAULT_FEATURES = {
EMBER_GLIMMER_ANGLE_BRACKET_NESTED_LOOKUP: true,
EMBER_ROUTING_BUILD_ROUTEINFO_METADATA: true,
EMBER_NATIVE_DECORATOR_SUPPORT: true,
EMBER_GLIMMER_FN_HELPER: null,
EMBER_GLIMMER_FN_HELPER: true,
EMBER_CUSTOM_COMPONENT_ARG_PROXY: null,
EMBER_GLIMMER_ON_MODIFIER: null,
};
Expand Down
1 change: 1 addition & 0 deletions tests/docs/expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ module.exports = {
'findModel',
'findWithAssert',
'firstObject',
'fn',
'focusIn',
'focusOut',
'followRedirects',
Expand Down

0 comments on commit c21c417

Please sign in to comment.