Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the helper from ember-resources #31

Merged
merged 2 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions reactiveweb/src/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { getValue } from '@glimmer/tracking/primitives/cache';
import { invokeHelper } from '@ember/helper';

import { DEFAULT_THUNK, normalizeThunk } from './utils.ts';

import type ClassBasedHelper from '@ember/component/helper';
import type { FunctionBasedHelper } from '@ember/component/helper';
import type { HelperLike } from '@glint/template';
import type { Thunk } from 'ember-resources';

// Should be from
// @glimmer/tracking/primitives/cache
type Cache = ReturnType<typeof invokeHelper>;

type Get<T, K, Otherwise = unknown> = K extends keyof T ? T[K] : Otherwise;

/**
* <div class="callout note">
*
* This is not a core part of ember-resources, but is an example utility to demonstrate a concept when authoring your own resources. However, this utility is still under the broader library's SemVer policy.
*
* A consuming app will not pay for the bytes of this utility unless imported.
*
* </div>
*
* implemented with raw `invokeHelper` API, no classes from `ember-resources` used.
*
* -----------------------
*
* Enables the use of template-helpers in JavaScript
*
* Note that it should be preferred to use regular functions in javascript
* whenever possible, as the runtime cost of "things as resources" is non-0.
* For example, if using `@ember/component/helper` utilities, it's a common p
* practice to split the actual behavior from the framework construct
* ```js
* export function plainJs() {}
*
* export default helper(() => plainJs())
* ```
* so in this case `plainJs` can be used separately.
*
* This differentiation makes less of a difference since
* [plain functions as helpers](https://github.com/emberjs/rfcs/pull/756)
* will be supported soon.
*
* @example
* ```js
* import intersect from 'ember-composable-helpers/addon/helpers/intersect';
*
* import { helper } from 'ember-resources/util/helper';
*
* class Demo {
* @tracked listA = [...];
* @tracked listB = [...]
*
* intersection = helper(this, intersect, () => [this.listA, this.listB])
*
* toString = (array) => array.join(', ');
* }
* ```
* ```hbs
* {{this.toString this.intersection.value}}
* ```
*/
export function helper<T = unknown, S = InferSignature<T>, Return = Get<S, 'Return'>>(
context: object,
helper: T,
thunk: Thunk = DEFAULT_THUNK
): { value: Return } {
let resource: Cache;

return {
get value(): Return {
if (!resource) {
resource = invokeHelper(context, helper as object, () => {
return normalizeThunk(thunk);
});
}

// SAFETY: we want whatever the Return type is to be forwarded.
// getValue could technically be undefined, but we *def*
// have an invokedHelper, so we can safely defer to the helper.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return getValue<Return>(resource as any) as Return;
},
};
}

type InferSignature<T> = T extends HelperLike<infer S>
? S
: T extends FunctionBasedHelper<infer S>
? S
: T extends ClassBasedHelper<infer S>
? S
: 'Signature not found';
34 changes: 34 additions & 0 deletions tests/test-app/tests/utils/helper/js-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { tracked } from '@glimmer/tracking';
import { setOwner } from '@ember/application';
import { helper as emberHelper } from '@ember/component/helper';
import { settled } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';

import { helper } from 'ember-resources/util/helper';

// not testing in template, because that's the easy part
module('Utils | helper | js', function (hooks) {
setupTest(hooks);

test('it works', async function (assert) {
class Test {
@tracked count = 1;

_doubler = emberHelper(([num]: number[]) => (num ? num * 2 : num));

doubler = helper(this, this._doubler, () => [this.count]);
}

let foo = new Test();

setOwner(foo, this.owner);

assert.strictEqual(foo.doubler.value, 2);

foo.count = 4;
await settled();

assert.strictEqual(foo.doubler.value, 8);
});
});
Loading