@praxisjs/runtime 0.6.0 → 0.7.0
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/CHANGELOG.md +15 -0
- package/dist/__tests__/children.test.js +54 -0
- package/dist/__tests__/children.test.js.map +1 -1
- package/dist/__tests__/component.test.js +60 -1
- package/dist/__tests__/component.test.js.map +1 -1
- package/dist/__tests__/dom.test.js +100 -0
- package/dist/__tests__/dom.test.js.map +1 -1
- package/dist/__tests__/portal.test.js +22 -0
- package/dist/__tests__/portal.test.js.map +1 -1
- package/dist/__tests__/remove.test.d.ts +2 -0
- package/dist/__tests__/remove.test.d.ts.map +1 -0
- package/dist/__tests__/remove.test.js +45 -0
- package/dist/__tests__/remove.test.js.map +1 -0
- package/dist/component.d.ts.map +1 -1
- package/dist/component.js +22 -1
- package/dist/component.js.map +1 -1
- package/dist/dom/events.d.ts.map +1 -1
- package/dist/dom/events.js +18 -2
- package/dist/dom/events.js.map +1 -1
- package/dist/dom/props.d.ts.map +1 -1
- package/dist/dom/props.js +27 -5
- package/dist/dom/props.js.map +1 -1
- package/dist/dom/remove.d.ts +2 -0
- package/dist/dom/remove.d.ts.map +1 -0
- package/dist/dom/remove.js +23 -0
- package/dist/dom/remove.js.map +1 -0
- package/dist/portal.d.ts.map +1 -1
- package/dist/portal.js +8 -7
- package/dist/portal.js.map +1 -1
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +6 -10
- package/dist/reactive.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/children.test.ts +58 -0
- package/src/__tests__/component.test.ts +67 -1
- package/src/__tests__/dom.test.ts +108 -0
- package/src/__tests__/portal.test.ts +24 -0
- package/src/__tests__/remove.test.ts +51 -0
- package/src/component.ts +23 -1
- package/src/dom/events.ts +21 -2
- package/src/dom/props.ts +30 -5
- package/src/dom/remove.ts +22 -0
- package/src/portal.ts +8 -7
- package/src/reactive.ts +5 -9
package/src/dom/events.ts
CHANGED
|
@@ -1,11 +1,30 @@
|
|
|
1
|
+
import { batch } from "@praxisjs/core/internal";
|
|
2
|
+
|
|
1
3
|
import type { Scope } from "../scope";
|
|
2
4
|
|
|
5
|
+
// One wrapper per handler keeps addEventListener's dedupe of an identical listener intact.
|
|
6
|
+
const batchedHandlers = new WeakMap<EventListener, EventListener>();
|
|
7
|
+
|
|
8
|
+
// Handlers run inside batch() so several signal writes in one event flush their effects once,
|
|
9
|
+
// instead of re-running the same reactive subtree after every write.
|
|
10
|
+
function batched(handler: EventListener): EventListener {
|
|
11
|
+
let wrapper = batchedHandlers.get(handler);
|
|
12
|
+
if (!wrapper) {
|
|
13
|
+
wrapper = function (this: Element, event) {
|
|
14
|
+
batch(() => { handler.call(this, event); });
|
|
15
|
+
};
|
|
16
|
+
batchedHandlers.set(handler, wrapper);
|
|
17
|
+
}
|
|
18
|
+
return wrapper;
|
|
19
|
+
}
|
|
20
|
+
|
|
3
21
|
export function addEvent(
|
|
4
22
|
el: Element,
|
|
5
23
|
eventName: string,
|
|
6
24
|
handler: EventListener,
|
|
7
25
|
scope: Scope,
|
|
8
26
|
): void {
|
|
9
|
-
|
|
10
|
-
|
|
27
|
+
const listener = batched(handler);
|
|
28
|
+
el.addEventListener(eventName, listener);
|
|
29
|
+
scope.add(() => { el.removeEventListener(eventName, listener); });
|
|
11
30
|
}
|
package/src/dom/props.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { untrack } from "@praxisjs/core/internal";
|
|
2
|
+
|
|
1
3
|
import { EVENT_MAP } from "./constants";
|
|
2
4
|
import { addEvent } from "./events";
|
|
3
5
|
|
|
@@ -42,10 +44,10 @@ function applyAttr(el: Element, key: string, value: unknown): void {
|
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
function
|
|
48
|
-
let target: object | null =
|
|
47
|
+
const writableCache = new WeakMap<object, Map<string, boolean>>();
|
|
48
|
+
|
|
49
|
+
function lookupWritable(proto: object, key: string): boolean {
|
|
50
|
+
let target: object | null = proto;
|
|
49
51
|
while (target) {
|
|
50
52
|
const descriptor = Object.getOwnPropertyDescriptor(target, key);
|
|
51
53
|
if (descriptor) {
|
|
@@ -56,6 +58,28 @@ function hasWritableProperty(el: Element, key: string): boolean {
|
|
|
56
58
|
return false;
|
|
57
59
|
}
|
|
58
60
|
|
|
61
|
+
// Read-only accessors (SVG geometry, `list`, `form`, `part`, `classList`…)
|
|
62
|
+
// must be excluded here or assignment below throws. The prototype walk is cached per
|
|
63
|
+
// prototype because every element of a given tag repeats the same lookups; a property defined on
|
|
64
|
+
// a prototype after its first lookup (e.g. a late polyfill) is therefore not picked up.
|
|
65
|
+
function hasWritableProperty(el: Element, key: string): boolean {
|
|
66
|
+
const own = Object.getOwnPropertyDescriptor(el, key);
|
|
67
|
+
if (own) return own.set !== undefined || own.writable === true;
|
|
68
|
+
|
|
69
|
+
const proto = Object.getPrototypeOf(el) as object;
|
|
70
|
+
let keys = writableCache.get(proto);
|
|
71
|
+
if (!keys) {
|
|
72
|
+
keys = new Map();
|
|
73
|
+
writableCache.set(proto, keys);
|
|
74
|
+
}
|
|
75
|
+
let writable = keys.get(key);
|
|
76
|
+
if (writable === undefined) {
|
|
77
|
+
writable = lookupWritable(proto, key);
|
|
78
|
+
keys.set(key, writable);
|
|
79
|
+
}
|
|
80
|
+
return writable;
|
|
81
|
+
}
|
|
82
|
+
|
|
59
83
|
function setProp(el: Element, key: string, value: unknown): void {
|
|
60
84
|
if (key === "class" || key === "className") {
|
|
61
85
|
applyClass(el, value);
|
|
@@ -81,7 +105,8 @@ export function applyProp(
|
|
|
81
105
|
if (normalizedKey === "key" || normalizedKey === "children") return;
|
|
82
106
|
|
|
83
107
|
if (normalizedKey === "ref") {
|
|
84
|
-
|
|
108
|
+
// untracked: a ref callback that reads a signal must not subscribe the enclosing reactive child
|
|
109
|
+
untrack(() => { (value as (el: Element) => void)(el); });
|
|
85
110
|
return;
|
|
86
111
|
}
|
|
87
112
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Replaces Range.deleteContents(): creating a Range per removal is several times slower than
|
|
2
|
+
// walking the siblings, and unreachable Ranges stay registered with the document until collected.
|
|
3
|
+
// Walking first → last (rather than only the tracked nodes) also removes anything a nested reactive
|
|
4
|
+
// child inserted between them after mount, which is what the Range used to cover.
|
|
5
|
+
export function removeNodes(nodes: Node[]): void {
|
|
6
|
+
const first = nodes[0];
|
|
7
|
+
const last = nodes[nodes.length - 1];
|
|
8
|
+
const parent = first.parentNode;
|
|
9
|
+
|
|
10
|
+
if (parent === null || last.parentNode !== parent) {
|
|
11
|
+
for (const node of nodes) node.parentNode?.removeChild(node);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let node: Node | null = first;
|
|
16
|
+
while (node) {
|
|
17
|
+
const next: Node | null = node.nextSibling;
|
|
18
|
+
parent.removeChild(node);
|
|
19
|
+
if (node === last) return;
|
|
20
|
+
node = next;
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/portal.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { isServerRenderPass } from "@praxisjs/core/internal";
|
|
|
3
3
|
|
|
4
4
|
import { mountChildren } from "./children";
|
|
5
5
|
import { getCurrentScope } from "./context";
|
|
6
|
+
import { removeNodes } from "./dom/remove";
|
|
6
7
|
|
|
7
8
|
export interface PortalProps {
|
|
8
9
|
to?: Element | string | null;
|
|
@@ -27,16 +28,16 @@ export class Portal extends StatelessComponent<PortalProps> {
|
|
|
27
28
|
const start = document.createComment("");
|
|
28
29
|
const end = document.createComment("");
|
|
29
30
|
|
|
30
|
-
target.
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
// Built off-document and attached once, so the target never sees a half-built subtree.
|
|
32
|
+
const fragment = document.createDocumentFragment();
|
|
33
|
+
fragment.appendChild(start);
|
|
34
|
+
mountChildren(fragment, this.props.children, scope);
|
|
35
|
+
fragment.appendChild(end);
|
|
36
|
+
target.appendChild(fragment);
|
|
33
37
|
|
|
34
38
|
scope.add(() => {
|
|
35
39
|
if (!start.parentNode || start.parentNode !== end.parentNode) return;
|
|
36
|
-
|
|
37
|
-
range.setStartBefore(start);
|
|
38
|
-
range.setEndAfter(end);
|
|
39
|
-
range.deleteContents();
|
|
40
|
+
removeNodes([start, end]);
|
|
40
41
|
});
|
|
41
42
|
|
|
42
43
|
return document.createComment("portal");
|
package/src/reactive.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { runInScope } from "./context";
|
|
2
|
+
import { removeNodes } from "./dom/remove";
|
|
2
3
|
import { isRecording, registerReactiveNode } from "./hydration-context";
|
|
3
4
|
import { Scope } from "./scope";
|
|
4
5
|
|
|
@@ -49,15 +50,10 @@ export function mountReactive(
|
|
|
49
50
|
|
|
50
51
|
const anchor = end.parentNode ?? parent;
|
|
51
52
|
|
|
52
|
-
if (currentNodes.length
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const range = document.createRange();
|
|
57
|
-
range.setStartBefore(currentNodes[0]);
|
|
58
|
-
range.setEndAfter(currentNodes[currentNodes.length - 1]);
|
|
59
|
-
range.deleteContents();
|
|
60
|
-
}
|
|
53
|
+
if (currentNodes.length === 1) {
|
|
54
|
+
(anchor as Element).removeChild(currentNodes[0]);
|
|
55
|
+
} else if (currentNodes.length > 1) {
|
|
56
|
+
removeNodes(currentNodes);
|
|
61
57
|
}
|
|
62
58
|
|
|
63
59
|
if (newNodes.length > 0) {
|