Skip to content

Commit

Permalink
Add the helper from ember-resources (#31)
Browse files Browse the repository at this point in the history
* Add the helper from ember-resources

* updates
  • Loading branch information
NullVoxPopuli authored Jan 5, 2024
1 parent fa156a4 commit 4fcd3a7
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 0 deletions.
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);
});
});

0 comments on commit 4fcd3a7

Please sign in to comment.