Skip to content

Commit

Permalink
Implementation for RFC #756
Browse files Browse the repository at this point in the history
  • Loading branch information
NullVoxPopuli committed Jan 13, 2022
1 parent e9e592e commit cf7f907
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,133 @@ class HelperManagerTest extends RenderTest {
this.assertHTML('hello');
}

@test '(Default Helper Manager) plain functions work as helpers'(assert: Assert) {
let count = 0;

const hello = () => {
count++;
return 'plain function';
};

const Main = defineComponent({ hello }, '{{hello}}');

this.renderComponent(Main);

assert.equal(count, 1, 'rendered once');
this.assertHTML('plain function');

this.rerender();

assert.equal(count, 1, 'rendered once');
this.assertHTML('plain function');
}

@test '(Default Helper Manager) plain functions track positional args'(assert: Assert) {
let count = 0;

let obj = (x: string) => {
count++;
return x;
};
let args = trackedObj({ value: 'hello', unused: 'unused' });

this.renderComponent(defineComponent({ obj }, '{{obj @value @unused}}'), args);

assert.equal(count, 1, 'rendered once');
this.assertHTML('hello');

args.value = 'there';
this.rerender();

assert.equal(count, 2, 'rendered twice');
this.assertHTML('there');

args.unused = 'unused2';
this.rerender();

assert.equal(count, 3, 'rendered thrice');
this.assertHTML('there');
}

@test '(Default Helper Manager) plain functions do not track unused named args'(assert: Assert) {
let count = 0;

let obj = (x: string, _options: Record<string, unknown>) => {
count++;
return x;
};
let args = trackedObj({ value: 'hello', unused: 'unused' });

this.renderComponent(defineComponent({ obj }, '{{obj @value namedOpt=@unused}}'), args);
assert.equal(count, 1, 'rendered once');
this.assertHTML('hello');

args.unused = 'unused2';
this.rerender();

assert.equal(count, 1, 'rendered once');
this.assertHTML('hello');
}

@test '(Default Helper Manager) plain functions tracked used named args'(assert: Assert) {
let count = 0;

let obj = (_x: string, options: Record<string, unknown>) => {
count++;
return options.namedOpt;
};

let args = trackedObj({ value: 'hello', used: 'used' });

this.renderComponent(defineComponent({ obj }, '{{obj @value namedOpt=@used}}'), args);
assert.equal(count, 1, 'rendered once');
this.assertHTML('used');

args.used = 'there';
this.rerender();

assert.equal(count, 2, 'rendered twice');
this.assertHTML('there');
}

@test '(Default Helper Manager) plain function helpers can have default values (missing data)'(
assert: Assert
) {
let count = 0;
let obj = (x = 'default value') => {
count++;
return x;
};

let args = trackedObj({});

this.renderComponent(defineComponent({ obj }, 'result: {{obj}}'), args);
this.assertHTML('result: default value');
assert.equal(count, 1, 'rendered once');
}

@test '(Default Helper Manager) plain function helpers can have overwritten default values'(
assert: Assert
) {
let count = 0;
let obj = (x = 'default value') => {
count++;
return x;
};

let args = trackedObj({ value: undefined });

this.renderComponent(defineComponent({ obj }, 'result: {{obj @value}}'), args);
this.assertHTML('result: default value');
assert.equal(count, 1, 'rendered once');

args.value = 'value';
this.rerender();

this.assertHTML('result: value');
assert.equal(count, 2, 'rendered twice');
}

@test 'tracks changes to named arguments'(assert: Assert) {
let count = 0;

Expand Down
50 changes: 50 additions & 0 deletions packages/@glimmer/manager/lib/internal/defaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { buildCapabilities } from '../util/capabilities';

import type { CapturedArguments as Arguments, HelperCapabilities } from '@glimmer/interfaces';

type FnArgs<Args extends Arguments = Arguments> =
| [...Args['positional'], Args['named']]
| [...Args['positional'], {}];

interface FunctionHelperState<Args extends Arguments = Arguments> {
fn: <Return>(...args: FnArgs<Args>) => Return;
args: Args;
}

export class FunctionHelperManager<State extends FunctionHelperState> {
capabilities = buildCapabilities({
hasValue: true,
hasDestroyable: false,
hasScheduledEffect: false,
}) as HelperCapabilities;

createHelper(fn: State['fn'], args: State['args']) {
return { fn, args };
}

getValue({ fn, args }: State) {
let namedKeys = Object.keys(args.named).length;

if (args.positional.length === 0 && namedKeys === 0) {
return fn();
}

// This consumes all positional args
let argsForFn: FnArgs<Arguments> = [...args.positional];

if (namedKeys > 0) {
// This consumes all named args
argsForFn.push(args.named);
}

return fn(...argsForFn);
}

getDebugName(fn: State['fn']) {
if (fn.name) {
return `(helper function ${fn.name})`;
}

return '(anonymous helper function)';
}
}
11 changes: 10 additions & 1 deletion packages/@glimmer/manager/lib/internal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Owner,
} from '@glimmer/interfaces';
import { CustomHelperManager } from '../public/helper';
import { FunctionHelperManager } from './defaults';

type InternalManager =
| InternalComponentManager
Expand Down Expand Up @@ -119,6 +120,8 @@ export function setInternalHelperManager<T extends object, O extends Owner>(
return setManager(HELPER_MANAGERS, manager, definition);
}

const DEFAULT_MANAGER = new CustomHelperManager(() => new FunctionHelperManager());

export function getInternalHelperManager(definition: object): CustomHelperManager | Helper;
export function getInternalHelperManager(
definition: object,
Expand All @@ -138,7 +141,13 @@ export function getInternalHelperManager(
);
}

const manager = getManager(HELPER_MANAGERS, definition);
let manager = getManager(HELPER_MANAGERS, definition);

// Functions are special-cased because functions are defined
// as the "default" helper, per: https://github.com/emberjs/rfcs/pull/756
if (manager === undefined && typeof definition === 'function') {
manager = DEFAULT_MANAGER;
}

if (manager) {
return manager;
Expand Down
17 changes: 16 additions & 1 deletion packages/@glimmer/manager/test/managers-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,11 +183,26 @@ module('Managers', () => {
assert.ok(typeof instance === 'object', 'manager is an internal manager');
assert.ok(
typeof instance.getHelper({}) === 'function',
'manager can generatew helper function'
'manager can generate helper function'
);
assert.equal(instance['factory'], factory, 'manager has correct delegate factory');
});

test('it determines the default manager', (assert) => {
let myTestHelper = () => 0;
let instance = getInternalHelperManager(myTestHelper) as CustomHelperManager<object>;

assert.ok(typeof instance === 'object', 'manager is an internal manager');
assert.ok(
typeof instance.getHelper({}) === 'function',
'manager can generate helper function'
);
assert.strictEqual(
instance['factory']({})?.getDebugName?.(myTestHelper),
'(helper function myTestHelper)'
);
});

test('it works with internal helpers', (assert) => {
let helper = () => {
return UNDEFINED_REFERENCE;
Expand Down

0 comments on commit cf7f907

Please sign in to comment.