diff --git a/examples/array-test/src/main.tsx b/examples/array-test/src/main.tsx
index aa3b9a2..748c6d5 100644
--- a/examples/array-test/src/main.tsx
+++ b/examples/array-test/src/main.tsx
@@ -10,7 +10,7 @@ const Component = () => {
{() => }
{() => [, ]}
- [ ,,]
+ {() => [, ]}
);
};
diff --git a/packages/core/src/createElement.spec.ts b/packages/core/src/createElement.spec.ts
index 790b077..70cab90 100644
--- a/packages/core/src/createElement.spec.ts
+++ b/packages/core/src/createElement.spec.ts
@@ -84,14 +84,56 @@ describe("render test", () => {
it("component return array", () => {
const Component = () => {
- return [createElement("div"), createElement("div"), createElement("div")];
+ return [
+ createElement("div", { children: "A" }),
+ createElement("div", { children: "B" }),
+ createElement("div", { children: "C" }),
+ ];
};
- const node = createElement("div", { children: createElement(Component) });
+ const node = createElement("div", {
+ children: [
+ createElement("div", {
+ children: () => createElement(Component),
+ }),
+ createElement("div", {
+ children: () => [createElement(Component)],
+ }),
+ createElement(Component),
+ ],
+ });
expect(node.el).toMatchInlineSnapshot(`
-
-
-
+
+
+ A
+
+
+ B
+
+
+ C
+
+
+
+
+ A
+
+
+ B
+
+
+ C
+
+
+
+ A
+
+
+ B
+
+
+ C
+
`);
});
@@ -392,7 +434,7 @@ describe("reactive test", () => {
vi.useRealTimers();
});
- it("Three Todo List with key", async () => {
+ it.only("Three Todo List with key", async () => {
vi.useFakeTimers();
const items = createState<{ id: number; hidden: boolean }[]>([
@@ -403,14 +445,6 @@ describe("reactive test", () => {
]);
const updateList = () => {
- // const values = new Array(10).fill("").map((v, index) => {
- // return {
- // id: index,
- // hidden: (Math.random() * 10) % 2 > 1,
- // };
- // });
- // console.log("test test", (Math.random() * 10) % 2, values);
- // items.val = values;
items.val = [
{ id: 1, hidden: false },
{ id: 2, hidden: false },
@@ -432,7 +466,7 @@ describe("reactive test", () => {
});
return createElement("p", {
children: () => {
- return `${name} counter ${state.val}`;
+ return `TodoItem1 ${name} counter ${state.val}`;
},
});
};
@@ -455,24 +489,6 @@ describe("reactive test", () => {
});
};
- const TodoItem3 = (props: { name: number }) => {
- const { name } = props;
- const state = createState(0);
- useEffect(() => {
- const handler = setInterval(() => {
- state.val++;
- }, 1000);
- return () => {
- clearInterval(handler);
- };
- });
- return createElement("p", {
- children: () => {
- return `TodoItem3 ${name} counter ${state.val}`;
- },
- });
- };
-
const TodoList = () => {
return createElement("div", {
children: [
@@ -494,12 +510,6 @@ describe("reactive test", () => {
? null
: createElement(TodoItem2, { name: item.id }, item.id);
}),
- () =>
- items.val.map((item) => {
- return item.hidden
- ? null
- : createElement(TodoItem3, { name: item.id }, item.id);
- }),
],
}),
],
@@ -514,13 +524,13 @@ describe("reactive test", () => {
- 1 counter 0
+ TodoItem1 1 counter 0
- 2 counter 0
+ TodoItem1 2 counter 0
- 4 counter 0
+ TodoItem1 4 counter 0
TodoItem2 1 counter 0
@@ -531,15 +541,6 @@ describe("reactive test", () => {
TodoItem2 4 counter 0
-
- TodoItem3 1 counter 0
-
-
- TodoItem3 2 counter 0
-
-
- TodoItem3 4 counter 0
-
`);
@@ -555,16 +556,16 @@ describe("reactive test", () => {
- 1 counter 10
+ TodoItem1 1 counter 10
- 2 counter 10
+ TodoItem1 2 counter 10
- 3 counter 5
+ TodoItem1 3 counter 5
- 4 counter 10
+ TodoItem1 4 counter 10
TodoItem2 1 counter 10
@@ -578,18 +579,6 @@ describe("reactive test", () => {
TodoItem2 4 counter 10
-
- TodoItem3 1 counter 10
-
-
- TodoItem3 2 counter 10
-
-
- TodoItem3 3 counter 5
-
-
- TodoItem3 4 counter 10
-
`);
diff --git a/packages/core/src/createElement.ts b/packages/core/src/createElement.ts
index e3f91d8..dbb07d3 100644
--- a/packages/core/src/createElement.ts
+++ b/packages/core/src/createElement.ts
@@ -8,21 +8,19 @@ import {
TagNameMap,
TagOption,
} from "./type";
-import arrify from "./utils/arrify";
import isPrimitive from "./utils/isPrimitive";
+import numberConcat from "./utils/numberConcat";
type Dispose = () => void;
-type JSXTag =
- | keyof TagNameMap
- | ((props: P) => SaplingElement | SaplingElement[] | PrimitiveChild | null);
+type JSXTag
= keyof TagNameMap | ((props: P) => SaplingNode);
export type JSXElementType
= (props: P) => SaplingNode | SaplingElement;
export type SaplingNode =
| SaplingElement
| PrimitiveChild
- | Iterable
+ | SaplingNode[]
| (() => SaplingNode)
| null;
@@ -89,6 +87,9 @@ export class SaplingElement {
}
if (params?.children != null) {
this.children = params.children;
+ Array.from(params.children).forEach(
+ (child) => child.el != null && this._el?.appendChild(child.el),
+ );
}
}
@@ -101,30 +102,60 @@ export class SaplingElement {
this.parent?.children.delete(this);
};
- public appendChildJSXNode = (childrenNode: SaplingElement[]) => {
- [...childrenNode].reduceRight(
- (p: SaplingElement | null, c): SaplingElement => {
- if (c.el?.parentElement != null) {
+ public append = (child: SaplingElement) => {
+ if (child.el != null) {
+ if (child.el.parentElement != null) {
+ // skip append for optimization
+ return child;
+ }
+ this.el?.appendChild(child.el);
+ this.children.add(child);
+ return child;
+ } else {
+ return Array.from(child.children).reduce(
+ (p: SaplingElement | null, c): SaplingElement | null => {
+ if (c.el == null) {
+ return this.append(c) ?? p;
+ }
+ if (c.el != null) {
+ if (c.el?.parentElement != null) {
+ return c;
+ }
+ if (p == null) {
+ // start from here
+ this.el?.appendChild(c.el);
+ }
+ if (p?.el != null) {
+ // continue insert
+ if (p.el.nextSibling != null) {
+ this.el?.insertBefore(c.el, p.el.nextSibling);
+ } else {
+ this.el?.appendChild(c.el);
+ }
+ }
+ this.children.add(c);
+ c.parent = this;
+ }
+
return c;
- }
- if (p == null && c.el != null) {
- this.el?.appendChild(c.el);
- }
- if (p?.el != null && c.el != null) {
- this.el?.insertBefore(c.el, p.el);
- }
- this.children.add(c);
- c.parent = this;
- return c;
- },
- null,
+ },
+ null,
+ );
+ }
+ };
+
+ public hasChild = (childElement: SaplingElement): boolean => {
+ return (
+ this.children.has(childElement) ||
+ Array.from(this.children).reduce((p, c) => {
+ return p || c.hasChild(childElement);
+ }, false)
);
- return this;
};
- public removeExtraNodes = (childrenNode: Set) => {
+ public disposeElementNotIn = (childElement: SaplingElement) => {
this.children.forEach((child) => {
- if (!childrenNode.has(child)) {
+ if (!childElement.hasChild(child)) {
child.dispose();
}
});
@@ -136,19 +167,6 @@ export class SaplingElement {
};
}
-function prepareChildrenNodes(
- children: SaplingNode | SaplingNode[],
-): SaplingElement[] {
- return arrify(children)
- .flatMap((child) => {
- if (isPrimitive(child)) {
- return primitiveToJSXNode(child);
- }
- return child;
- })
- .filter((v): v is SaplingElement => v != null);
-}
-
const primitiveToJSXNode = (primitive: Primitive) =>
new SaplingElement({
node: new Text(primitive.toString()),
@@ -158,6 +176,49 @@ const primitiveToJSXNode = (primitive: Primitive) =>
const JSXFactory = () => {
const jsxScope = new JSXScope();
+ // parse SaplingNode to SaplingElement
+ const prepareSaplingElement = (
+ saplingNode: SaplingNode,
+ nodeCaches?: Map[],
+ cacheKey: number = 0,
+ ): SaplingElement => {
+ if (saplingNode instanceof SaplingElement) {
+ return saplingNode;
+ }
+ if (Array.isArray(saplingNode)) {
+ return new SaplingElement({
+ children: new Set(
+ saplingNode.map((child, index) =>
+ prepareSaplingElement(
+ child,
+ nodeCaches,
+ numberConcat(index, cacheKey),
+ ),
+ ),
+ ),
+ });
+ }
+ if (isPrimitive(saplingNode)) {
+ return primitiveToJSXNode(saplingNode);
+ }
+ if (typeof saplingNode === "function") {
+ let resume;
+
+ if (nodeCaches != null) {
+ const nodeCache = (nodeCaches[cacheKey] ||= new Map<
+ Key,
+ SaplingElement
+ >());
+ resume = jsxScope.collectNodeCache(nodeCache);
+ }
+
+ const element = prepareSaplingElement(saplingNode(), nodeCaches);
+ resume?.();
+ return element;
+ }
+ return new SaplingElement();
+ };
+
const upsert = (element: Node, child: SaplingElement) => {
child.el && element.appendChild(child.el);
return () => {
@@ -179,9 +240,7 @@ const JSXFactory = () => {
): SaplingElement;
function createElement(
- jsxTag: (
- props: P,
- ) => SaplingElement | SaplingElement[] | PrimitiveChild | null,
+ jsxTag: (props: P) => SaplingNode,
options?: P,
key?: Key,
_isStaticChildren?: boolean,
@@ -216,27 +275,12 @@ const JSXFactory = () => {
const resume = jsxScope.collectDispose(disposeStack);
const node = jsxTag(options as P);
resume();
-
- let jsxNode: SaplingElement;
- if (Array.isArray(node)) {
- jsxNode = new SaplingElement({
- node: document.createDocumentFragment(),
- });
- jsxNode.appendChildJSXNode(node);
- } else if (isPrimitive(node) || node == null) {
- jsxNode = new SaplingElement({
- node: node == null ? null : new Text(node.toString()),
- disposeStack,
- children: null,
- });
- } else {
- jsxNode = node;
- }
- jsxNode.mergeDisposeStack(disposeStack);
+ const element = prepareSaplingElement(node);
+ element.mergeDisposeStack(disposeStack);
if (key != null) {
- jsxScope.setCache(key, jsxNode);
+ jsxScope.setCache(key, element);
}
- return jsxNode;
+ return element;
}
const { children, ref, ...props } = (options ?? {}) as TagOption<
@@ -248,38 +292,24 @@ const JSXFactory = () => {
ref.val = el;
}
- const jsxNode = new SaplingElement({
+ const currentElement = new SaplingElement({
node: el,
});
if (children != null) {
- let nodeCaches: (Map | undefined)[] = [];
+ let nodeCaches: Map[] = [];
effect(() => {
- const childrenNode = arrify(children).map((child, index) => {
- const nodeCache = (nodeCaches[index] ||= new Map<
- Key,
- SaplingElement
- >());
- const resume = jsxScope.collectNodeCache(nodeCache);
- const children = prepareChildrenNodes(
- typeof child === "function" ? child() : child,
- );
- resume();
- return children;
- });
-
- jsxNode.removeExtraNodes(
- new Set(childrenNode.flatMap((child) => child)),
- );
- jsxNode.appendChildJSXNode(childrenNode.flatMap((child) => child));
+ const element = prepareSaplingElement(children, nodeCaches);
+ currentElement.disposeElementNotIn(element);
+ currentElement.append(element);
});
}
if (key != null) {
- jsxScope.setCache(key, jsxNode);
+ jsxScope.setCache(key, currentElement);
}
- return jsxNode;
+ return currentElement;
}
const useEffect = (callback: () => Dispose | void) => {
diff --git a/packages/core/src/utils/numberConcat.ts b/packages/core/src/utils/numberConcat.ts
new file mode 100644
index 0000000..3eae5b9
--- /dev/null
+++ b/packages/core/src/utils/numberConcat.ts
@@ -0,0 +1,4 @@
+const numberConcat = (a: number, b: number) =>
+ (a << (Math.ceil(Math.log2(b)) + 1)) + b;
+
+export default numberConcat;