-
Notifications
You must be signed in to change notification settings - Fork 30
/
modifier.ts
269 lines (257 loc) · 9.87 KB
/
modifier.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
import { deprecate } from '@ember/debug';
import { setModifierManager } from '@ember/modifier';
import Opaque from '../opaque';
import {
DefaultSignature,
ElementFor,
ModifierArgs,
NamedArgs,
PositionalArgs,
} from '../signature';
import FunctionBasedModifierManager from './modifier-manager';
// Provide a singleton manager for each of the options. (If we extend this to
// many more options in the future, we can revisit, but for now this means we
// only ever allocate two managers.)
const EAGER_MANAGER = new FunctionBasedModifierManager({ eager: true });
const LAZY_MANAGER = new FunctionBasedModifierManager({ eager: false });
// This provides a signature whose only purpose here is to represent the runtime
// type of a function-based modifier: an opaque item. The fact that it's an
// empty interface is actually the point: it *only* hooks the type parameter
// into the opaque (nominal) type.
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface FunctionBasedModifier<S = DefaultSignature>
extends Opaque<S> {}
/**
* The (optional) return type for a modifier which needs to perform some kind of
* cleanup or teardown -- for example, removing an event listener from an
* element besides the one passed into the modifier.
*/
export type Teardown = () => unknown;
/**
* An API for writing simple modifiers.
*
* This function runs the first time when the element the modifier was applied
* to is inserted into the DOM, and it *autotracks* while running. Any values
* that it accesses will be tracked, and if any of them changes, the function
* will run again.
*
* **Note:** this will rerun if any of its arguments change, *whether or not you
* access them*. This is legacy behavior and you should switch to using the
* `{ eager: false }` variant, which has normal auto-tracking semantics.
*
* The modifier can also optionally return a *destructor*. The destructor
* function will be run just before the next update, and when the element is
* being removed entirely. It should generally clean up the changes that the
* modifier made in the first place.
*
* @deprecated Until 4.0. Calling `modifier()` without an options argument is
* deprecated. It is supported until 4.0 so that existing modifiers can be
* migrated individually. Please update your function-based modifiers to pass
* `{ eager: false }`.
*
* @param fn The function which defines the modifier.
*/
// This overload allows users to write types directly on the callback passed to
// the `modifier` function and infer the resulting type correctly.
export default function modifier<
E extends Element,
P extends unknown[],
N extends object = Record<string, unknown>
>(
fn: (element: E, positional: P, named: N) => void | Teardown
): FunctionBasedModifier<{
Args: { Named: N; Positional: P };
Element: E;
}>;
/**
* An API for writing simple modifiers.
*
* This function runs the first time when the element the modifier was applied
* to is inserted into the DOM, and it *autotracks* while running. Any values
* that it accesses will be tracked, and if any of them changes, the function
* will run again.
*
* **Note:** this will rerun if any of its arguments change, *whether or not you
* access them*. This is legacy behavior and you should switch to using the
* `{ eager: false }` variant, which has normal auto-tracking semantics.
*
* The modifier can also optionally return a *destructor*. The destructor
* function will be run just before the next update, and when the element is
* being removed entirely. It should generally clean up the changes that the
* modifier made in the first place.
*
* @deprecated Until 4.0. Calling `modifier()` with `{ eager: true }` is
* deprecated. It is supported until 4.0 so that existing modifiers can be
* migrated individually. Please update your function-based modifiers to pass
* `{ eager: false }`.
*
* @param fn The function which defines the modifier.
* @param options Configuration for the modifier.
*/
// This overload allows users to write types directly on the callback passed to
// the `modifier` function and infer the resulting type correctly.
export default function modifier<
E extends Element,
P extends unknown[],
N extends object = Record<string, unknown>
>(
fn: (element: E, positional: P, named: N) => void | Teardown,
options: { eager: true }
): FunctionBasedModifier<{
Args: { Named: N; Positional: P };
Element: E;
}>;
/**
* An API for writing simple modifiers.
*
* This function runs the first time when the element the modifier was applied
* to is inserted into the DOM, and it *autotracks* while running. Any values
* that it accesses will be tracked, including any of its arguments that it
* accesses, and if any of them changes, the function will run again.
*
* **Note:** this will *not* automatically rerun because an argument changes. It
* will only rerun if it is *using* that argument (the same as with auto-tracked
* state in general).
*
* The modifier can also optionally return a *destructor*. The destructor
* function will be run just before the next update, and when the element is
* being removed entirely. It should generally clean up the changes that the
* modifier made in the first place.
*
* @param fn The function which defines the modifier.
* @param options Configuration for the modifier.
*/
// This overload allows users to write types directly on the callback passed to
// the `modifier` function and infer the resulting type correctly.
export default function modifier<
E extends Element,
P extends unknown[],
N extends object = Record<string, unknown>
>(
fn: (element: E, positional: P, named: N) => void | Teardown,
options: { eager: false }
): FunctionBasedModifier<{
Args: { Named: N; Positional: P };
Element: E;
}>;
/**
* An API for writing simple modifiers.
*
* This function runs the first time when the element the modifier was applied
* to is inserted into the DOM, and it *autotracks* while running. Any values
* that it accesses will be tracked, including any of its arguments that it
* accesses, and if any of them changes, the function will run again.
*
* **Note:** this will *not* automatically rerun because an argument changes. It
* will only rerun if it is *using* that argument (the same as with auto-tracked
* state in general).
*
* The modifier can also optionally return a *destructor*. The destructor
* function will be run just before the next update, and when the element is
* being removed entirely. It should generally clean up the changes that the
* modifier made in the first place.
*
* @param fn The function which defines the modifier.
* @param options Configuration for the modifier.
*/
// This overload allows users to provide a `Signature` type explicitly at the
// modifier definition site, e.g. `modifier<Sig>((el, pos, named) => {...})`.
// **Note:** this overload must appear second, since TS' inference engine will
// not correctly infer the type of `S` here from the types on the supplied
// callback.
export default function modifier<S>(
fn: (
element: ElementFor<S>,
positional: PositionalArgs<S>,
named: NamedArgs<S>
) => void | Teardown,
options: { eager: false }
): FunctionBasedModifier<{
Element: ElementFor<S>;
Args: {
Named: NamedArgs<S>;
Positional: PositionalArgs<S>;
};
}>;
// This is the runtime signature; it performs no inference whatsover and just
// uses the simplest version of the invocation possible since, for the case of
// setting it on the modifier manager, we don't *need* any of that info, and
// the two previous overloads capture all invocations from a type perspective.
export default function modifier(
fn: (
element: Element,
positional: unknown[],
named: object
) => void | Teardown,
options: { eager: boolean } = { eager: true }
): FunctionBasedModifier<{
Element: Element;
Args: {
Named: object;
Positional: unknown[];
};
}> {
deprecate(
`ember-modifier (for ${fn.name ?? fn} at ${
new Error().stack
}): creating a function-based modifier without options is deprecated and will be removed at v4.0`,
options === undefined,
{
id: 'ember-modifier.function-based-options',
for: 'ember-modifier',
since: {
available: '3.2.0',
enabled: '3.2.0',
},
until: '4.0.0',
}
);
deprecate(
`ember-modifier (for ${fn.name ?? fn} at ${
new Error().stack
}): creating a function-based modifier with \`{ eager: true }\` is deprecated and will be removed at v4.0`,
options?.eager === true,
{
id: 'ember-modifier.function-based-options',
for: 'ember-modifier',
since: {
available: '3.2.0',
enabled: '3.2.0',
},
until: '4.0.0',
}
);
// SAFETY: the cast here is a *lie*, but it is a useful one. The actual return
// type of `setModifierManager` today is `void`; we pretend it actually
// returns an opaque `Modifier` type so that we can provide a result from this
// type which is useful to TS-aware tooling (e.g. Glint).
return setModifierManager(
() => (options.eager ? EAGER_MANAGER : LAZY_MANAGER),
fn
) as unknown as FunctionBasedModifier<{
Element: Element;
Args: {
Named: object;
Positional: unknown[];
};
}>;
}
/**
* @internal
*/
export type FunctionalModifierDefinition<
S,
E extends ElementFor<S> = ElementFor<S>,
P extends PositionalArgs<S> = PositionalArgs<S>,
N extends NamedArgs<S> = NamedArgs<S>
> = (element: E, positional: P, named: N) => void | Teardown;
/**
* @deprecated Instead of defining a function to match this type, simply define
* a function-based modifier either using a `Signature` or by defining the
* types on the callback passed to the `modifier`.
*/
export type FunctionalModifier<
P extends ModifierArgs['positional'] = ModifierArgs['positional'],
N extends ModifierArgs['named'] = ModifierArgs['named'],
E extends Element = Element
> = (element: E, positional: P, named: N) => unknown;