Skip to content

Commit 4b1f708

Browse files
Merge pull request #952 from NullVoxPopuli/modifiers
Modifiers
2 parents 9213df6 + aa2c453 commit 4b1f708

File tree

12 files changed

+1893
-635
lines changed

12 files changed

+1893
-635
lines changed

.changeset/polite-tips-bow.md

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
"ember-resources": minor
3+
---
4+
5+
Introduce resources as modifiers.
6+
This brings alignment with Starbeam's plans for modifiers as a universal primitive.
7+
8+
In ember-resources, using modifiers as resources looks like this:
9+
10+
```js
11+
import { resource } from 'ember-resources';
12+
import { modifier } from 'ember-resources/modifier';
13+
14+
const wiggle = modifier((element, arg1, arg2, namedArgs) => {
15+
return resource(({ on }) => {
16+
let animation = element.animate([
17+
{ transform: `translateX(${arg1}px)` },
18+
{ transform: `translateX(-${arg2}px)` },
19+
], {
20+
duration: 100,
21+
iterations: Infinity,
22+
});
23+
24+
on.cleanup(() => animation.cancel());
25+
});
26+
});
27+
28+
<template>
29+
<div {{wiggle 2 5 named="hello"}}>hello</div>
30+
</template>
31+
```
32+
33+
The signature for the modifier here is _different_ from `ember-modifier`, where positional args and named args are grouped together into an array and object respectively.
34+
35+
This signature for ember-resource's `modifier` follows the [plain function invocation](https://blog.emberjs.com/plain-old-functions-as-helpers/) signature.
36+
37+
<details><summary>in Starbeam</summary>
38+
39+
```js
40+
import { resource } from '@starbeam/universal';
41+
42+
function wiggle(element, arg1, arg2, namedArgs) {
43+
return resource(({ on }) => {
44+
let animation = element.animate([
45+
{ transform: `translateX(${arg1}px)` },
46+
{ transform: `translateX(-${arg2}px)` },
47+
], {
48+
duration: 100,
49+
iterations: Infinity,
50+
});
51+
52+
on.cleanup(() => animation.cancel());
53+
});
54+
}
55+
56+
<template>
57+
<div {{wiggle 2 5 named="hello"}}>hello</div>
58+
</template>
59+
```
60+
61+
</details>

ember-resources/package.json

