diff --git a/packages/universal/resource/src/resource-list.ts b/packages/universal/resource/src/resource-list.ts index 5edd770b..92a29570 100644 --- a/packages/universal/resource/src/resource-list.ts +++ b/packages/universal/resource/src/resource-list.ts @@ -9,6 +9,9 @@ import { type UseFnOptions, } from "./api.js"; +/** + * TODO: handle static + */ export function ResourceList( list: Iterable, { diff --git a/packages/x/vanilla/index.ts b/packages/x/vanilla/index.ts index 198c6961..2d1901e2 100644 --- a/packages/x/vanilla/index.ts +++ b/packages/x/vanilla/index.ts @@ -1,2 +1,2 @@ export { Cursor } from "./src/cursor.js"; -export { Attr, Element as El, Fragment, Text } from "./src/dom.js"; +export { Attr, Comment,Element as El, Fragment, Text } from "./src/dom.js"; diff --git a/packages/x/vanilla/package.json b/packages/x/vanilla/package.json index 87e2498f..e9aa043c 100644 --- a/packages/x/vanilla/package.json +++ b/packages/x/vanilla/package.json @@ -34,6 +34,7 @@ }, "devDependencies": { "@starbeam-dev/build-support": "workspace:*", - "rollup": "^3.20.6" + "rollup": "^3.20.6", + "typescript": "^5.0.4" } } diff --git a/packages/x/vanilla/src/dom.ts b/packages/x/vanilla/src/dom.ts index c7fdd26e..ca19572f 100644 --- a/packages/x/vanilla/src/dom.ts +++ b/packages/x/vanilla/src/dom.ts @@ -1,6 +1,22 @@ +/** + * TODO: + * - DynamicFragment + * - Namespaces + * - SVG + * - Modifier + * - Portal + * - SSR + * + * Goals: + * - Implement Glimmer compatibility + * - Write compiler Glimmer -> whatever this DSL ends up being + * + * Stretch Goals: + * - other compilers (html``) + */ import type { Description, Reactive } from "@starbeam/interfaces"; import { CachedFormula, DEBUG, type FormulaFn } from "@starbeam/reactive"; -import { RUNTIME } from "@starbeam/runtime"; +import { Resource,type ResourceBlueprint,RUNTIME,use } from "@starbeam/universal"; import { Cursor } from "./cursor.js"; @@ -8,27 +24,30 @@ export function Text( text: Reactive, description?: string | Description ): ContentNode { - return ContentNode(({ into }) => { - const node = into.insert(into.document.createTextNode(text.read())); + return Content( + ({ into, owner }) => { + const node = into.insert(into.document.createTextNode(text.read())); + + return Resource(({ on }) => { + on.cleanup(() => void node.remove()); + node.textContent = text.read(); + }) + } - return { - cleanup: () => void node.remove(), - update: () => (node.textContent = text.read()), - }; - }, DEBUG?.Desc("resource", description, "Text")); + , DEBUG?.Desc('resource', description, 'Text')); } export function Comment( text: Reactive, description?: string | Description ): ContentNode { - return ContentNode(({ into }) => { + return Content(({ into }) => { const node = into.insert(into.document.createComment(text.read())); - return { - cleanup: () => void node.remove(), - update: () => (node.textContent = text.read()), - }; + return Resource(({ on }) => { + on.cleanup(() => void node.remove()); + node.textContent = text.read(); + }); }, DEBUG?.Desc("resource", description, "Comment")); } @@ -36,22 +55,23 @@ export function Fragment( nodes: ContentNode[], description?: string | Description ): ContentNode { - return ContentNode(({ into, owner }) => { + return Content(({ into, owner }) => { const start = placeholder(into.document); into.insert(start); - const renderedNodes = nodes.map((nodeConstructor) => - nodeConstructor(into).create({ owner }) + const renderedNodes = + nodes.map( + (nodeConstructor) => nodeConstructor(into).create({ owner }) ); const end = placeholder(into.document); into.insert(end); const range = FragmentRange.create(start, end); - return { - cleanup: () => void range.clear(), - update: () => void poll(renderedNodes), - }; + return Resource(({ on }) => { + on.cleanup(() => void range.clear()); + poll(renderedNodes); + }) }, DEBUG?.Desc("resource", description, "Fragment")); } @@ -60,7 +80,7 @@ export function Attr( value: Reactive, description?: string | Description ): AttrNode { - return ContentNode(({ into }) => { + return Content(({ into }) => { const current = value.read(); if (typeof current === "string") { @@ -69,22 +89,18 @@ export function Attr( into.setAttribute(name, ""); } - return { - cleanup: () => { + return Resource(({ on }) => { + on.cleanup(() => void into.removeAttribute(name)); + const next = value.read(); + + if (typeof next === "string") { + into.setAttribute(name, next); + } else if (next === true) { + into.setAttribute(name, ""); + } else if (next === false) { into.removeAttribute(name); - }, - update: () => { - const next = value.read(); - - if (typeof next === "string") { - into.setAttribute(name, next); - } else if (next === true) { - into.setAttribute(name, ""); - } else if (next === false) { - into.removeAttribute(name); - } - }, - }; + } + }); }, DEBUG?.Desc("resource", description, "Attr")); } @@ -100,7 +116,7 @@ export function Element( }, description?: Description | string ): ContentNode { - return ContentNode(({ into, owner }) => { + return Content(({ into, owner }) => { const element = into.document.createElement(tag); const elementCursor = Cursor.appendTo(element); @@ -113,13 +129,16 @@ export function Element( into.insert(element); - return { - cleanup: () => void element.remove(), - update: () => { - poll(renderAttributes); - poll(renderBody); - }, - }; + return Resource(({on}, meta) => { + on.cleanup(() => void element.remove()); + + return { + update: () => { + renderAttributes.forEach(a => a.read()); + poll(renderBody); + }, + } + }); }, DEBUG?.Desc("resource", description, "Element")); } @@ -129,12 +148,11 @@ function placeholder(document: Document): Text { return document.createTextNode(""); } -type Rendered = FormulaFn; +type Rendered = FormulaFn; interface OutputConstructor { - create: (options: { owner: object }) => Rendered; + create: (options: { owner: object }) => FormulaFn; } - type ContentNode = (into: Cursor) => OutputConstructor; type AttrNode = (into: E) => OutputConstructor; @@ -146,21 +164,19 @@ function poll(rendered: Rendered[] | Rendered): void { } } -function ContentNode( - create: (options: { into: T; owner: object }) => { - cleanup: () => void; - update: () => void; - }, +type ContentConstructor = (options: { into: T, owner: object }) => ResourceBlueprint; + +function Content( + create: ContentConstructor, description: Description | undefined ): (into: T) => OutputConstructor { return (into: T) => { return { create({ owner }) { - const { cleanup, update } = create({ into, owner }); - - const formula = CachedFormula(update, description); + const blueprint = create({ into, owner }); + const formula = CachedFormula(() => (use(blueprint, { lifetime: owner, metadata: { owner } })).current, description); - RUNTIME.onFinalize(owner, cleanup); + RUNTIME.onFinalize(owner, () => void RUNTIME.finalize(formula)); return formula; }, diff --git a/packages/x/vanilla/tests/package.json b/packages/x/vanilla/tests/package.json index 13a2aa64..42887391 100644 --- a/packages/x/vanilla/tests/package.json +++ b/packages/x/vanilla/tests/package.json @@ -11,5 +11,9 @@ "dependencies": { "@starbeam/universal": "workspace:^", "@starbeamx/vanilla": "workspace:^" + }, + "devDependencies": { + "typescript": "^5.0.4", + "vitest": "^0.30.1" } } diff --git a/packages/x/vanilla/tests/vanilla.spec.ts b/packages/x/vanilla/tests/vanilla.spec.ts index 70d53753..cd7cf61a 100644 --- a/packages/x/vanilla/tests/vanilla.spec.ts +++ b/packages/x/vanilla/tests/vanilla.spec.ts @@ -1,7 +1,7 @@ // @vitest-environment happy-dom import { Cell, RUNTIME } from "@starbeam/universal"; -import { Cursor, El, Fragment, Text } from "@starbeamx/vanilla"; +import { Comment, Cursor, El, Fragment, Text } from "@starbeamx/vanilla"; import { describe, expect, test } from "vitest"; import { env } from "./env"; @@ -11,7 +11,9 @@ describe("Vanilla Renderer", () => { const { body, owner } = env(); const cell = Cell("Hello World"); - const render = Text(cell)(body.cursor).create({ owner }); + const text = Text(cell); + const renderer = text(body.cursor); + const render = renderer.create({ owner }); expect(body.innerHTML).toBe("Hello World"); @@ -26,6 +28,27 @@ describe("Vanilla Renderer", () => { expect(body.innerHTML).toBe(""); }); + test("it can render a comment", () => { + const { body, owner } = env(); + + const cell = Cell("Hello World"); + const text = Comment(cell); + const renderer = text(body.cursor); + const render = renderer.create({ owner }); + + expect(body.innerHTML).toBe(""); + + cell.set("Goodbye world"); + render.read(); + + expect(body.innerHTML).toBe(""); + + RUNTIME.finalize(owner); + render.read(); + + expect(body.innerHTML).toBe(""); + }); + test("it can render fragments", () => { const { body, owner } = env(); @@ -59,7 +82,7 @@ describe("Vanilla Renderer", () => { expect(body.innerHTML).toBe(""); }); - test("it can render elements", () => { + test('it can render elements', () => { const { body, owner } = env(); const cursor = body.cursor; @@ -80,6 +103,34 @@ describe("Vanilla Renderer", () => { `
Hello World - Goodbye World
` ); }); + + // This is currently *very* slow + test("it can render many elements", () => { + const { body, owner } = env(); + const cursor = body.cursor; + const fragments = []; + + for (let i = 0; i < 1000; i++) { + const a = Cell("Hello World"); + const b = Cell(" - "); + const c = Cell("Goodbye World"); + + const title = El.Attr("title", a); + + const el = El({ + tag: "div", + attributes: [title], + body: [Text(a), Text(b), Text(c)], + }); + fragments.push(el); + } + + console.time('render'); + Fragment(fragments)(cursor).create({ owner }); + console.timeEnd('render'); + + expect(body.snapshot().length).toBe(fragments.length); + }); }); export class Body { @@ -99,8 +150,8 @@ export class Body { return this.#body.innerHTML; } - snapshot(): void { - this.#snapshot = [...this.#body.childNodes]; + snapshot() : ChildNode[] { + return this.#snapshot = [...this.#body.childNodes]; } expectStable(): void { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d9bf8f86..5c136199 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1533,6 +1533,9 @@ importers: rollup: specifier: ^3.20.6 version: 3.20.6 + typescript: + specifier: ^5.0.4 + version: 5.0.4 packages/x/vanilla/tests: dependencies: @@ -1542,6 +1545,13 @@ importers: '@starbeamx/vanilla': specifier: workspace:^ version: link:.. + devDependencies: + typescript: + specifier: ^5.0.4 + version: 5.0.4 + vitest: + specifier: ^0.30.1 + version: 0.30.1(@vitest/ui@0.30.1)(happy-dom@9.8.1)(jsdom@21.1.1) workspace/@domtree/any: dependencies: