From 634c78ff76244638a36fdb9989592bdba8029a5c Mon Sep 17 00:00:00 2001 From: NullVoxPopuli Date: Sat, 16 Oct 2021 13:28:44 -0400 Subject: [PATCH] Implementation for RFC #756 --- .../test/managers/helper-manager-test.ts | 146 ++++++++++++++++++ .../@glimmer/manager/lib/internal/defaults.ts | 42 +++++ .../@glimmer/manager/lib/internal/index.ts | 11 +- .../@glimmer/manager/test/managers-test.ts | 17 +- 4 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 packages/@glimmer/manager/lib/internal/defaults.ts diff --git a/packages/@glimmer/integration-tests/test/managers/helper-manager-test.ts b/packages/@glimmer/integration-tests/test/managers/helper-manager-test.ts index 9f786fcd9b..511b09ace3 100644 --- a/packages/@glimmer/integration-tests/test/managers/helper-manager-test.ts +++ b/packages/@glimmer/integration-tests/test/managers/helper-manager-test.ts @@ -32,6 +32,152 @@ 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 entangle with any tracked data'(assert: Assert) { + let count = 0; + let trackedState = trackedObj({ value: 'hello' }); + + let obj = () => { + count++; + return trackedState.value; + }; + + this.renderComponent(defineComponent({ obj }, '{{obj}}')); + + assert.equal(count, 1, 'rendered once'); + this.assertHTML('hello'); + + trackedState.value = 'there'; + this.rerender(); + 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) => { + 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) => { + 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; diff --git a/packages/@glimmer/manager/lib/internal/defaults.ts b/packages/@glimmer/manager/lib/internal/defaults.ts new file mode 100644 index 0000000000..3a1936d12e --- /dev/null +++ b/packages/@glimmer/manager/lib/internal/defaults.ts @@ -0,0 +1,42 @@ +import { buildCapabilities } from '../util/capabilities'; + +import type { CapturedArguments as Arguments, HelperCapabilities } from '@glimmer/interfaces'; + +type FnArgs = + | [...Args['positional'], Args['named']] + | [...Args['positional']]; + +interface FunctionHelperState { + fn: (...args: FnArgs) => Return; + args: Args; +} + +export class FunctionHelperManager { + capabilities = buildCapabilities({ + hasValue: true, + hasDestroyable: false, + hasScheduledEffect: false, + }) as HelperCapabilities; + + createHelper(fn: State['fn'], args: State['args']) { + return { fn, args }; + } + + getValue({ fn, args }: State) { + if (Object.keys(args.named).length > 0) { + let argsForFn: FnArgs = [...args.positional, args.named]; + + return fn(...argsForFn); + } + + return fn(...args.positional); + } + + getDebugName(fn: State['fn']) { + if (fn.name) { + return `(helper function ${fn.name})`; + } + + return '(anonymous helper function)'; + } +} diff --git a/packages/@glimmer/manager/lib/internal/index.ts b/packages/@glimmer/manager/lib/internal/index.ts index 175ceb8ed5..d0f6ae53bb 100644 --- a/packages/@glimmer/manager/lib/internal/index.ts +++ b/packages/@glimmer/manager/lib/internal/index.ts @@ -7,6 +7,7 @@ import { Owner, } from '@glimmer/interfaces'; import { CustomHelperManager } from '../public/helper'; +import { FunctionHelperManager } from './defaults'; type InternalManager = | InternalComponentManager @@ -119,6 +120,8 @@ export function setInternalHelperManager( 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, @@ -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; diff --git a/packages/@glimmer/manager/test/managers-test.ts b/packages/@glimmer/manager/test/managers-test.ts index 84b24ff0d7..b0eae5136b 100644 --- a/packages/@glimmer/manager/test/managers-test.ts +++ b/packages/@glimmer/manager/test/managers-test.ts @@ -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; + + 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;