+6-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"./core/function-based": "./dist/core/function-based/index.js",
1515
"./link": "./dist/link.js",
1616
"./service": "./dist/service.js",
17+
"./modifier": "./dist/modifier/index.js",
1718
"./util": "./dist/util/index.js",
1819
"./util/cell": "./dist/util/cell.js",
1920
"./util/keep-latest": "./dist/util/keep-latest.js",
@@ -40,6 +41,9 @@
4041
"service": [
4142
"dist/service.d.ts"
4243
],
44+
"modifier": [
45+
"dist/modifier/index.d.ts"
46+
],
4347
"util": [
4448
"dist/util/index.d.ts"
4549
],
@@ -102,7 +106,7 @@
102106
"dependencies": {
103107
"@babel/runtime": "^7.17.8",
104108
"@embroider/addon-shim": "^1.2.0",
105-
"@embroider/macros": "^1.2.0",
109+
"@embroider/macros": "^1.12.3",
106110
"ember-async-data": "^1.0.1"
107111
},
108112
"peerDependencies": {
@@ -134,7 +138,7 @@
134138
"@babel/plugin-transform-typescript": "^7.18.4",
135139
"@babel/preset-typescript": "^7.17.12",
136140
"@ember/test-waiters": "^3.0.0",
137-
"@embroider/addon-dev": "^3.0.0",
141+
"@embroider/addon-dev": "^3.1.2",
138142
"@glimmer/component": "^1.1.2",
139143
"@glimmer/tracking": "^1.1.2",
140144
"@glint/template": "^1.0.2",

ember-resources/rollup.config.mjs

-18
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,6 @@ export default defineConfig({
3030
// addon. Anything not listed here may get optimized away.
3131
addon.publicEntrypoints(['**/*.ts']),
3232

33-
// These are the modules that should get reexported into the traditional
34-
// "app" tree. Things in here should also be in publicEntrypoints above, but
35-
// not everything in publicEntrypoints necessarily needs to go here.
36-
// addon.appReexports([]),
37-
38-
// This babel config should *not* apply presets or compile away ES modules.
39-
// It exists only to provide development niceties for you, like automatic
40-
// template colocation.
41-
//
42-
// By default, this will load the actual babel config from the file
43-
// babel.config.json.
4433
ts({
4534
// can be changed to swc or other transpilers later
4635
// but we need the ember plugins converted first
@@ -69,13 +58,6 @@ export default defineConfig({
6958
// package names.
7059
addon.dependencies(),
7160

72-
// Ensure that standalone .hbs files are properly integrated as Javascript.
73-
// addon.hbs(),
74-
75-
// addons are allowed to contain imports of .css files, which we want rollup
76-
// to leave alone and keep in the published output.
77-
// addon.keepAssets(['**/*.css']),
78-
7961
// Start with a clean output directory before building
8062
addon.clean(),
8163

ember-resources/src/core/function-based/immediate-invocation.ts

-22
Original file line numberDiff line numberDiff line change
@@ -73,28 +73,6 @@ class ResourceInvokerManager {
7373
getDestroyable({ cache }: State) {
7474
return cache;
7575
}
76-
77-
// createHelper(fn: AnyFunction, args: Arguments): State {
78-
// return { fn, args };
79-
// }
80-
81-
// getValue({ fn, args }: State): unknown {
82-
// if (Object.keys(args.named).length > 0) {
83-
// let argsForFn: FnArgs<Arguments> = [...args.positional, args.named];
84-
85-
// return fn(...argsForFn);
86-
// }
87-
88-
// return fn(...args.positional);
89-
// }
90-
91-
// getDebugName(fn: AnyFunction): string {
92-
// if (fn.name) {
93-
// return `(helper function ${fn.name})`;
94-
// }
95-
96-
// return '(anonymous helper function)';
97-
// }
9876
}
9977

10078
/**

ember-resources/src/core/types/signature-args.ts

+6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ export type ArgsFor<S> =
1414
}
1515
: { Named: EmptyObject; Positional: [] };
1616

17+
export type ElementFor<S> = 'Element' extends keyof S
18+
? S['Element'] extends Element
19+
? S['Element']
20+
: Element
21+
: Element;
22+
1723
/**
1824
* Converts a variety of types to the expanded arguments type
1925
* that aligns with the 'Args' portion of the 'Signature' types

ember-resources/src/modifier/index.ts

+227
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import { assert } from '@ember/debug';
2+
// @ts-expect-error
3+
import { setModifierManager } from '@ember/modifier';
4+
5+
import { resourceFactory } from '../index';
6+
import FunctionBasedModifierManager from './manager';
7+
8+
import type { resource } from '../index';
9+
import type { ArgsFor, ElementFor, EmptyObject } from '[core-types]';
10+
import type { ModifierLike } from '@glint/template';
11+
12+
// Provide a singleton manager.
13+
const MANAGER = new FunctionBasedModifierManager();
14+
15+
type PositionalArgs<S> = S extends { Args?: object } ? ArgsFor<S['Args']>['Positional'] : [];
16+
type NamedArgs<S> = S extends { Args?: object }
17+
? ArgsFor<S['Args']>['Named'] extends object
18+
? ArgsFor<S['Args']>['Named']
19+
: EmptyObject
20+
: EmptyObject;
21+
22+
type ArgsForFn<S> = S extends { Args?: object }
23+
? ArgsFor<S['Args']>['Named'] extends EmptyObject
24+
? [...PositionalArgs<S>]
25+
: [...PositionalArgs<S>, NamedArgs<S>]
26+
: [];
27+
28+
/**
29+
* A resource-based API for building modifiers.
30+
*
31+
* You can attach this to an element, and use a `resource` to manage
32+
* the state, add event listeners, remove event listeners on cleanup, etc.
33+
*
34+
* Using resources for modifiers provides a clear and concise API with
35+
* easy to read concerns.
36+
*
37+
*
38+
* The signature for the modifier here is _different_ from `ember-modifier`, where positional args and named args are grouped together into an array and object respectively.
39+
40+
* This signature for ember-resource's `modifier` follows the [plain function invocation](https://blog.emberjs.com/plain-old-functions-as-helpers/) signature.
41+
*
42+
* ```js
43+
* import { resource } from 'ember-resources';
44+
* import { modifier } from 'ember-resources/modifier';
45+
*
46+
* const wiggle = modifier((element, arg1, arg2, namedArgs) => {
47+
* return resource(({ on }) => {
48+
* let animation = element.animate([
49+
* { transform: `translateX(${arg1}px)` },
50+
* { transform: `translateX(-${arg2}px)` },
51+
* ], {
52+
* duration: 100,
53+
* iterations: Infinity,
54+
* });
55+
*
56+
* on.cleanup(() => animation.cancel());
57+
* });
58+
* });
59+
*
60+
* <template>
61+
* <div {{wiggle 2 5 named="hello"}}>hello</div>
62+
* </template>
63+
* ```
64+
*
65+
*/
66+
export function modifier<El extends Element, Args extends unknown[] = unknown[]>(
67+
fn: (element: El, ...args: Args) => void
68+
): ModifierLike<{
69+
Element: El;
70+
Args: {
71+
Named: EmptyObject;
72+
Positional: Args;
73+
};
74+
}>;
75+
76+
/**
77+
* A resource-based API for building modifiers.
78+
*
79+
* You can attach this to an element, and use a `resource` to manage
80+
* the state, add event listeners, remove event listeners on cleanup, etc.
81+
*
82+
* Using resources for modifiers provides a clear and concise API with
83+
* easy to read concerns.
84+
*
85+
*
86+
* The signature for the modifier here is _different_ from `ember-modifier`, where positional args and named args are grouped together into an array and object respectively.
87+
88+
* This signature for ember-resource's `modifier` follows the [plain function invocation](https://blog.emberjs.com/plain-old-functions-as-helpers/) signature.
89+
*
90+
* ```js
91+
* import { resource } from 'ember-resources';
92+
* import { modifier } from 'ember-resources/modifier';
93+
*
94+
* const wiggle = modifier((element, arg1, arg2, namedArgs) => {
95+
* return resource(({ on }) => {
96+
* let animation = element.animate([
97+
* { transform: `translateX(${arg1}px)` },
98+
* { transform: `translateX(-${arg2}px)` },
99+
* ], {
100+
* duration: 100,
101+
* iterations: Infinity,
102+
* });
103+
*
104+
* on.cleanup(() => animation.cancel());
105+
* });
106+
* });
107+
*
108+
* <template>
109+
* <div {{wiggle 2 5 named="hello"}}>hello</div>
110+
* </template>
111+
* ```
112+
*
113+
*/
114+
export function modifier<S extends { Element?: Element }>(
115+
fn: (element: ElementFor<S>, ...args: ArgsForFn<S>) => ReturnType<typeof resource>
116+
): ModifierLike<S>;
117+
/**
118+
* A resource-based API for building modifiers.
119+
*
120+
* You can attach this to an element, and use a `resource` to manage
121+
* the state, add event listeners, remove event listeners on cleanup, etc.
122+
*
123+
* Using resources for modifiers provides a clear and concise API with
124+
* easy to read concerns.
125+
*
126+
*
127+
* The signature for the modifier here is _different_ from `ember-modifier`, where positional args and named args are grouped together into an array and object respectively.
128+
129+
* This signature for ember-resource's `modifier` follows the [plain function invocation](https://blog.emberjs.com/plain-old-functions-as-helpers/) signature.
130+
*
131+
* ```js
132+
* import { resource } from 'ember-resources';
133+
* import { modifier } from 'ember-resources/modifier';
134+
*
135+
* const wiggle = modifier((element, arg1, arg2, namedArgs) => {
136+
* return resource(({ on }) => {
137+
* let animation = element.animate([
138+
* { transform: `translateX(${arg1}px)` },
139+
* { transform: `translateX(-${arg2}px)` },
140+
* ], {
141+
* duration: 100,
142+
* iterations: Infinity,
143+
* });
144+
*
145+
* on.cleanup(() => animation.cancel());
146+
* });
147+
* });
148+
*
149+
* <template>
150+
* <div {{wiggle 2 5 named="hello"}}>hello</div>
151+
* </template>
152+
* ```
153+
*
154+
*/
155+
export function modifier<S extends { Args?: object }>(
156+
fn: (element: ElementFor<S>, ...args: ArgsForFn<S>) => ReturnType<typeof resource>
157+
): ModifierLike<S>;
158+
/**
159+
* A resource-based API for building modifiers.
160+
*
161+
* You can attach this to an element, and use a `resource` to manage
162+
* the state, add event listeners, remove event listeners on cleanup, etc.
163+
*
164+
* Using resources for modifiers provides a clear and concise API with
165+
* easy to read concerns.
166+
*
167+
*
168+
* The signature for the modifier here is _different_ from `ember-modifier`, where positional args and named args are grouped together into an array and object respectively.
169+
170+
* This signature for ember-resource's `modifier` follows the [plain function invocation](https://blog.emberjs.com/plain-old-functions-as-helpers/) signature.
171+
*
172+
* ```js
173+
* import { resource } from 'ember-resources';
174+
* import { modifier } from 'ember-resources/modifier';
175+
*
176+
* const wiggle = modifier((element, arg1, arg2, namedArgs) => {
177+
* return resource(({ on }) => {
178+
* let animation = element.animate([
179+
* { transform: `translateX(${arg1}px)` },
180+
* { transform: `translateX(-${arg2}px)` },
181+
* ], {
182+
* duration: 100,
183+
* iterations: Infinity,
184+
* });
185+
*
186+
* on.cleanup(() => animation.cancel());
187+
* });
188+
* });
189+
*
190+
* <template>
191+
* <div {{wiggle 2 5 named="hello"}}>hello</div>
192+
* </template>
193+
* ```
194+
*
195+
*/
196+
export function modifier<S extends { Element?: Element; Args?: object }>(
197+
fn: (element: ElementFor<S>, ...args: ArgsForFn<S>) => ReturnType<typeof resource>
198+
): ModifierLike<S>;
199+
200+
export function modifier(fn: (element: Element, ...args: unknown[]) => void): ModifierLike<{
201+
Element: Element;
202+
Args: {
203+
Named: {};
204+
Positional: [];
205+
};
206+
}> {
207+
assert(`modifier() must be invoked with a function`, typeof fn === 'function');
208+
setModifierManager(() => MANAGER, fn);
209+
resourceFactory(fn);
210+
211+
return fn as unknown as ModifierLike<{
212+
Element: Element;
213+
Args: {
214+
Named: {};
215+
Positional: [];
216+
};
217+
}>;
218+
}
219+
220+
/**
221+
* @internal
222+
*/
223+
export type FunctionBasedModifierDefinition<S> = (
224+
element: ElementFor<S>,
225+
positional: PositionalArgs<S>,
226+
named: NamedArgs<S>
227+
) => void;

0 commit comments

Comments
 (0)