Skip to content

Commit 79b40cc

Browse files
authoredMay 20, 2022
Merge pull request #490 from NullVoxPopuli/remote-data-in-templates
fix(RemoteData, function-resource): wrapped template usage
2 parents 59de931 + aa806e3 commit 79b40cc

File tree

6 files changed

+242
-6
lines changed

6 files changed

+242
-6
lines changed
 

‎.npmrc

+5
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
11
public-hoist-pattern[]=*@types*
2+
3+
# Required because broccoli has hard-coded paths to *all* packages
4+
# not just relevant ones..........
5+
public-hoist-pattern[]=eslint-plugin*
6+
public-hoist-pattern[]=eslint-config*

‎ember-resources/src/util/function-resource.ts

+88
Original file line numberDiff line numberDiff line change
@@ -313,8 +313,96 @@ class FunctionResourceManager {
313313
}
314314
}
315315

316+
type ResourceFactory = (...args: any[]) => ReturnType<typeof resource>;
317+
318+
class ResourceInvokerManager {
319+
capabilities = helperCapabilities('3.23', {
320+
hasValue: true,
321+
hasDestroyable: true,
322+
});
323+
324+
createHelper(fn: ResourceFactory, args: any): ReturnType<typeof resource> {
325+
// this calls `resource`, which registers
326+
// with the other helper manager
327+
return fn(...args.positional);
328+
}
329+
330+
getValue(helper: ReturnType<typeof resource>) {
331+
let result = invokeHelper(this, helper, () => ({}));
332+
333+
return getValue(result);
334+
}
335+
336+
getDestroyable(helper: ReturnType<typeof resource>) {
337+
return helper;
338+
}
339+
}
340+
316341
// Provide a singleton manager.
317342
const MANAGER = new FunctionResourceManager();
343+
const ResourceInvoker = new ResourceInvokerManager();
344+
345+
/**
346+
* Allows wrapper functions to provide a [[resource]] for use in templates.
347+
*
348+
* Only library authors may care about this, but helper function is needed to "register"
349+
* the wrapper function with a helper manager that specifically handles invoking both the
350+
* resource wrapper function as well as the underlying resource.
351+
*
352+
* _App-devs / consumers may not ever need to know this utility function exists_
353+
*
354+
* Example using strict mode + <template> syntax and a template-only component:
355+
* ```js
356+
* import { resource, registerResourceWrapper } from 'ember-resources/util/function-resource';
357+
*
358+
* function RemoteData(url) {
359+
* return resource(({ on }) => {
360+
* let state = new TrackedObject({});
361+
* let controller = new AbortController();
362+
*
363+
* on.cleanup(() => controller.abort());
364+
*
365+
* fetch(url, { signal: controller.signal })
366+
* .then(response => response.json())
367+
* .then(data => {
368+
* state.value = data;
369+
* })
370+
* .catch(error => {
371+
* state.error = error;
372+
* });
373+
*
374+
* return state;
375+
* })
376+
* }
377+
*
378+
* registerResourceWrapper(RemoteData)
379+
*
380+
* <template>
381+
* {{#let (load) as |state|}}
382+
* {{#if state.value}}
383+
* ...
384+
* {{else if state.error}}
385+
* {{state.error}}
386+
* {{/if}}
387+
* {{/let}}
388+
* </template>
389+
* ```
390+
*
391+
* Alternatively, `registerResourceWrapper` can wrap the wrapper function.
392+
*
393+
* ```js
394+
* const RemoteData = registerResourceWrapper((url) => {
395+
* return resource(({ on }) => {
396+
* ...
397+
* });
398+
* })
399+
* ```
400+
*/
401+
export function registerResourceWrapper(wrapperFn: ResourceFactory) {
402+
setHelperManager(() => ResourceInvoker, wrapperFn);
403+
404+
return wrapperFn;
405+
}
318406

