Skip to content

Commit

Permalink
Merge pull request #17941 from rwjblue/add-fn-helper
Browse files Browse the repository at this point in the history
[FEATURE EMBER_GLIMMER_FN_HELPER] Initial implementation of fn helper.
  • Loading branch information
rwjblue authored Apr 19, 2019
2 parents d63e7f7 + c4b9e3a commit f0816d4
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 4 deletions.
57 changes: 57 additions & 0 deletions packages/@ember/-internals/glimmer/lib/helpers/fn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { HAS_NATIVE_PROXY } from '@ember/-internals/utils';
import { assert } from '@ember/debug';
import { DEBUG } from '@glimmer/env';
import { Arguments, VM } from '@glimmer/runtime';
import { ICapturedArguments } from '@glimmer/runtime/dist/types/lib/vm/arguments';
import { InternalHelperReference } from '../utils/references';

let context: any = null;
if (DEBUG && HAS_NATIVE_PROXY) {
let assertOnProperty = (property: string | number | symbol) => {
assert(
`You accessed \`this.${String(
property
)}\` 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\`.`
);
};

context = new Proxy(
{},
{
get(_target: {}, property: string | symbol) {
assertOnProperty(property);
},

set(_target: {}, property: string | symbol) {
assertOnProperty(property);

return false;
},

has(_target: {}, property: string | symbol) {
assertOnProperty(property);

return false;
},
}
);
}

function fnHelper({ positional }: ICapturedArguments) {
assert(
`You must pass a function as the \`fn\` helpers first argument, you passed ${positional
.at(0)
.value()}`,
typeof positional.at(0).value() === 'function'
);

return () => {
let [fn, ...args] = positional.value();

return fn!['apply'](context, args);
};
}

export default function(_vm: VM, args: Arguments) {
return new InternalHelperReference(fnHelper, args.capture());
}
17 changes: 13 additions & 4 deletions packages/@ember/-internals/glimmer/lib/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { LookupOptions, Owner, setOwner } from '@ember/-internals/owner';
import { lookupComponent, lookupPartial, OwnedTemplateMeta } from '@ember/-internals/views';
import {
EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS,
EMBER_GLIMMER_FN_HELPER,
EMBER_MODULE_UNIFICATION,
} from '@ember/canary-features';
import { assert } from '@ember/debug';
Expand Down Expand Up @@ -32,6 +33,7 @@ import { default as action } from './helpers/action';
import { default as array } from './helpers/array';
import { default as concat } from './helpers/concat';
import { default as eachIn } from './helpers/each-in';
import { default as fn } from './helpers/fn';
import { default as get } from './helpers/get';
import { default as hash } from './helpers/hash';
import { inlineIf, inlineUnless } from './helpers/if-unless';
Expand Down Expand Up @@ -61,7 +63,11 @@ function makeOptions(moduleName: string, namespace?: string): LookupOptions {
};
}

