From 80b4831e4075b9f2aa0287e446243d83e9d00db8 Mon Sep 17 00:00:00 2001 From: "glm-5.1" Date: Thu, 21 May 2026 21:04:18 +0000 Subject: [PATCH] Implement Map ujsx component for dynamic array mapping --- src/component/index.ts | 2 + src/component/map.ts | 25 ++++++++++- test/component/map.test.ts | 87 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 test/component/map.test.ts diff --git a/src/component/index.ts b/src/component/index.ts index 71b81bf..ac2efa5 100644 --- a/src/component/index.ts +++ b/src/component/index.ts @@ -2,5 +2,7 @@ export { Operation } from "./operation.js"; export type { OperationProps } from "./operation.js"; export { Parallel } from "./parallel.js"; export type { ParallelProps } from "./parallel.js"; +export { Map } from "./map.js"; +export type { MapOver, MapProps } from "./map.js"; export { Sequential } from "./sequential.js"; export type { SequentialProps } from "./sequential.js"; \ No newline at end of file diff --git a/src/component/map.ts b/src/component/map.ts index 8cec2e9..32526f8 100644 --- a/src/component/map.ts +++ b/src/component/map.ts @@ -1 +1,24 @@ -export {}; \ No newline at end of file +import type { UComponent, UElement, UNode, PropValue, UniversalProps } from "@alkdev/ujsx"; +import type { Signal } from "@preact/signals-core"; +import type { CallResult } from "../schema/edge.js"; + +export type MapOver = Signal | unknown[] | ((results: Record) => unknown[]); + +export interface MapProps { + over: MapOver; + as: string; +} + +export const Map: UComponent = ( + props: (MapProps & UniversalProps) & { children?: UNode[] }, +): UElement => { + const { over, as, children, ...rest } = props; + const elementProps: UniversalProps = { ...rest, over: over as PropValue, as }; + return { + type: "map", + props: elementProps, + children: children ?? [], + }; +}; + +Map.displayName = "Map"; \ No newline at end of file diff --git a/test/component/map.test.ts b/test/component/map.test.ts new file mode 100644 index 0000000..fbcdc8d --- /dev/null +++ b/test/component/map.test.ts @@ -0,0 +1,87 @@ +import { describe, it, expect } from "vitest"; +import { Map } from "../../src/component/map.js"; +import type { MapOver, MapProps } from "../../src/component/map.js"; +import type { UElement, UNode } from "@alkdev/ujsx"; +import type { CallResult } from "../../src/schema/edge.js"; +import { signal } from "@preact/signals-core"; + +describe("Map", () => { + it("produces UElement with type map", () => { + const el = Map({ + over: ["a", "b"], + as: "item", + } as MapProps & { children?: UNode[] }); + expect(el.type).toBe("map"); + }); + + it("preserves static array as over prop", () => { + const items = [1, 2, 3]; + const el = Map({ + over: items, + as: "item", + } as MapProps & { children?: UNode[] }); + expect((el as UElement).props.over).toBe(items); + }); + + it("preserves function as over prop", () => { + const fn: MapOver = (results: Record) => + results["fetch-items"]!.output as unknown[]; + const el = Map({ + over: fn, + as: "item", + } as MapProps & { children?: UNode[] }); + expect((el as UElement).props.over).toBe(fn); + }); + + it("preserves signal as over prop", () => { + const sig = signal(["x", "y"]); + const el = Map({ + over: sig as unknown as MapOver, + as: "item", + } as MapProps & { children?: UNode[] }); + expect((el as UElement).props.over).toBe(sig); + }); + + it("preserves as prop", () => { + const el = Map({ + over: [1, 2], + as: "element", + } as MapProps & { children?: UNode[] }); + expect((el as UElement).props.as).toBe("element"); + }); + + it("includes children", () => { + const child: UElement = { type: "operation", props: { name: "process" }, children: [] }; + const el = Map({ + over: [1, 2], + as: "item", + children: [child], + } as MapProps & { children?: UNode[] }); + expect(el.children).toEqual([child]); + }); + + it("defaults children to empty array", () => { + const el = Map({ + over: [], + as: "item", + } as MapProps & { children?: UNode[] }); + expect(el.children).toEqual([]); + }); + + it("is a valid UElement (has type, props, children)", () => { + const el = Map({ + over: [], + as: "item", + } as MapProps & { children?: UNode[] }); + expect(el).toHaveProperty("type"); + expect(el).toHaveProperty("props"); + expect(el).toHaveProperty("children"); + expect(typeof el.type).toBe("string"); + expect(typeof el.props).toBe("object"); + expect(Array.isArray(el.children)).toBe(true); + }); + + it("displayName is Map", () => { + expect(Map.displayName).toBe("Map"); + }); +}); \ No newline at end of file