diff --git a/ember-resources/package.json b/ember-resources/package.json index a2dd56e21..5de55175e 100644 --- a/ember-resources/package.json +++ b/ember-resources/package.json @@ -12,6 +12,7 @@ "./core": "./dist/core/index.js", "./core/class-based": "./dist/core/class-based/index.js", "./core/function-based": "./dist/core/function-based/index.js", + "./override-default-managers": "./dist/override-default-managers.js", "./link": "./dist/link.js", "./service": "./dist/service.js", "./modifier": "./dist/modifier/index.js", @@ -38,6 +39,9 @@ "link": [ "dist/link.d.ts" ], + "override-default-managers": [ + "dist/override-default-managers.d.ts" + ], "service": [ "dist/service.d.ts" ], diff --git a/ember-resources/src/core/function-based/immediate-invocation.ts b/ember-resources/src/core/function-based/immediate-invocation.ts index 6d7ab7f02..2d81fd797 100644 --- a/ember-resources/src/core/function-based/immediate-invocation.ts +++ b/ember-resources/src/core/function-based/immediate-invocation.ts @@ -43,11 +43,19 @@ class ResourceInvokerManager { /** * This cache is for args passed to the ResourceInvoker/Factory * - * We want to cache the helper result, and only re-inoke when the args + * We want to cache the helper result, and only re-invoke when the args * change. */ let cache = createCache(() => { - let resource = fn(...args.positional) as object; + let argsForFn = []; + + if (Object.keys(args.named).length > 0 ) { + argsForFn = [...args.positional, args.named]; + } else { + argsForFn = [...args.positional]; + } + + let resource = fn(...argsForFn) as object; setOwner(resource, this.owner); @@ -178,4 +186,5 @@ type ResourceBlueprint = // semicolon // Provide a singleton manager. -const ResourceInvokerFactory = (owner: Owner) => new ResourceInvokerManager(owner); +export const ResourceInvokerFactory = (owner: Owner) => new ResourceInvokerManager(owner); + diff --git a/ember-resources/src/modifier/index.ts b/ember-resources/src/modifier/index.ts index aad61c782..1b220c1b8 100644 --- a/ember-resources/src/modifier/index.ts +++ b/ember-resources/src/modifier/index.ts @@ -3,15 +3,12 @@ import { assert } from '@ember/debug'; import { setModifierManager } from '@ember/modifier'; import { resourceFactory } from '../index'; -import FunctionBasedModifierManager from './manager'; +import { MANAGER } from './manager'; import type { resource } from '../index'; import type { ArgsFor, ElementFor, EmptyObject } from '[core-types]'; import type { ModifierLike } from '@glint/template'; -// Provide a singleton manager. -const MANAGER = new FunctionBasedModifierManager(); - type PositionalArgs = S extends { Args?: object } ? ArgsFor['Positional'] : []; type NamedArgs = S extends { Args?: object } ? ArgsFor['Named'] extends object diff --git a/ember-resources/src/modifier/manager.ts b/ember-resources/src/modifier/manager.ts index 1c122128b..27a2873e5 100644 --- a/ember-resources/src/modifier/manager.ts +++ b/ember-resources/src/modifier/manager.ts @@ -83,3 +83,7 @@ export default class FunctionBasedModifierManager { } } } + +// Provide a singleton manager. +export const MANAGER = new FunctionBasedModifierManager(); + diff --git a/ember-resources/src/override-default-managers.ts b/ember-resources/src/override-default-managers.ts new file mode 100644 index 000000000..d436b0db6 --- /dev/null +++ b/ember-resources/src/override-default-managers.ts @@ -0,0 +1,12 @@ +// @ts-expect-error +import { setHelperManager } from '@ember/helper'; +// @ts-expect-error +import { setModifierManager } from '@ember/modifier'; + +import { ResourceInvokerFactory } from './core/function-based/immediate-invocation'; +import { MANAGER } from './modifier/manager'; + +export function overrideDefaultManagers() { + setModifierManager(() => MANAGER, Function.prototype); + setHelperManager(ResourceInvokerFactory, Function.prototype); +} diff --git a/test-app/tests/function-wrappers/factory-test.gts b/test-app/tests/function-wrappers/factory-test.gts new file mode 100644 index 000000000..e05fefa3d --- /dev/null +++ b/test-app/tests/function-wrappers/factory-test.gts @@ -0,0 +1,59 @@ + +import { render} from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; + +import { resource, cell } from 'ember-resources'; + +// Will need to be a class for .current flattening / auto-rendering +interface Reactive { + current: Value; +} + +module('function-wrappers | Core | (function) resource | use | rendering', function (hooks) { + setupRenderingTest(hooks); + + const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + + let formatter = new Intl.DateTimeFormat('en-US', { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + hour12: false, + }); + + test('it works', async function (assert) { + let nowDate = Date.now(); + let format = (time: Reactive) => formatter.format(time.current); + + function Now(ms = 1000) { + return resource(({ on }) => { + let now = cell(nowDate); + let timer = setInterval(() => now.set(Date.now()), ms); + + on.cleanup(() => clearInterval(timer)); + + return () => now.current; + }); + } + + function Stopwatch(ms = 500) { + return resource(({ use }) => { + let time = use(Now(ms)); + + return () => format(time); + }); + } + + await render(); + + let first = formatter.format(Date.now()); + assert.dom('time').hasText(first); + + await wait(1010); + + let second = formatter.format(Date.now()); + assert.dom('time').hasText(second); + assert.notEqual(first, second); + }); +}); diff --git a/test-app/tests/function-wrappers/modifier-test.gjs b/test-app/tests/function-wrappers/modifier-test.gjs new file mode 100644 index 000000000..b13ce058b --- /dev/null +++ b/test-app/tests/function-wrappers/modifier-test.gjs @@ -0,0 +1,77 @@ +import { render, settled } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; + +import { resource, cell } from 'ember-resources'; +import { overrideDefaultManagers } from 'ember-resources/override-default-managers'; + +overrideDefaultManagers(); + +module('function wrappers | modifier | rendering', function (hooks) { + setupRenderingTest(hooks); + + test('no arguments', async function (assert) { + function capture(...args) { + assert.step(`${args.length} args`); + + let [element] = args; + + return resource(() => { + assert.step(`received element ${element.tagName}`); + }); + } + + await render(); + + assert.verifySteps(['1 args', 'received element DIV']); + }); + + test('mixed arguments', async function (assert) { + const named = cell(0); + const positional = cell(0); + const visible = cell(true); + + function capture(...args: [Element, number, { value: number }]) { + assert.step(`${args.length} args`); + + return resource(({ on }) => { + let positionalValue = args[1]; + let { value } = args[2]; + + assert.step(`received element ${args[0].tagName} for value ${positionalValue},${value}`); + + on.cleanup(() => assert.step(`cleanup ${positionalValue},${value}`)); + }); + } + + await render(); + + assert.verifySteps(['3 args', 'received element DIV for value 0,0']); + + named.current++; + await settled(); + assert.verifySteps(['3 args', 'received element DIV for value 0,1', 'cleanup 0,0']); + + visible.current = false; + await settled(); + assert.verifySteps(['cleanup 0,1']); + + positional.current++; + visible.current = true; + await settled(); + + assert.verifySteps(['3 args', 'received element DIV for value 1,1']); + + positional.current++; + await settled(); + assert.verifySteps(['3 args', 'received element DIV for value 2,1', 'cleanup 1,1']); + + visible.current = false; + await settled(); + assert.verifySteps(['cleanup 2,1']); + }); +});