Add key field to UElement (ADR-004)
This commit is contained in:
@@ -3,7 +3,8 @@ import type { UNode, UElement, URoot, UType, UComponent, UniversalProps } from "
|
|||||||
let _idCounter = 0;
|
let _idCounter = 0;
|
||||||
|
|
||||||
export function h(type: UType, props?: UniversalProps | null, ...children: UNode[]): UElement | URoot {
|
export function h(type: UType, props?: UniversalProps | null, ...children: UNode[]): UElement | URoot {
|
||||||
const resolvedProps: UniversalProps = props ? { ...props } : {};
|
const { key, ...restProps } = props ?? {};
|
||||||
|
const resolvedProps: UniversalProps = restProps;
|
||||||
const flatChildren = children.flat(Infinity as 1).filter((c: UNode) => c != null && c !== false) as UNode[];
|
const flatChildren = children.flat(Infinity as 1).filter((c: UNode) => c != null && c !== false) as UNode[];
|
||||||
|
|
||||||
if (type === "root") {
|
if (type === "root") {
|
||||||
@@ -18,6 +19,7 @@ export function h(type: UType, props?: UniversalProps | null, ...children: UNode
|
|||||||
type: type as string,
|
type: type as string,
|
||||||
props: resolvedProps,
|
props: resolvedProps,
|
||||||
children: flatChildren,
|
children: flatChildren,
|
||||||
|
...(key != null ? { key: key as string } : {}),
|
||||||
} as UElement;
|
} as UElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export const UJSX = Type.Module({
|
|||||||
type: Type.String(),
|
type: Type.String(),
|
||||||
props: Type.Ref("UniversalProps"),
|
props: Type.Ref("UniversalProps"),
|
||||||
children: Type.Array(Type.Ref("UNode")),
|
children: Type.Array(Type.Ref("UNode")),
|
||||||
|
key: Type.Optional(Type.String()),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
URoot: Type.Object({
|
URoot: Type.Object({
|
||||||
@@ -45,6 +46,7 @@ export type UElement = {
|
|||||||
type: string;
|
type: string;
|
||||||
props: UniversalProps;
|
props: UniversalProps;
|
||||||
children: UNode[];
|
children: UNode[];
|
||||||
|
key?: string;
|
||||||
};
|
};
|
||||||
export type URoot = {
|
export type URoot = {
|
||||||
type: "root";
|
type: "root";
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { h, createRoot, createComponent, Fragment, jsx, jsxs, jsxDEV } from "../src/core/h.js";
|
import { h, createRoot, createComponent, Fragment, jsx, jsxs, jsxDEV } from "../src/core/h.js";
|
||||||
import { isUElement, isURoot, isUPrimitive } from "../src/core/schema.js";
|
import { isUElement, isURoot, isUPrimitive, UJSX } from "../src/core/schema.js";
|
||||||
import type { UNode, UElement } from "../src/core/schema.js";
|
import type { UNode, UElement } from "../src/core/schema.js";
|
||||||
|
import { Value } from "@alkdev/typebox/value";
|
||||||
import { Context } from "../src/core/context.js";
|
import { Context } from "../src/core/context.js";
|
||||||
import { TransformRegistry, childCtx, ctx as transformCtx } from "../src/transform/registry.js";
|
import { TransformRegistry, childCtx, ctx as transformCtx } from "../src/transform/registry.js";
|
||||||
import type { Direction } from "../src/core/context.js";
|
import type { Direction } from "../src/core/context.js";
|
||||||
@@ -98,6 +99,60 @@ describe("type guards", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("UElement key field", () => {
|
||||||
|
it("h() extracts key from props and promotes to element level", () => {
|
||||||
|
const el = h("div", { key: "item-1", class: "test" }, "hello");
|
||||||
|
expect(isUElement(el)).toBe(true);
|
||||||
|
if (isUElement(el)) {
|
||||||
|
expect(el.key).toBe("item-1");
|
||||||
|
expect(el.props.key).toBeUndefined();
|
||||||
|
expect(el.props.class).toBe("test");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("h() without key does not add key field", () => {
|
||||||
|
const el = h("div", { class: "test" }, "hello");
|
||||||
|
expect(isUElement(el)).toBe(true);
|
||||||
|
if (isUElement(el)) {
|
||||||
|
expect(el.key).toBeUndefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("h() with null props has no key", () => {
|
||||||
|
const el = h("div", null, "hello");
|
||||||
|
expect(isUElement(el)).toBe(true);
|
||||||
|
if (isUElement(el)) {
|
||||||
|
expect(el.key).toBeUndefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("UElement with key validates against TypeBox schema", () => {
|
||||||
|
const UElementSchema = UJSX.Import("UElement");
|
||||||
|
const el = h("li", { key: "a" }, "item");
|
||||||
|
expect(Value.Check(UElementSchema, el)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("UElement without key validates against TypeBox schema (backward compat)", () => {
|
||||||
|
const UElementSchema = UJSX.Import("UElement");
|
||||||
|
const el = h("li", { class: "item" }, "item");
|
||||||
|
expect(Value.Check(UElementSchema, el)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("URoot does not have a key field", () => {
|
||||||
|
const root = createRoot("r");
|
||||||
|
expect("key" in root).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("h() with root type does not promote key to URoot", () => {
|
||||||
|
const root = h("root", { key: "should-not-appear", id: "test" }, "child");
|
||||||
|
expect(isURoot(root)).toBe(true);
|
||||||
|
if (isURoot(root)) {
|
||||||
|
expect("key" in root).toBe(false);
|
||||||
|
expect(root.props.key).toBeUndefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("Context", () => {
|
describe("Context", () => {
|
||||||
it("stores and updates values", () => {
|
it("stores and updates values", () => {
|
||||||
const ctx = new Context({ density: "full", target: "markdown" });
|
const ctx = new Context({ density: "full", target: "markdown" });
|
||||||
|
|||||||
Reference in New Issue
Block a user