const BUILTINS_HELPERS = {
interface IBuiltInHelpers {
[name: string]: Helper | undefined;
}

const BUILTINS_HELPERS: IBuiltInHelpers = {
if: inlineIf,
action,
array,
Expand All @@ -82,8 +88,13 @@ const BUILTINS_HELPERS = {
'-mount': mountHelper,
'-outlet': outletHelper,
'-assert-implicit-component-helper-argument': componentAssertionHelper,
fn: undefined,
};

if (EMBER_GLIMMER_FN_HELPER) {
BUILTINS_HELPERS.fn = fn;
}

const BUILTIN_MODIFIERS = {
action: { manager: new ActionModifierManager(), state: null },
};
Expand All @@ -96,9 +107,7 @@ export default class RuntimeResolver implements IRuntimeResolver<OwnedTemplateMe
];
private objToHandle = new WeakMap<any, number>();

private builtInHelpers: {
[name: string]: Helper | undefined;
} = BUILTINS_HELPERS;
private builtInHelpers: IBuiltInHelpers = BUILTINS_HELPERS;

private builtInModifiers: {
[name: string]: ModifierDefinition;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
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';

if (EMBER_GLIMMER_FN_HELPER) {
moduleFor(
'Helpers test: {{fn}}',
class extends RenderingTestCase {
beforeEach() {
this.registerHelper('invoke', function([fn]) {
return fn();
});

let testContext = this;
this.registerComponent('stash', {
ComponentClass: Component.extend({
init() {
this._super(...arguments);
testContext.stashedFn = this.stashedFn;
},
}),
});
}

'@test updates when arguments change'() {
this.render(`{{invoke (fn this.myFunc this.arg1 this.arg2)}}`, {
myFunc(arg1, arg2) {
return `arg1: ${arg1}, arg2: ${arg2}`;
},

arg1: 'foo',
arg2: 'bar',
});

this.assertText('arg1: foo, arg2: bar');

this.assertStableRerender();

runTask(() => set(this.context, 'arg1', 'qux'));
this.assertText('arg1: qux, arg2: bar');

runTask(() => set(this.context, 'arg2', 'derp'));
this.assertText('arg1: qux, arg2: derp');

runTask(() => {
set(this.context, 'arg1', 'foo');
set(this.context, 'arg2', 'bar');
});

this.assertText('arg1: foo, arg2: bar');
}

'@test updates when the function changes'() {
let func1 = (arg1, arg2) => `arg1: ${arg1}, arg2: ${arg2}`;
let func2 = (arg1, arg2) => `arg2: ${arg2}, arg1: ${arg1}`;

this.render(`{{invoke (fn this.myFunc this.arg1 this.arg2)}}`, {
myFunc: func1,

arg1: 'foo',
arg2: 'bar',
});

this.assertText('arg1: foo, arg2: bar');
this.assertStableRerender();

runTask(() => set(this.context, 'myFunc', func2));
this.assertText('arg2: bar, arg1: foo');

runTask(() => set(this.context, 'myFunc', func1));
this.assertText('arg1: foo, arg2: bar');
}

'@test a stashed fn result update arguments when invoked'(assert) {
this.render(`{{stash stashedFn=(fn this.myFunc this.arg1 this.arg2)}}`, {
myFunc(arg1, arg2) {
return `arg1: ${arg1}, arg2: ${arg2}`;
},

arg1: 'foo',
arg2: 'bar',
});

assert.equal(this.stashedFn(), 'arg1: foo, arg2: bar');

runTask(() => set(this.context, 'arg1', 'qux'));
assert.equal(this.stashedFn(), 'arg1: qux, arg2: bar');

runTask(() => set(this.context, 'arg2', 'derp'));
assert.equal(this.stashedFn(), 'arg1: qux, arg2: derp');

runTask(() => {
set(this.context, 'arg1', 'foo');
set(this.context, 'arg2', 'bar');
});

assert.equal(this.stashedFn(), 'arg1: foo, arg2: bar');
}

'@test a stashed fn result invokes the correct function when the bound function changes'(
assert
) {
let func1 = (arg1, arg2) => `arg1: ${arg1}, arg2: ${arg2}`;
let func2 = (arg1, arg2) => `arg2: ${arg2}, arg1: ${arg1}`;

this.render(`{{stash stashedFn=(fn this.myFunc this.arg1 this.arg2)}}`, {
myFunc: func1,

arg1: 'foo',
arg2: 'bar',
});

assert.equal(this.stashedFn(), 'arg1: foo, arg2: bar');

runTask(() => set(this.context, 'myFunc', func2));
assert.equal(this.stashedFn(), 'arg2: bar, arg1: foo');

runTask(() => set(this.context, 'myFunc', func1));
assert.equal(this.stashedFn(), 'arg1: foo, arg2: bar');
}

'@test asserts if the first argument is not a function'() {
expectAssertion(() => {
this.render(`{{invoke (fn this.myFunc this.arg1 this.arg2)}}`, {
myFunc: null,
arg1: 'foo',
arg2: 'bar',
});
}, /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'() {
this.render(`{{stash stashedFn=(fn this.myFunc this.arg1)}}`, {
myFunc(arg1) {
return `arg1: ${arg1}, arg2: ${this.arg2}`;
},

arg1: 'foo',
arg2: 'bar',
});

expectAssertion(() => {
this.stashedFn();
}, /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 can use `this` if bound prior to passing to fn'(assert) {
this.render(`{{stash stashedFn=(fn (action this.myFunc) this.arg1)}}`, {
myFunc(arg1) {
return `arg1: ${arg1}, arg2: ${this.arg2}`;
},

arg1: 'foo',
arg2: 'bar',
});

assert.equal(this.stashedFn(), 'arg1: foo, arg2: bar');
}
}
);
}
2 changes: 2 additions & 0 deletions packages/@ember/canary-features/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +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,
};

/**
Expand Down Expand Up @@ -88,3 +89,4 @@ export const EMBER_ROUTING_BUILD_ROUTEINFO_METADATA = featureValue(
FEATURES.EMBER_ROUTING_BUILD_ROUTEINFO_METADATA
);
export const EMBER_NATIVE_DECORATOR_SUPPORT = featureValue(FEATURES.EMBER_NATIVE_DECORATOR_SUPPORT);
export const EMBER_GLIMMER_FN_HELPER = featureValue(FEATURES.EMBER_GLIMMER_FN_HELPER);

0 comments on commit f0816d4

Please sign in to comment.