Skip to content

Commit

Permalink
Avoid using signals in Template.build()
Browse files Browse the repository at this point in the history
  • Loading branch information
yishn committed May 18, 2024
1 parent acc86a5 commit 2e99fe2
Show file tree
Hide file tree
Showing 14 changed files with 134 additions and 108 deletions.
16 changes: 11 additions & 5 deletions src/component.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Component,
If,
Template,
TemplateNodes,
defineComponents,
event,
prop,
Expand Down Expand Up @@ -34,7 +35,9 @@ test("Component with reactive prop", () => {
const [name, setName] = useSignal("World");
const ref = useRef<Greeting>();

document.body.append(...(<Greeting ref={ref} name={name} />).build()());
TemplateNodes.forEach((<Greeting ref={ref} name={name} />).build(), (node) =>
document.body.append(node),
);

const renderRoot = ref()!.shadowRoot!;
const h1 = renderRoot.querySelector("h1")!;
Expand Down Expand Up @@ -71,7 +74,9 @@ test("Component with attributes", () => {
defineComponents(Greeting);

const ref = useRef<Greeting>();
document.body.append(...(<Greeting ref={ref} />).build()());
TemplateNodes.forEach((<Greeting ref={ref} />).build(), (node) =>
document.body.append(node),
);

const renderRoot = ref()!.shadowRoot!;
const h1 = renderRoot.querySelector("h1")!;
Expand Down Expand Up @@ -114,8 +119,8 @@ test("Component with events", () => {
const ref = useRef<Button>();
let clicked: boolean = false;

document.body.append(
...(
TemplateNodes.forEach(
(
<Button
ref={ref}
text="Click me"
Expand All @@ -124,7 +129,8 @@ test("Component with events", () => {
clicked = true;
}}
/>
).build()(),
).build(),
(node) => document.body.append(node),
);

ref()?.click();
Expand Down
6 changes: 4 additions & 2 deletions src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from "./utils.js";
import { useScope } from "./scope.js";
import { Context, isContext, provideContext } from "./context.js";
import { Template } from "./template.js";
import { Template, TemplateNodes } from "./template.js";

interface Tagged<in out T> {
_tag: T;
Expand Down Expand Up @@ -543,7 +543,9 @@ export const Component: ((tagName: string) => ComponentConstructor<{}>) &
mountEffects = [];

try {
renderParent?.append(...this.render().build()());
TemplateNodes.forEach(this.render().build(), (node) => {
renderParent.append(node);
});

// Run mount effects

Expand Down
2 changes: 1 addition & 1 deletion src/intrinsic/ClassComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ export const ClassComponent = <T extends HTMLElement>(

hydrateElement(node, false, props);

return () => [node];
return [node];
});
23 changes: 13 additions & 10 deletions src/intrinsic/For.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { GlobalRegistrator } from "@happy-dom/global-registrator";
import { afterEach, beforeEach, test } from "node:test";
import assert from "node:assert";
import { For, useSignal, useRef, If, ElseIf } from "../mod.js";
import { For, useSignal, useRef, If, ElseIf, TemplateNodes } from "../mod.js";
import { useScope } from "../scope.js";

beforeEach(() => {
Expand All @@ -17,14 +17,15 @@ test("For", async () => {
const [list, setList] = useSignal<string[]>([]);
const ulRef = useRef<HTMLUListElement>();

document.body.append(
...(
TemplateNodes.forEach(
(
<ul ref={ulRef}>
<For each={list} key={(item) => item}>
{(item) => <li>{item}</li>}
</For>
</ul>
).build()(),
).build(),
(node) => document.body.append(node),
);

const effectsCount = s._effects.length;
Expand Down Expand Up @@ -66,16 +67,17 @@ test("For in If", async () => {
const [list, setList] = useSignal<string[]>(["a"]);
const ulRef = useRef<HTMLUListElement>();

document.body.append(
...(
TemplateNodes.forEach(
(
<If condition={condition}>
<ul ref={ulRef}>
<For each={list} key={(item) => item}>
{(item) => <li>{item}</li>}
</For>
</ul>
</If>
).build()(),
).build(),
(node) => document.body.append(node),
);

const effectsCount = s._effects.length;
Expand Down Expand Up @@ -105,8 +107,8 @@ test("Fragment and If in For", async () => {
const [list, setList] = useSignal<string[]>(["a", "b", "c"]);
const ulRef = useRef<HTMLUListElement>();

document.body.append(
...(
TemplateNodes.forEach(
(
<ul ref={ulRef}>
<For each={list}>
{(item) => (
Expand All @@ -121,7 +123,8 @@ test("Fragment and If in For", async () => {
)}
</For>
</ul>
).build()(),
).build(),
(node) => document.body.append(node),
);

const effectsCount = s._effects.length;
Expand Down
56 changes: 17 additions & 39 deletions src/intrinsic/For.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import {
useSubscope,
} from "../scope.js";
import { useRenderer } from "../renderer.js";
import { createTemplate, Template } from "../template.js";
import { createTemplate, Template, TemplateNodes } from "../template.js";

interface KeyMeta {
_subnodes?: SignalLike<Node[]>;
_subnodes: TemplateNodes;
_destroy: () => void;
}

Expand All @@ -33,40 +33,27 @@ export const For = <T>(props: {
const items = MaybeSignal.upgrade(props.each ?? []);
const anchor = renderer._node(() => document.createComment(""));
const keyFn = props.key ?? ((_, i) => i);
const [nodes, setNodes] = useSignal<(SignalLike<Node[]> | undefined)[]>(
[],
{ force: true },
);
const nodes: [Comment, TemplateNodes[]] = [anchor, []];
const keyMap = new Map<unknown, KeyMeta>();
const mutationResult = useArrayMutation(items, keyFn);

const lookForAnchor = (index: number): Node => {
for (let i = index - 1; i >= 0; i--) {
const subnodes = nodes()[i];

if (subnodes && subnodes().length > 0) {
return subnodes()[subnodes().length - 1];
}
}

return anchor;
};
const lookForAnchor = (index: number): Node =>
TemplateNodes.last(nodes[1], index - 1) ?? anchor;

useEffect(() => {
for (const mutation of mutationResult()._mutations) {
if (mutation._type == "r") {
const { _subnodes, _destroy } = keyMap.get(mutation._key) ?? {};
_destroy?.();

setNodes((nodes) => {
nodes.splice(mutation._index, 1);
return nodes;
});
nodes[1].splice(mutation._index, 1);

_subnodes?.().forEach((node) => node.parentNode?.removeChild(node));
TemplateNodes.forEach(_subnodes ?? [], (node) =>
node.parentNode?.removeChild(node),
);
keyMap.delete(mutation._key);
} else if (mutation._type == "a") {
let _subnodes: SignalLike<Node[]> | undefined;
let _subnodes!: TemplateNodes;

const [, destroy] = useSubscope(() => {
const [index, setIndex] = useSignal(mutation._index);
Expand All @@ -86,16 +73,12 @@ export const For = <T>(props: {
}
});

_subnodes = props.children?.(item, index, items).build();
_subnodes = props.children?.(item, index, items).build() ?? [];
nodes[1].splice(mutation._index, 0, _subnodes);

let itemAnchor = lookForAnchor(mutation._index);

setNodes((nodes) => {
nodes.splice(mutation._index, 0, _subnodes);
return nodes;
});

_subnodes?.().forEach((node) => {
TemplateNodes.forEach(_subnodes, (node) => {
itemAnchor.parentNode?.insertBefore(node, itemAnchor.nextSibling);
itemAnchor = node;
});
Expand All @@ -105,23 +88,18 @@ export const For = <T>(props: {
} else if (mutation._type == "m") {
const { _subnodes } = keyMap.get(mutation._key) ?? {};

setNodes((nodes) => {
nodes.splice(mutation._from, 1);
nodes.splice(mutation._to, 0, _subnodes);
return nodes;
});
nodes[1].splice(mutation._from, 1);
nodes[1].splice(mutation._to, 0, _subnodes ?? []);

let itemAnchor = lookForAnchor(mutation._to);

_subnodes?.().forEach((node) => {
TemplateNodes.forEach(_subnodes ?? [], (node) => {
itemAnchor.parentNode?.insertBefore(node, itemAnchor.nextSibling);
itemAnchor = node;
});
}
}
}, [mutationResult]);

return useMemo(() =>
[anchor as Node].concat(nodes().flatMap((nodes) => nodes?.() ?? [])),
);
return nodes;
});
4 changes: 1 addition & 3 deletions src/intrinsic/Fragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const Fragment: FunctionalComponent<{
children?: Children;
}> = ({ children }) =>
createTemplate(() => {
const arr = !Array.isArray(children)
return !Array.isArray(children)
? children == null
? []
: [
Expand All @@ -37,6 +37,4 @@ export const Fragment: FunctionalComponent<{
: Text({ text: children }).build(),
]
: children.flatMap((children) => Fragment({ children }).build());

return useMemo(() => arr.flatMap((signal) => signal()));
});
50 changes: 27 additions & 23 deletions src/intrinsic/If.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { GlobalRegistrator } from "@happy-dom/global-registrator";
import { afterEach, beforeEach, test } from "node:test";
import assert from "node:assert";
import { If, useSignal, Else, ElseIf } from "../mod.js";
import { useScope } from "../scope.js";
import { If, useSignal, Else, ElseIf, TemplateNodes } from "../mod.js";
import { useRef, useScope } from "../scope.js";

beforeEach(() => {
GlobalRegistrator.register();
Expand All @@ -17,42 +17,46 @@ test("If", async () => {
const [show, setShow] = useSignal(true);
const [failMessage, setFailMessage] = useSignal("Failure");
const [obj, setObj] = useSignal<{ value: string }>();
const elRef = useRef<HTMLDivElement>();

const el = (
<div>
<If condition={show}>
<h1>Success!</h1>
</If>
<ElseIf condition={() => obj() != null}>
<h1>{() => obj()?.value}</h1>
</ElseIf>
<Else>
<h1>{failMessage}</h1>
</Else>
</div>
).build()()[0];
TemplateNodes.forEach(
(
<div ref={elRef}>
<If condition={show}>
<h1>Success!</h1>
</If>
<ElseIf condition={() => obj() != null}>
<h1>{() => obj()?.value}</h1>
</ElseIf>
<Else>
<h1>{failMessage}</h1>
</Else>
</div>
).build(),
(node) => document.body.append(node),
);

const effectsCount = s._effects.length;
const subscopesCount = s._subscopes.length;

assert.strictEqual(el.textContent, "Success!");
assert.strictEqual(elRef()!.textContent, "Success!");

setShow(false);
assert.strictEqual(el.textContent, "Failure");
const innerElement = el.childNodes[1];
assert.strictEqual(elRef()!.textContent, "Failure");
const innerElement = elRef()!.childNodes[1];

setFailMessage("Unknown Failure");
assert.strictEqual(el.textContent, "Unknown Failure");
assert.strictEqual(el.childNodes[1], innerElement);
assert.strictEqual(elRef()!.textContent, "Unknown Failure");
assert.strictEqual(elRef()!.childNodes[1], innerElement);

setObj({ value: "Object Success!" });
assert.strictEqual(el.textContent, "Object Success!");
assert.strictEqual(elRef()!.textContent, "Object Success!");

setObj(undefined);
assert.strictEqual(el.textContent, "Unknown Failure");
assert.strictEqual(elRef()!.textContent, "Unknown Failure");

setShow(true);
assert.strictEqual(el.textContent, "Success!");
assert.strictEqual(elRef()!.textContent, "Success!");

assert.deepStrictEqual(
[s._effects.length, s._subscopes.length],
Expand Down
24 changes: 15 additions & 9 deletions src/intrinsic/If.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { FunctionalComponent } from "../component.js";
import { createTemplate, type Template } from "../template.js";
import { TemplateNodes, createTemplate, type Template } from "../template.js";
import {
MaybeSignal,
SignalLike,
Expand Down Expand Up @@ -45,22 +45,28 @@ export const ElseIf: FunctionalComponent<{
return runWithRenderer({ _ifConditions: [] }, () =>
createTemplate(() => {
const anchor = renderer._node(() => document.createComment(""));
const [nodes, setNodes] = useSignal<Node[]>([anchor], { force: true });
const nodes: [Comment, TemplateNodes] = [anchor, []];
const template = useMemo(() =>
condition() ? Fragment({ children: props.children }) : null,
);

let subnodes: SignalLike<Node[]> | undefined;
let subnodes: TemplateNodes = [];

useEffect(() => {
subnodes?.().forEach((node) => node.parentNode?.removeChild(node));
setNodes((nodes) => [nodes[0]]);
TemplateNodes.forEach(subnodes, (node) =>
node.parentNode?.removeChild(node),
);
nodes[1] = [];

const [, destroy] = useSubscope(() => {
subnodes = template()?.build();
const subnodesValue = subnodes?.() ?? [];
anchor.after(...subnodesValue);
setNodes((nodes) => [...nodes, ...subnodesValue]);
subnodes = template()?.build() ?? [];
nodes[1] = subnodes;

let before: Node = anchor;
TemplateNodes.forEach(subnodes, (node) => {
before.parentNode?.insertBefore(node, before.nextSibling);
before = node;
});
});

return destroy;
Expand Down
Loading

0 comments on commit 2e99fe2

Please sign in to comment.