319407
interface Descriptor {
320408
initializer: () => unknown;

‎ember-resources/src/util/remote-data.ts

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { tracked } from '@glimmer/tracking';
22
import { waitForPromise } from '@ember/test-waiters';
33

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

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

@@ -109,6 +109,24 @@ export function remoteData({ on }: Hooks, url: string, options: FetchOptions = {
109109
* }
110110
* ```
111111
*
112+
* In strict mode with <template>
113+
* ```gjs
114+
* import { RemoteData } from 'ember-resources/util/remote-data';
115+
*
116+
* const options = (token) => ({
117+
* headers: {
118+
* Authorization: `Bearer ${token}`
119+
* }
120+
* });
121+
*
122+
* <template>
123+
* {{#let (RemoteData "https://some.domain" (options "my-token")) as |state|}}
124+
* {{state.isLoading}}
125+
* {{state.value}}
126+
* {{/let}}
127+
* </template>
128+
* ```
129+
*
112130
*/
113131
export function RemoteData(url: string, options?: FetchOptions): State;
114132

@@ -188,3 +206,5 @@ export function RemoteData(
188206
return remoteData(hooks, targetUrl, options);
189207
});
190208
}
209+
210+
registerResourceWrapper(RemoteData);

‎pnpm-lock.yaml

+21-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎testing/ember-app/tests/utils/function-resource/rendering-test.ts

+57-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { hbs } from 'ember-cli-htmlbars';
44
import { module, test } from 'qunit';
55
import { setupRenderingTest } from 'ember-qunit';
66

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

99
module('Utils | resource | rendering', function (hooks) {
1010
setupRenderingTest(hooks);
@@ -57,4 +57,60 @@ module('Utils | resource | rendering', function (hooks) {
5757
'destroy 7',
5858
]);
5959
});
60+
61+
module('with a registered wrapper', function () {
62+
test('lifecycle', async function (assert) {
63+
function Wrapper(initial: number) {
64+
return resource(({ on }) => {
65+
on.cleanup(() => assert.step(`destroy ${initial}`));
66+
67+
assert.step(`resolve ${initial}`);
68+
69+
return initial + 1;
70+
});
71+
}
72+
73+
registerResourceWrapper(Wrapper);
74+
75+
class Test {
76+
@tracked num = 0;
77+
}
78+
79+
let foo = new Test();
80+
81+
this.setProperties({ Wrapper, foo });
82+
83+
await render(hbs`
84+
{{#let (this.Wrapper this.foo.num) as |state|}}
85+
<out>{{state}}</out>
86+
{{/let}}
87+
`);
88+
89+
assert.dom('out').containsText('1');
90+
91+
foo.num = 2;
92+
await settled();
93+
94+
assert.dom('out').containsText('3');
95+
96+
foo.num = 7;
97+
await settled();
98+
99+
assert.dom('out').containsText('8');
100+
101+
await clearRender();
102+
103+
/**
104+
* As a reminder, destruction is async
105+
*/
106+
assert.verifySteps([
107+
'resolve 0',
108+
'resolve 2',
109+
'destroy 0',
110+
'resolve 7',
111+
'destroy 2',
112+
'destroy 7',
113+
]);
114+
});
115+
});
60116
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { render } from '@ember/test-helpers';
2+
import { hbs } from 'ember-cli-htmlbars';
3+
import { module, test } from 'qunit';
4+
import { setupRenderingTest } from 'ember-qunit';
5+
6+
import { setupMSW } from 'ember-app/tests/msw';
7+
import { RemoteData } from 'ember-resources/util/remote-data';
8+
9+
let data = [
10+
{ id: '1', type: 'blogs', attributes: { name: `name:1` } },
11+
{ id: '2', type: 'blogs', attributes: { name: `name:2` } },
12+
{ id: '3', type: 'blogs', attributes: { name: `name:3` } },
13+
];
14+
15+
module('Utils | remote-data | rendering', function (hooks) {
16+
setupRenderingTest(hooks);
17+
setupMSW(hooks, ({ rest }) => [
18+
rest.get('/blogs/:id', (req, res, ctx) => {
19+
let record = data.find((datum) => datum.id === req.params.id);
20+
21+
return res(ctx.json({ ...record }));
22+
}),
23+
]);
24+
25+
module('RemoteData', function () {
26+
test('works with static url', async function (assert) {
27+
this.setProperties({ RemoteData });
28+
29+
await render(hbs`
30+
{{#let (this.RemoteData "/blogs/1") as |blog|}}
31+
{{blog.value.attributes.name}}
32+
{{/let}}
33+
`);
34+
35+
assert.dom().hasText('name:1');
36+
});
37+
38+
test('works with dynamic url', async function (assert) {
39+
this.setProperties({ RemoteData });
40+
41+
await render(hbs`
42+
{{#let (this.RemoteData "/blogs/1") as |blog|}}
43+
{{blog.value.attributes.name}}
44+
{{/let}}
45+
`);
46+
47+
assert.dom().hasText('name:1');
48+
});
49+
});
50+
});

0 commit comments

Comments
 (0)
Failed to load comments.