Skip to content

Commit

Permalink
Merge pull request #490 from NullVoxPopuli/remote-data-in-templates
Browse files Browse the repository at this point in the history
fix(RemoteData, function-resource): wrapped template usage
  • Loading branch information
NullVoxPopuli authored May 20, 2022
2 parents 59de931 + aa806e3 commit 79b40cc
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 6 deletions.
5 changes: 5 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
public-hoist-pattern[]=*@types*

# Required because broccoli has hard-coded paths to *all* packages
# not just relevant ones..........
public-hoist-pattern[]=eslint-plugin*
public-hoist-pattern[]=eslint-config*
88 changes: 88 additions & 0 deletions ember-resources/src/util/function-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,8 +313,96 @@ class FunctionResourceManager {
}
}

type ResourceFactory = (...args: any[]) => ReturnType<typeof resource>;

class ResourceInvokerManager {
capabilities = helperCapabilities('3.23', {
hasValue: true,
hasDestroyable: true,
});

createHelper(fn: ResourceFactory, args: any): ReturnType<typeof resource> {
// this calls `resource`, which registers
// with the other helper manager
return fn(...args.positional);
}

getValue(helper: ReturnType<typeof resource>) {
let result = invokeHelper(this, helper, () => ({}));

return getValue(result);
}

getDestroyable(helper: ReturnType<typeof resource>) {
return helper;
}
}

// Provide a singleton manager.
const MANAGER = new FunctionResourceManager();
const ResourceInvoker = new ResourceInvokerManager();

/**
* Allows wrapper functions to provide a [[resource]] for use in templates.
*
* Only library authors may care about this, but helper function is needed to "register"
* the wrapper function with a helper manager that specifically handles invoking both the
* resource wrapper function as well as the underlying resource.
*
* _App-devs / consumers may not ever need to know this utility function exists_
*
* Example using strict mode + <template> syntax and a template-only component:
* ```js
* import { resource, registerResourceWrapper } from 'ember-resources/util/function-resource';
*
* function RemoteData(url) {
* return resource(({ on }) => {
* let state = new TrackedObject({});
* let controller = new AbortController();
*
* on.cleanup(() => controller.abort());
*
* fetch(url, { signal: controller.signal })
* .then(response => response.json())
* .then(data => {
* state.value = data;
* })
* .catch(error => {
* state.error = error;
* });
*
* return state;
* })
* }
*
* registerResourceWrapper(RemoteData)
*
* <template>
* {{#let (load) as |state|}}
* {{#if state.value}}
* ...
* {{else if state.error}}
* {{state.error}}
* {{/if}}
* {{/let}}
* </template>
* ```
*
* Alternatively, `registerResourceWrapper` can wrap the wrapper function.
*
* ```js
* const RemoteData = registerResourceWrapper((url) => {
* return resource(({ on }) => {
* ...
* });
* })
* ```
*/
export function registerResourceWrapper(wrapperFn: ResourceFactory) {
setHelperManager(() => ResourceInvoker, wrapperFn);

return wrapperFn;
}

interface Descriptor {
initializer: () => unknown;
Expand Down
22 changes: 21 additions & 1 deletion ember-resources/src/util/remote-data.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { tracked } from '@glimmer/tracking';
import { waitForPromise } from '@ember/test-waiters';

import { resource } from './function-resource';
import { registerResourceWrapper, resource } from './function-resource';

import type { Hooks } from './function-resource';

Expand Down Expand Up @@ -109,6 +109,24 @@ export function remoteData({ on }: Hooks, url: string, options: FetchOptions = {
* }
* ```
*
* In strict mode with <template>
* ```gjs
* import { RemoteData } from 'ember-resources/util/remote-data';
*
* const options = (token) => ({
* headers: {
* Authorization: `Bearer ${token}`
* }
* });
*
* <template>
* {{#let (RemoteData "https://some.domain" (options "my-token")) as |state|}}
* {{state.isLoading}}
* {{state.value}}
* {{/let}}
* </template>
* ```
*
*/
export function RemoteData(url: string, options?: FetchOptions): State;

Expand Down Expand Up @@ -188,3 +206,5 @@ export function RemoteData(
return remoteData(hooks, targetUrl, options);
});
}

registerResourceWrapper(RemoteData);
25 changes: 21 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { hbs } from 'ember-cli-htmlbars';
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';

import { resource } from 'ember-resources/util/function-resource';
import { registerResourceWrapper, resource } from 'ember-resources/util/function-resource';

module('Utils | resource | rendering', function (hooks) {
setupRenderingTest(hooks);
Expand Down Expand Up @@ -57,4 +57,60 @@ module('Utils | resource | rendering', function (hooks) {
'destroy 7',
]);
});

module('with a registered wrapper', function () {
test('lifecycle', async function (assert) {
function Wrapper(initial: number) {
return resource(({ on }) => {
on.cleanup(() => assert.step(`destroy ${initial}`));

assert.step(`resolve ${initial}`);

return initial + 1;
});
}

registerResourceWrapper(Wrapper);

class Test {
@tracked num = 0;
}

let foo = new Test();

this.setProperties({ Wrapper, foo });

await render(hbs`
{{#let (this.Wrapper this.foo.num) as |state|}}
<out>{{state}}</out>
{{/let}}
`);

assert.dom('out').containsText('1');

foo.num = 2;
await settled();

assert.dom('out').containsText('3');

foo.num = 7;
await settled();

assert.dom('out').containsText('8');

await clearRender();

/**
* As a reminder, destruction is async
*/
assert.verifySteps([
'resolve 0',
'resolve 2',
'destroy 0',
'resolve 7',
'destroy 2',
'destroy 7',
]);
});
});
});
50 changes: 50 additions & 0 deletions testing/ember-app/tests/utils/remote-data/rendering-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';

import { setupMSW } from 'ember-app/tests/msw';
import { RemoteData } from 'ember-resources/util/remote-data';

let data = [
{ id: '1', type: 'blogs', attributes: { name: `name:1` } },
{ id: '2', type: 'blogs', attributes: { name: `name:2` } },
{ id: '3', type: 'blogs', attributes: { name: `name:3` } },
];

module('Utils | remote-data | rendering', function (hooks) {
setupRenderingTest(hooks);
setupMSW(hooks, ({ rest }) => [
rest.get('/blogs/:id', (req, res, ctx) => {
let record = data.find((datum) => datum.id === req.params.id);

return res(ctx.json({ ...record }));
}),
]);

module('RemoteData', function () {
test('works with static url', async function (assert) {
this.setProperties({ RemoteData });

await render(hbs`
{{#let (this.RemoteData "/blogs/1") as |blog|}}
{{blog.value.attributes.name}}
{{/let}}
`);

assert.dom().hasText('name:1');
});

test('works with dynamic url', async function (assert) {
this.setProperties({ RemoteData });

await render(hbs`
{{#let (this.RemoteData "/blogs/1") as |blog|}}
{{blog.value.attributes.name}}
{{/let}}
`);

assert.dom().hasText('name:1');
});
});
});

0 comments on commit 79b40cc

Please sign in to comment.