@preact/signals 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.d.ts +1 -0
- package/dist/signals.js +2 -0
- package/dist/signals.js.map +1 -0
- package/dist/signals.min.js +2 -0
- package/dist/signals.min.js.map +1 -0
- package/dist/signals.mjs +2 -0
- package/dist/signals.modern.js +2 -0
- package/dist/signals.modern.js.map +1 -0
- package/dist/signals.module.js +2 -0
- package/dist/signals.module.js.map +1 -0
- package/package.json +42 -0
- package/src/index.ts +345 -0
- package/src/internal.d.ts +45 -0
- package/test/index.test.ts +149 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022-present Preact Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const bar = 42;
|
package/dist/signals.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signals.js","sources":["../src/index.ts"],"sourcesContent":["export const bar = 42;\n"],"names":[],"mappings":"YAAmB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signals.min.js","sources":["../src/index.ts"],"sourcesContent":["export const bar = 42;\n"],"names":[],"mappings":"6OAAmB"}
|
package/dist/signals.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signals.modern.js","sources":["../src/index.ts"],"sourcesContent":["export const bar = 42;\n"],"names":["bar"],"mappings":"AAAaA,MAAGA,EAAG"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signals.module.js","sources":["../src/index.ts"],"sourcesContent":["export const bar = 42;\n"],"names":["bar"],"mappings":"AAAaA,IAAGA,EAAG"}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@preact/signals",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "",
|
|
6
|
+
"keywords": [],
|
|
7
|
+
"authors": [
|
|
8
|
+
"The Preact Authors (https://github.com/preactjs/signals/contributors)"
|
|
9
|
+
],
|
|
10
|
+
"repository": "preactjs/preact",
|
|
11
|
+
"bugs": "https://github.com/preactjs/signals/issues",
|
|
12
|
+
"homepage": "https://preactjs.com",
|
|
13
|
+
"funding": {
|
|
14
|
+
"type": "opencollective",
|
|
15
|
+
"url": "https://opencollective.com/preact"
|
|
16
|
+
},
|
|
17
|
+
"amdName": "preactSignals",
|
|
18
|
+
"main": "dist/signals.js",
|
|
19
|
+
"module": "dist/signals.module.js",
|
|
20
|
+
"unpkg": "dist/signals.min.js",
|
|
21
|
+
"types": "dist/signals.d.ts",
|
|
22
|
+
"source": "src/index.ts",
|
|
23
|
+
"mangle": "../../mangle.json",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"browser": "./dist/signals.module.js",
|
|
27
|
+
"umd": "./dist/signals.umd.js",
|
|
28
|
+
"import": "./dist/signals.mjs",
|
|
29
|
+
"require": "./dist/signals.js",
|
|
30
|
+
"types": "./dist/signals.d.ts"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@preact/signals-core": "0.0.2"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"preact": "10.x"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"preact": "10.9.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { options, Component, createElement } from "preact";
|
|
2
|
+
import { useRef, useMemo } from "preact/hooks";
|
|
3
|
+
import { signal, computed, effect, Signal } from "@preact/signals-core";
|
|
4
|
+
import {
|
|
5
|
+
VNode,
|
|
6
|
+
ComponentType,
|
|
7
|
+
OptionsTypes,
|
|
8
|
+
HookFn,
|
|
9
|
+
Updater,
|
|
10
|
+
ElementUpdater,
|
|
11
|
+
} from "./internal";
|
|
12
|
+
|
|
13
|
+
// @todo: export Signal only as a type?
|
|
14
|
+
export { signal, computed, effect, Signal };
|
|
15
|
+
|
|
16
|
+
// Components that have a pending Signal update: (used to bypass default sCU:false)
|
|
17
|
+
const hasPendingUpdate = new WeakSet<Component>();
|
|
18
|
+
|
|
19
|
+
// Components that have useState()/useReducer() hooks:
|
|
20
|
+
const hasHookState = new WeakSet<Component>();
|
|
21
|
+
|
|
22
|
+
// Components that have useComputed():
|
|
23
|
+
const hasComputeds = new WeakSet<Component>();
|
|
24
|
+
|
|
25
|
+
// Install a Preact options hook
|
|
26
|
+
function hook<T extends OptionsTypes>(hookName: T, hookFn: HookFn<T>) {
|
|
27
|
+
// @ts-ignore-next-line private options hooks usage
|
|
28
|
+
options[hookName] = hookFn.bind(null, options[hookName] || (() => {}));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let currentComponent: Component | undefined;
|
|
32
|
+
let currentUpdater: Updater | undefined;
|
|
33
|
+
let finishUpdate: ReturnType<Updater["_setCurrent"]> | undefined;
|
|
34
|
+
const updaterForComponent = new WeakMap<Component | VNode, Updater>();
|
|
35
|
+
|
|
36
|
+
function setCurrentUpdater(updater?: Updater) {
|
|
37
|
+
// end tracking for the current update:
|
|
38
|
+
if (finishUpdate) finishUpdate(true, true);
|
|
39
|
+
// start tracking the new update:
|
|
40
|
+
currentUpdater = updater;
|
|
41
|
+
finishUpdate = updater && updater._setCurrent();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function createUpdater(updater: () => void) {
|
|
45
|
+
const s = signal(undefined) as Updater;
|
|
46
|
+
s._canActivate = true;
|
|
47
|
+
s._updater = updater;
|
|
48
|
+
return s;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Get a (cached) Signal property updater for an element VNode
|
|
52
|
+
function getElementUpdater(vnode: VNode) {
|
|
53
|
+
let updater = updaterForComponent.get(vnode) as ElementUpdater;
|
|
54
|
+
if (!updater) {
|
|
55
|
+
let signalProps: string[] = [];
|
|
56
|
+
updater = createUpdater(() => {
|
|
57
|
+
let dom = vnode.__e as Element;
|
|
58
|
+
let props = vnode.props;
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < signalProps.length; i++) {
|
|
61
|
+
let prop = signalProps[i];
|
|
62
|
+
let value = props[prop]._value;
|
|
63
|
+
if (prop in dom) {
|
|
64
|
+
// @ts-ignore-next-line silly
|
|
65
|
+
dom[prop] = value;
|
|
66
|
+
} else if (value) {
|
|
67
|
+
dom.setAttribute(prop, value);
|
|
68
|
+
} else {
|
|
69
|
+
dom.removeAttribute(prop);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}) as ElementUpdater;
|
|
73
|
+
updater._props = signalProps;
|
|
74
|
+
updaterForComponent.set(vnode, updater);
|
|
75
|
+
} else {
|
|
76
|
+
updater._props.length = 0;
|
|
77
|
+
}
|
|
78
|
+
return updater;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** @todo This may be needed for complex prop value detection. */
|
|
82
|
+
// function isSignalValue(value: any): value is Signal {
|
|
83
|
+
// if (typeof value !== "object" || value == null) return false;
|
|
84
|
+
// if (value instanceof Signal) return true;
|
|
85
|
+
// // @TODO: uncomment this when we land Reactive (ideally behind a brand check)
|
|
86
|
+
// // for (let i in value) if (value[i] instanceof Signal) return true;
|
|
87
|
+
// return false;
|
|
88
|
+
// }
|
|
89
|
+
|
|
90
|
+
/** Convert Signals within (nested) props.children into Text components */
|
|
91
|
+
function childToSignal<T>(child: any, i: keyof T, arr: T) {
|
|
92
|
+
if (typeof child !== "object" || child == null) {
|
|
93
|
+
// can't be a signal
|
|
94
|
+
} else if (Array.isArray(child)) {
|
|
95
|
+
child.forEach(childToSignal);
|
|
96
|
+
} else if (child instanceof Signal) {
|
|
97
|
+
// @ts-ignore-next-line yes, arr can accept VNodes:
|
|
98
|
+
arr[i] = createElement(Text, { data: child });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A wrapper component that renders a Signal directly as a Text node.
|
|
104
|
+
* @todo: in Preact 11, just decorate Signal with `type:null`
|
|
105
|
+
*/
|
|
106
|
+
function Text(this: ComponentType, { data }: { data: Signal }) {
|
|
107
|
+
// hasComputeds.add(this);
|
|
108
|
+
|
|
109
|
+
const s = useMemo(() => {
|
|
110
|
+
// mark the parent component as having computeds so it gets optimized
|
|
111
|
+
let v = this.__v;
|
|
112
|
+
while ((v = v.__!)) {
|
|
113
|
+
if (v.__c) {
|
|
114
|
+
hasComputeds.add(v.__c);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Replace this component's vdom updater with a direct text one:
|
|
120
|
+
currentUpdater!._updater = () => {
|
|
121
|
+
(this.base as Text).data = s._value;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return computed(() => {
|
|
125
|
+
let s = data.value;
|
|
126
|
+
return s === 0 ? 0 : s === true ? "" : s || "";
|
|
127
|
+
});
|
|
128
|
+
}, []);
|
|
129
|
+
|
|
130
|
+
return s.value;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Inject low-level property/attribute bindings for Signals into Preact's diff */
|
|
134
|
+
hook(OptionsTypes.DIFF, (old, vnode) => {
|
|
135
|
+
if (typeof vnode.type === "string") {
|
|
136
|
+
// let orig = vnode.__o || vnode;
|
|
137
|
+
let props = vnode.props;
|
|
138
|
+
let updater;
|
|
139
|
+
|
|
140
|
+
for (let i in props) {
|
|
141
|
+
let value = props[i];
|
|
142
|
+
if (i === "children") {
|
|
143
|
+
childToSignal(value, "children", props);
|
|
144
|
+
} else if (value instanceof Signal) {
|
|
145
|
+
// first Signal prop triggers creation/cleanup of the updater:
|
|
146
|
+
if (!updater) updater = getElementUpdater(vnode);
|
|
147
|
+
// track which props are Signals for precise updates:
|
|
148
|
+
updater._props.push(i);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
setCurrentUpdater(updater);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
old(vnode);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
/** Set up Updater before rendering a component */
|
|
159
|
+
hook(OptionsTypes.RENDER, (old, vnode) => {
|
|
160
|
+
let updater;
|
|
161
|
+
|
|
162
|
+
let component = vnode.__c;
|
|
163
|
+
if (component) {
|
|
164
|
+
hasPendingUpdate.delete(component);
|
|
165
|
+
|
|
166
|
+
updater = updaterForComponent.get(component);
|
|
167
|
+
if (updater === undefined) {
|
|
168
|
+
updater = createUpdater(() => {
|
|
169
|
+
hasPendingUpdate.add(component);
|
|
170
|
+
component.setState({});
|
|
171
|
+
});
|
|
172
|
+
updaterForComponent.set(component, updater);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
currentComponent = component;
|
|
177
|
+
setCurrentUpdater(updater);
|
|
178
|
+
old(vnode);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
/** Finish current updater if a component errors */
|
|
182
|
+
hook(OptionsTypes.CATCH_ERROR, (old, error, vnode, oldVNode) => {
|
|
183
|
+
setCurrentUpdater();
|
|
184
|
+
currentComponent = undefined;
|
|
185
|
+
old(error, vnode, oldVNode);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
/** Finish current updater after rendering any VNode */
|
|
189
|
+
hook(OptionsTypes.DIFFED, (old, vnode) => {
|
|
190
|
+
setCurrentUpdater();
|
|
191
|
+
currentComponent = undefined;
|
|
192
|
+
old(vnode);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
/** Unsubscribe from Signals when unmounting components/vnodes */
|
|
196
|
+
hook(OptionsTypes.UNMOUNT, (old, vnode: VNode) => {
|
|
197
|
+
let thing = vnode.__c || vnode;
|
|
198
|
+
const updater = updaterForComponent.get(thing);
|
|
199
|
+
if (updater) {
|
|
200
|
+
updaterForComponent.delete(thing);
|
|
201
|
+
const signals = updater._deps;
|
|
202
|
+
if (signals) {
|
|
203
|
+
signals.forEach(signal => signal._subs.delete(updater));
|
|
204
|
+
signals.clear();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
old(vnode);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
/** Mark components that use hook state so we can skip sCU optimization. */
|
|
211
|
+
hook(OptionsTypes.HOOK, (old, component, index, type) => {
|
|
212
|
+
if (type < 3) hasHookState.add(component);
|
|
213
|
+
old(component, index, type);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Auto-memoize components that use Signals/Computeds.
|
|
218
|
+
* Note: Does _not_ optimize components that use hook/class state.
|
|
219
|
+
*/
|
|
220
|
+
Component.prototype.shouldComponentUpdate = function (props, state) {
|
|
221
|
+
// @todo: Once preactjs/preact#3671 lands, this could just use `currentUpdater`:
|
|
222
|
+
const updater = updaterForComponent.get(this);
|
|
223
|
+
|
|
224
|
+
const hasSignals = updater && updater._deps?.size !== 0;
|
|
225
|
+
|
|
226
|
+
// let reason;
|
|
227
|
+
// if (!hasSignals && !hasComputeds.has(this)) {
|
|
228
|
+
// reason = "no signals or computeds";
|
|
229
|
+
// } else if (hasPendingUpdate.has(this)) {
|
|
230
|
+
// reason = "has pending update";
|
|
231
|
+
// } else if (hasHookState.has(this)) {
|
|
232
|
+
// reason = "has hook state";
|
|
233
|
+
// }
|
|
234
|
+
// if (reason) {
|
|
235
|
+
// if (!this) reason += " (`this` bug)";
|
|
236
|
+
// console.log("not optimizing", this?.constructor?.name, ": ", reason, {
|
|
237
|
+
// details: {
|
|
238
|
+
// hasSignals,
|
|
239
|
+
// hasComputeds: hasComputeds.has(this),
|
|
240
|
+
// hasPendingUpdate: hasPendingUpdate.has(this),
|
|
241
|
+
// hasHookState: hasHookState.has(this),
|
|
242
|
+
// deps: Array.from(updater._deps),
|
|
243
|
+
// updater,
|
|
244
|
+
// },
|
|
245
|
+
// });
|
|
246
|
+
// }
|
|
247
|
+
|
|
248
|
+
// if this component used no signals or computeds, update:
|
|
249
|
+
if (!hasSignals && !hasComputeds.has(this)) return true;
|
|
250
|
+
|
|
251
|
+
// if there is a pending re-render triggered from Signals, update:
|
|
252
|
+
if (hasPendingUpdate.has(this)) return true;
|
|
253
|
+
|
|
254
|
+
// if there is hook or class state, update:
|
|
255
|
+
if (hasHookState.has(this)) return true;
|
|
256
|
+
for (let i in state) return true;
|
|
257
|
+
|
|
258
|
+
// if any non-Signal props changed, update:
|
|
259
|
+
for (let i in props) {
|
|
260
|
+
if (i !== "__source" && props[i] !== this.props[i]) return true;
|
|
261
|
+
}
|
|
262
|
+
for (let i in this.props) if (!(i in props)) return true;
|
|
263
|
+
|
|
264
|
+
// this is a purely Signal-driven component, don't update:
|
|
265
|
+
return false;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
export function useSignal<T>(value: T) {
|
|
269
|
+
return useMemo(() => signal<T>(value), []);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function useComputed<T>(compute: () => T) {
|
|
273
|
+
const $compute = useRef(compute);
|
|
274
|
+
$compute.current = compute;
|
|
275
|
+
hasComputeds.add(currentComponent!);
|
|
276
|
+
return useMemo(() => computed<T>(() => $compute.current()), []);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* @todo Determine which Reactive implementation we'll be using.
|
|
281
|
+
* @internal
|
|
282
|
+
*/
|
|
283
|
+
// export function useReactive<T extends object>(value: T): Reactive<T> {
|
|
284
|
+
// return useMemo(() => reactive<T>(value), []);
|
|
285
|
+
// }
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* @internal
|
|
289
|
+
* Update a Reactive's using the properties of an object or other Reactive.
|
|
290
|
+
* Also works for Signals.
|
|
291
|
+
* @example
|
|
292
|
+
* // Update a Reactive with Object.assign()-like syntax:
|
|
293
|
+
* const r = reactive({ name: "Alice" });
|
|
294
|
+
* update(r, { name: "Bob" });
|
|
295
|
+
* update(r, { age: 42 }); // property 'age' does not exist in type '{ name?: string }'
|
|
296
|
+
* update(r, 2); // '2' has no properties in common with '{ name?: string }'
|
|
297
|
+
* console.log(r.name.value); // "Bob"
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* // Update a Reactive with the properties of another Reactive:
|
|
301
|
+
* const A = reactive({ name: "Alice" });
|
|
302
|
+
* const B = reactive({ name: "Bob", age: 42 });
|
|
303
|
+
* update(A, B);
|
|
304
|
+
* console.log(`${A.name} is ${A.age}`); // "Bob is 42"
|
|
305
|
+
*
|
|
306
|
+
* @example
|
|
307
|
+
* // Update a signal with assign()-like syntax:
|
|
308
|
+
* const s = signal(42);
|
|
309
|
+
* update(s, "hi"); // Argument type 'string' not assignable to type 'number'
|
|
310
|
+
* update(s, {}); // Argument type '{}' not assignable to type 'number'
|
|
311
|
+
* update(s, 43);
|
|
312
|
+
* console.log(s.value); // 43
|
|
313
|
+
*
|
|
314
|
+
* @param obj The Reactive or Signal to be updated
|
|
315
|
+
* @param update The value, Signal, object or Reactive to update `obj` to match
|
|
316
|
+
* @param overwrite If `true`, any properties `obj` missing from `update` are set to `undefined`
|
|
317
|
+
*/
|
|
318
|
+
/*
|
|
319
|
+
export function update<T extends SignalOrReactive>(
|
|
320
|
+
obj: T,
|
|
321
|
+
update: Partial<Unwrap<T>>,
|
|
322
|
+
overwrite = false
|
|
323
|
+
) {
|
|
324
|
+
if (obj instanceof Signal) {
|
|
325
|
+
obj.value = peekValue(update);
|
|
326
|
+
} else {
|
|
327
|
+
for (let i in update) {
|
|
328
|
+
if (i in obj) {
|
|
329
|
+
obj[i].value = peekValue(update[i]);
|
|
330
|
+
} else {
|
|
331
|
+
let sig = signal(peekValue(update[i]));
|
|
332
|
+
sig[KEY] = i;
|
|
333
|
+
obj[i] = sig;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (overwrite) {
|
|
337
|
+
for (let i in obj) {
|
|
338
|
+
if (!(i in update)) {
|
|
339
|
+
obj[i].value = undefined;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
*/
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Component } from "preact";
|
|
2
|
+
import { Signal } from "@preact/signals-core";
|
|
3
|
+
|
|
4
|
+
export interface VNode<P = any> extends preact.VNode<P> {
|
|
5
|
+
/** The component instance for this VNode */
|
|
6
|
+
__c: Component;
|
|
7
|
+
/** The parent VNode */
|
|
8
|
+
__?: VNode;
|
|
9
|
+
/** The DOM node for this VNode */
|
|
10
|
+
__e?: Element | Text;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ComponentType extends Component {
|
|
14
|
+
/** This component's owner VNode */
|
|
15
|
+
__v: VNode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type Updater = Signal<unknown>;
|
|
19
|
+
|
|
20
|
+
export interface ElementUpdater extends Updater {
|
|
21
|
+
_props: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const enum OptionsTypes {
|
|
25
|
+
HOOK = "__h",
|
|
26
|
+
DIFF = "__b",
|
|
27
|
+
DIFFED = "diffed",
|
|
28
|
+
RENDER = "__r",
|
|
29
|
+
CATCH_ERROR = "__e",
|
|
30
|
+
UNMOUNT = "unmount",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface OptionsType {
|
|
34
|
+
[OptionsTypes.HOOK](component: Component, index: number, type: number): void;
|
|
35
|
+
[OptionsTypes.DIFF](vnode: VNode): void;
|
|
36
|
+
[OptionsTypes.DIFFED](vnode: VNode): void;
|
|
37
|
+
[OptionsTypes.RENDER](vnode: VNode): void;
|
|
38
|
+
[OptionsTypes.CATCH_ERROR](error: any, vnode: VNode, oldVNode: VNode): void;
|
|
39
|
+
[OptionsTypes.UNMOUNT](vnode: VNode): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type HookFn<T extends keyof OptionsType> = (
|
|
43
|
+
old: OptionsType[T],
|
|
44
|
+
...a: Parameters<OptionsType[T]>
|
|
45
|
+
) => ReturnType<OptionsType[T]>;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { signal, useComputed } from "@preact/signals";
|
|
2
|
+
import { h, render } from "preact";
|
|
3
|
+
import { useMemo } from "preact/hooks";
|
|
4
|
+
import { setupRerender } from "preact/test-utils";
|
|
5
|
+
|
|
6
|
+
describe("@preact/signals", () => {
|
|
7
|
+
let scratch: HTMLDivElement;
|
|
8
|
+
let rerender: () => void;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
scratch = document.createElement("div");
|
|
12
|
+
rerender = setupRerender();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
render(null, scratch);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("Text bindings", () => {
|
|
20
|
+
it("should render text without signals", () => {
|
|
21
|
+
render(h("span", null, "test"), scratch);
|
|
22
|
+
const span = scratch.firstChild;
|
|
23
|
+
const text = span?.firstChild;
|
|
24
|
+
expect(text).to.have.property("data", "test");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should render Signals as Text", () => {
|
|
28
|
+
const sig = signal("test");
|
|
29
|
+
render(h("span", null, sig), scratch);
|
|
30
|
+
const span = scratch.firstChild;
|
|
31
|
+
expect(span).to.have.property("firstChild").that.is.an.instanceOf(Text);
|
|
32
|
+
const text = span?.firstChild;
|
|
33
|
+
expect(text).to.have.property("data", "test");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should update Signal-based Text (no parent component)", () => {
|
|
37
|
+
const sig = signal("test");
|
|
38
|
+
render(h("span", null, sig), scratch);
|
|
39
|
+
|
|
40
|
+
const text = scratch.firstChild!.firstChild!;
|
|
41
|
+
expect(text).to.have.property("data", "test");
|
|
42
|
+
|
|
43
|
+
sig.value = "changed";
|
|
44
|
+
|
|
45
|
+
// should not remount/replace Text
|
|
46
|
+
expect(scratch.firstChild!.firstChild!).to.equal(text);
|
|
47
|
+
// should update the text in-place
|
|
48
|
+
expect(text).to.have.property("data", "changed");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should update Signal-based Text (in a parent component)", () => {
|
|
52
|
+
const sig = signal("test");
|
|
53
|
+
function App({ x }: { x: typeof sig }) {
|
|
54
|
+
return h("span", null, x);
|
|
55
|
+
}
|
|
56
|
+
render(h(App, { x: sig }), scratch);
|
|
57
|
+
|
|
58
|
+
const text = scratch.firstChild!.firstChild!;
|
|
59
|
+
expect(text).to.have.property("data", "test");
|
|
60
|
+
|
|
61
|
+
sig.value = "changed";
|
|
62
|
+
|
|
63
|
+
// should not remount/replace Text
|
|
64
|
+
expect(scratch.firstChild!.firstChild!).to.equal(text);
|
|
65
|
+
// should update the text in-place
|
|
66
|
+
expect(text).to.have.property("data", "changed");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("Component bindings", () => {
|
|
71
|
+
it("should subscribe to signals", () => {
|
|
72
|
+
const sig = signal("foo");
|
|
73
|
+
|
|
74
|
+
function App() {
|
|
75
|
+
const value = sig.value;
|
|
76
|
+
return h("p", null, value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
render(h(App, {}), scratch);
|
|
80
|
+
expect(scratch.textContent).to.equal("foo");
|
|
81
|
+
|
|
82
|
+
sig.value = "bar";
|
|
83
|
+
rerender();
|
|
84
|
+
expect(scratch.textContent).to.equal("bar");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should activate signal accessed in render", () => {
|
|
88
|
+
const sig = signal(null);
|
|
89
|
+
|
|
90
|
+
function App() {
|
|
91
|
+
const arr = useComputed(() => {
|
|
92
|
+
// trigger read
|
|
93
|
+
sig.value;
|
|
94
|
+
|
|
95
|
+
return [];
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const str = arr.value.join(", ");
|
|
99
|
+
return h("p", null, str);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const fn = () => render(h(App, {}), scratch);
|
|
103
|
+
expect(fn).not.to.throw;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should not subscribe to child signals", () => {
|
|
107
|
+
const sig = signal("foo");
|
|
108
|
+
|
|
109
|
+
function Child() {
|
|
110
|
+
const value = sig.value;
|
|
111
|
+
return h("p", null, value);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const spy = sinon.spy();
|
|
115
|
+
function App() {
|
|
116
|
+
spy();
|
|
117
|
+
return h(Child, null);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
render(h(App, {}), scratch);
|
|
121
|
+
expect(scratch.textContent).to.equal("foo");
|
|
122
|
+
|
|
123
|
+
sig.value = "bar";
|
|
124
|
+
rerender();
|
|
125
|
+
expect(spy).to.be.calledOnce;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should update memo'ed component via signals", async () => {
|
|
129
|
+
const sig = signal("foo");
|
|
130
|
+
|
|
131
|
+
function Inner() {
|
|
132
|
+
const value = sig.value;
|
|
133
|
+
return h("p", null, value);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function App() {
|
|
137
|
+
sig.value;
|
|
138
|
+
return useMemo(() => h(Inner, { foo: 1 }), []);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
render(h(App, {}), scratch);
|
|
142
|
+
expect(scratch.textContent).to.equal("foo");
|
|
143
|
+
|
|
144
|
+
sig.value = "bar";
|
|
145
|
+
rerender();
|
|
146
|
+
expect(scratch.textContent).to.equal("bar");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|