From 663d2098f8c50b5cb056735b0e185078243538f9 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 24 Apr 2019 13:01:26 -0400 Subject: [PATCH 1/3] Add documentation for `fn`. --- .../-internals/glimmer/lib/helpers/fn.ts | 73 +++++++++++++++++++ .../-internals/glimmer/lib/helpers/get.ts | 8 +- .../-internals/glimmer/lib/helpers/mut.ts | 24 +++--- tests/docs/expected.js | 1 + 4 files changed, 89 insertions(+), 17 deletions(-) diff --git a/packages/@ember/-internals/glimmer/lib/helpers/fn.ts b/packages/@ember/-internals/glimmer/lib/helpers/fn.ts index 6322a4ed63f..205c5525f21 100644 --- a/packages/@ember/-internals/glimmer/lib/helpers/fn.ts +++ b/packages/@ember/-internals/glimmer/lib/helpers/fn.ts @@ -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|}} + + {{/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); diff --git a/packages/@ember/-internals/glimmer/lib/helpers/get.ts b/packages/@ember/-internals/glimmer/lib/helpers/get.ts index 6efd0f5968e..b3782431513 100644 --- a/packages/@ember/-internals/glimmer/lib/helpers/get.ts +++ b/packages/@ember/-internals/glimmer/lib/helpers/get.ts @@ -40,16 +40,16 @@ import { CachedReference, referenceFromParts, UPDATE } from '../utils/references ```handlebars {{get person factName}} - - + + ``` The `{{get}}` helper can also respect mutable values itself. For example: ```handlebars {{input value=(mut (get person factName)) type="text"}} - - + + ``` Would allow the user to swap what fact is being displayed, and also edit diff --git a/packages/@ember/-internals/glimmer/lib/helpers/mut.ts b/packages/@ember/-internals/glimmer/lib/helpers/mut.ts index 8b3f17ac0eb..5ef02420a04 100644 --- a/packages/@ember/-internals/glimmer/lib/helpers/mut.ts +++ b/packages/@ember/-internals/glimmer/lib/helpers/mut.ts @@ -13,7 +13,7 @@ import { INVOKE, UPDATE } from '../utils/references'; To specify that a parameter is mutable, when invoking the child `Component`: ```handlebars - + ``` or @@ -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 - + ``` 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 @@ -61,17 +61,15 @@ 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 }} - ``` @@ -79,7 +77,7 @@ import { INVOKE, UPDATE } from '../utils/references'; You can also use the `value` option: ```handlebars - + ``` @method mut diff --git a/tests/docs/expected.js b/tests/docs/expected.js index 4666e7965e2..bf658c30c21 100644 --- a/tests/docs/expected.js +++ b/tests/docs/expected.js @@ -217,6 +217,7 @@ module.exports = { 'findModel', 'findWithAssert', 'firstObject', + 'fn', 'focusIn', 'focusOut', 'followRedirects', From 4ba941ec47ba33b043b47d4f6414686ea3d936b3 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 24 Apr 2019 13:01:47 -0400 Subject: [PATCH 2/3] [FEATURE] Enable fn helper by default. --- packages/@ember/canary-features/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@ember/canary-features/index.ts b/packages/@ember/canary-features/index.ts index fcd9aa0eb6d..973f83e465f 100644 --- a/packages/@ember/canary-features/index.ts +++ b/packages/@ember/canary-features/index.ts @@ -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, }; From ab381eef4eeb142c08e90e146c93fe7805c4f7bf Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Wed, 24 Apr 2019 15:52:13 -0400 Subject: [PATCH 3/3] Fix up test assertions to avoid issues in IE11. * Guards the test asserting on specific `this` property access (because it relies on native proxies existing) * Adds another test that runs in production builds and IE11 that ensures `this` is `null`. --- .../tests/integration/helpers/fn-test.js | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/fn-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/fn-test.js index 693dfa95b8d..f82b74c4df7 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/helpers/fn-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/fn-test.js @@ -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( @@ -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}`; @@ -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) {