@praxisjs/runtime 0.6.0 → 0.7.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/CHANGELOG.md +21 -0
- package/dist/__tests__/children.test.js +54 -0
- package/dist/__tests__/children.test.js.map +1 -1
- package/dist/__tests__/component.test.js +76 -1
- package/dist/__tests__/component.test.js.map +1 -1
- package/dist/__tests__/dom.test.js +144 -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 +1 -1
- package/dist/dom/events.d.ts.map +1 -1
- package/dist/dom/events.js +22 -2
- package/dist/dom/events.js.map +1 -1
- package/dist/dom/props.d.ts.map +1 -1
- package/dist/dom/props.js +28 -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 +4 -4
- package/src/__tests__/children.test.ts +58 -0
- package/src/__tests__/component.test.ts +85 -1
- package/src/__tests__/dom.test.ts +157 -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 +25 -3
- 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
|
@@ -323,4 +323,161 @@ describe("addEvent", () => {
|
|
|
323
323
|
expect(fn).toHaveBeenCalledOnce();
|
|
324
324
|
scope.dispose();
|
|
325
325
|
});
|
|
326
|
+
|
|
327
|
+
it("batches signal writes made by a handler so effects run once", () => {
|
|
328
|
+
const el = document.createElement("button");
|
|
329
|
+
const scope = new Scope();
|
|
330
|
+
const a = signal(0);
|
|
331
|
+
const b = signal(0);
|
|
332
|
+
const runs = vi.fn();
|
|
333
|
+
scope.effect(() => {
|
|
334
|
+
void a();
|
|
335
|
+
void b();
|
|
336
|
+
runs();
|
|
337
|
+
});
|
|
338
|
+
runs.mockClear();
|
|
339
|
+
addEvent(el, "click", () => {
|
|
340
|
+
a.set(1);
|
|
341
|
+
b.set(1);
|
|
342
|
+
}, scope);
|
|
343
|
+
el.dispatchEvent(new MouseEvent("click"));
|
|
344
|
+
expect(runs).toHaveBeenCalledOnce();
|
|
345
|
+
scope.dispose();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("calls the handler with the element as this and forwards the event", () => {
|
|
349
|
+
const el = document.createElement("button");
|
|
350
|
+
const scope = new Scope();
|
|
351
|
+
let seenThis: unknown;
|
|
352
|
+
let seenEvent: Event | undefined;
|
|
353
|
+
addEvent(el, "click", function (this: unknown, e: Event) {
|
|
354
|
+
seenThis = this;
|
|
355
|
+
seenEvent = e;
|
|
356
|
+
}, scope);
|
|
357
|
+
const event = new MouseEvent("click");
|
|
358
|
+
el.dispatchEvent(event);
|
|
359
|
+
expect(seenThis).toBe(el);
|
|
360
|
+
expect(seenEvent).toBe(event);
|
|
361
|
+
scope.dispose();
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("registers the same handler on different elements independently", () => {
|
|
365
|
+
const a = document.createElement("button");
|
|
366
|
+
const b = document.createElement("button");
|
|
367
|
+
const scope = new Scope();
|
|
368
|
+
const fn = vi.fn();
|
|
369
|
+
addEvent(a, "click", fn, scope);
|
|
370
|
+
addEvent(b, "click", fn, scope);
|
|
371
|
+
a.dispatchEvent(new MouseEvent("click"));
|
|
372
|
+
b.dispatchEvent(new MouseEvent("click"));
|
|
373
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
374
|
+
scope.dispose();
|
|
375
|
+
a.dispatchEvent(new MouseEvent("click"));
|
|
376
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe("applyProp writable-property cache", () => {
|
|
381
|
+
it("assigns prototype properties and keeps doing so for later elements of the same tag", () => {
|
|
382
|
+
const scope = new Scope();
|
|
383
|
+
const first = document.createElement("input");
|
|
384
|
+
const second = document.createElement("input");
|
|
385
|
+
applyProp(first, "value", "a", scope);
|
|
386
|
+
applyProp(second, "value", "b", scope);
|
|
387
|
+
expect(first.value).toBe("a");
|
|
388
|
+
expect(second.value).toBe("b");
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("falls back to attributes for unknown and read-only properties, consistently across elements", () => {
|
|
392
|
+
const scope = new Scope();
|
|
393
|
+
for (let i = 0; i < 2; i++) {
|
|
394
|
+
const input = document.createElement("input");
|
|
395
|
+
applyProp(input, "data-x", "1", scope);
|
|
396
|
+
applyProp(input, "list", "dl", scope);
|
|
397
|
+
expect(input.getAttribute("data-x")).toBe("1");
|
|
398
|
+
expect(input.getAttribute("list")).toBe("dl");
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("honours own properties: writable ones are assigned, getter-only ones become attributes", () => {
|
|
403
|
+
const scope = new Scope();
|
|
404
|
+
const el = document.createElement("div") as unknown as HTMLElement & Record<string, unknown>;
|
|
405
|
+
el.expando = "old";
|
|
406
|
+
Object.defineProperty(el, "readonlyThing", { get: () => "fixed", configurable: true });
|
|
407
|
+
applyProp(el, "expando", "new", scope);
|
|
408
|
+
applyProp(el, "readonlyThing", "attr", scope);
|
|
409
|
+
expect(el.expando).toBe("new");
|
|
410
|
+
expect(el.readonlyThing).toBe("fixed");
|
|
411
|
+
expect(el.getAttribute("readonlyThing")).toBe("attr");
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("an own property with a setter is assigned", () => {
|
|
415
|
+
const scope = new Scope();
|
|
416
|
+
const el = document.createElement("div") as unknown as HTMLElement & Record<string, unknown>;
|
|
417
|
+
let stored = "";
|
|
418
|
+
Object.defineProperty(el, "custom", { get: () => stored, set: (v: string) => { stored = v; }, configurable: true });
|
|
419
|
+
applyProp(el, "custom", "set", scope);
|
|
420
|
+
expect(stored).toBe("set");
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("runs ref callbacks untracked so they do not subscribe the enclosing effect", () => {
|
|
424
|
+
const scope = new Scope();
|
|
425
|
+
const s = signal(0);
|
|
426
|
+
const runs = vi.fn();
|
|
427
|
+
scope.effect(() => {
|
|
428
|
+
runs();
|
|
429
|
+
applyProp(document.createElement("div"), "ref", () => { void s(); }, scope);
|
|
430
|
+
});
|
|
431
|
+
s.set(1);
|
|
432
|
+
expect(runs).toHaveBeenCalledOnce();
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
describe("event props without a usable handler", () => {
|
|
437
|
+
it.each([undefined, null])("addEvent ignores a %s handler", (handler) => {
|
|
438
|
+
const el = document.createElement("button");
|
|
439
|
+
const scope = new Scope();
|
|
440
|
+
const add = vi.spyOn(el, "addEventListener");
|
|
441
|
+
expect(() => { addEvent(el, "click", handler, scope); }).not.toThrow();
|
|
442
|
+
expect(add).not.toHaveBeenCalled();
|
|
443
|
+
expect(() => { scope.dispose(); }).not.toThrow();
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it.each([undefined, null])("applyProp skips an onClick prop whose value is %s", (value) => {
|
|
447
|
+
const el = document.createElement("button");
|
|
448
|
+
const scope = new Scope();
|
|
449
|
+
expect(() => { applyProp(el, "onClick", value, scope); }).not.toThrow();
|
|
450
|
+
expect(() => { el.dispatchEvent(new MouseEvent("click")); }).not.toThrow();
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("still registers a handler passed later in the same props object", () => {
|
|
454
|
+
const el = document.createElement("form");
|
|
455
|
+
const scope = new Scope();
|
|
456
|
+
const onInput = vi.fn();
|
|
457
|
+
applyProp(el, "onSubmit", undefined, scope);
|
|
458
|
+
applyProp(el, "onInput", onInput, scope);
|
|
459
|
+
el.dispatchEvent(new Event("input"));
|
|
460
|
+
expect(onInput).toHaveBeenCalledOnce();
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it("supports an EventListenerObject, calling handleEvent as the DOM does", () => {
|
|
464
|
+
const el = document.createElement("button");
|
|
465
|
+
const scope = new Scope();
|
|
466
|
+
const handleEvent = vi.fn();
|
|
467
|
+
const listener = { handleEvent };
|
|
468
|
+
addEvent(el, "click", listener, scope);
|
|
469
|
+
const event = new MouseEvent("click");
|
|
470
|
+
el.dispatchEvent(event);
|
|
471
|
+
expect(handleEvent).toHaveBeenCalledWith(event);
|
|
472
|
+
scope.dispose();
|
|
473
|
+
el.dispatchEvent(new MouseEvent("click"));
|
|
474
|
+
expect(handleEvent).toHaveBeenCalledOnce();
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
describe("ref prop without a callback", () => {
|
|
479
|
+
it.each([undefined, null])("applyProp ignores a ref whose value is %s", (value) => {
|
|
480
|
+
const el = document.createElement("div");
|
|
481
|
+
expect(() => { applyProp(el, "ref", value, new Scope()); }).not.toThrow();
|
|
482
|
+
});
|
|
326
483
|
});
|
|
@@ -150,4 +150,28 @@ describe("resolvePortalTarget", () => {
|
|
|
150
150
|
expect(resolvePortalTarget("#resolve-test")).toBe(el);
|
|
151
151
|
document.body.removeChild(el);
|
|
152
152
|
});
|
|
153
|
+
|
|
154
|
+
it("attaches the whole subtree to the target in one operation", () => {
|
|
155
|
+
const target = document.createElement("div");
|
|
156
|
+
document.body.appendChild(target);
|
|
157
|
+
const spy = vi.spyOn(target, "appendChild");
|
|
158
|
+
const children = [document.createElement("p"), document.createElement("span")];
|
|
159
|
+
const { scope } = mount({ to: target, children });
|
|
160
|
+
expect(spy).toHaveBeenCalledOnce();
|
|
161
|
+
expect(target.querySelector("p")).not.toBeNull();
|
|
162
|
+
expect(target.querySelector("span")).not.toBeNull();
|
|
163
|
+
scope.dispose();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("reactive children mounted through the portal keep updating after attach", () => {
|
|
167
|
+
const target = document.createElement("div");
|
|
168
|
+
document.body.appendChild(target);
|
|
169
|
+
const s = signal("one");
|
|
170
|
+
const { scope } = mount({ to: target, children: () => s() });
|
|
171
|
+
expect(target.textContent).toContain("one");
|
|
172
|
+
s.set("two");
|
|
173
|
+
expect(target.textContent).toContain("two");
|
|
174
|
+
expect(target.textContent).not.toContain("one");
|
|
175
|
+
scope.dispose();
|
|
176
|
+
});
|
|
153
177
|
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { removeNodes } from "../dom/remove";
|
|
5
|
+
|
|
6
|
+
function siblings(...names: string[]): { parent: HTMLElement; nodes: HTMLElement[] } {
|
|
7
|
+
const parent = document.createElement("div");
|
|
8
|
+
const nodes = names.map((name) => {
|
|
9
|
+
const el = document.createElement(name);
|
|
10
|
+
parent.appendChild(el);
|
|
11
|
+
return el;
|
|
12
|
+
});
|
|
13
|
+
return { parent, nodes };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("removeNodes", () => {
|
|
17
|
+
it("removes the first-to-last span and leaves the surrounding siblings", () => {
|
|
18
|
+
const { parent, nodes } = siblings("a", "b", "c", "d", "e");
|
|
19
|
+
removeNodes(nodes.slice(1, 4));
|
|
20
|
+
expect([...parent.children].map((c) => c.localName)).toEqual(["a", "e"]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("also removes nodes inserted between first and last after the list was captured", () => {
|
|
24
|
+
const { parent, nodes } = siblings("a", "b");
|
|
25
|
+
const late = document.createElement("late");
|
|
26
|
+
parent.insertBefore(late, nodes[1]);
|
|
27
|
+
removeNodes(nodes);
|
|
28
|
+
expect(parent.childNodes).toHaveLength(0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("removes a single node", () => {
|
|
32
|
+
const { parent, nodes } = siblings("a", "b");
|
|
33
|
+
removeNodes([nodes[0]]);
|
|
34
|
+
expect([...parent.children]).toEqual([nodes[1]]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("removes the attached nodes individually when the first one is detached", () => {
|
|
38
|
+
const { parent, nodes } = siblings("a", "b", "c");
|
|
39
|
+
parent.removeChild(nodes[0]);
|
|
40
|
+
expect(() => { removeNodes(nodes); }).not.toThrow();
|
|
41
|
+
expect(parent.childNodes).toHaveLength(0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("removes each node from its own parent when first and last have different parents", () => {
|
|
45
|
+
const one = siblings("a", "b");
|
|
46
|
+
const two = siblings("c", "d");
|
|
47
|
+
removeNodes([one.nodes[0], one.nodes[1], two.nodes[0]]);
|
|
48
|
+
expect(one.parent.childNodes).toHaveLength(0);
|
|
49
|
+
expect([...two.parent.children]).toEqual([two.nodes[1]]);
|
|
50
|
+
});
|
|
51
|
+
});
|
package/src/component.ts
CHANGED
|
@@ -11,6 +11,28 @@ import { runInScope } from "./context";
|
|
|
11
11
|
|
|
12
12
|
import type { Scope } from "./scope";
|
|
13
13
|
|
|
14
|
+
let pendingMounts: Array<() => void> = [];
|
|
15
|
+
|
|
16
|
+
// One shared microtask drains every component mounted in the same synchronous pass, in mount
|
|
17
|
+
// order (children before parents). A throwing onMount is rethrown on its own microtask so it
|
|
18
|
+
// can't stop the others, as it couldn't when each component had a microtask of its own.
|
|
19
|
+
function scheduleMount(run: () => void): void {
|
|
20
|
+
if (pendingMounts.length === 0) {
|
|
21
|
+
queueMicrotask(() => {
|
|
22
|
+
const queue = pendingMounts;
|
|
23
|
+
pendingMounts = [];
|
|
24
|
+
for (const job of queue) {
|
|
25
|
+
try {
|
|
26
|
+
job();
|
|
27
|
+
} catch (e) {
|
|
28
|
+
queueMicrotask(() => { throw e; });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
pendingMounts.push(run);
|
|
34
|
+
}
|
|
35
|
+
|
|
14
36
|
export function mountComponent(
|
|
15
37
|
ctor: ComponentConstructor,
|
|
16
38
|
props: Record<string, unknown>,
|
|
@@ -56,7 +78,7 @@ export function mountComponent(
|
|
|
56
78
|
mountChildren(container, dom, scope);
|
|
57
79
|
container.appendChild(end);
|
|
58
80
|
|
|
59
|
-
|
|
81
|
+
scheduleMount(() => {
|
|
60
82
|
if (disposed) return;
|
|
61
83
|
setComponentMounted(instance, true);
|
|
62
84
|
instance.onMount?.();
|
package/src/dom/events.ts
CHANGED
|
@@ -1,11 +1,33 @@
|
|
|
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
|
-
handler: EventListener,
|
|
24
|
+
handler: EventListener | EventListenerObject | null | undefined,
|
|
7
25
|
scope: Scope,
|
|
8
26
|
): void {
|
|
9
|
-
|
|
10
|
-
|
|
27
|
+
// Components forward optional handlers (`onSubmit={this.onSubmit}`) as-is, and addEventListener
|
|
28
|
+
// has always treated a missing listener as a no-op.
|
|
29
|
+
if (handler == null) return;
|
|
30
|
+
const listener = typeof handler === "function" ? batched(handler) : handler;
|
|
31
|
+
el.addEventListener(eventName, listener);
|
|
32
|
+
scope.add(() => { el.removeEventListener(eventName, listener); });
|
|
11
33
|
}
|
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
|
+
if (typeof value === "function") 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) {
|