Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/universal/resource/src/resource-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import {
type UseFnOptions,
} from "./api.js";

/**
* TODO: handle static
*/
export function ResourceList<Item, T>(
list: Iterable<Item>,
{
Expand Down
2 changes: 1 addition & 1 deletion packages/x/vanilla/index.ts
Original file line number Diff line number Diff line change
@@ -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";
3 changes: 2 additions & 1 deletion packages/x/vanilla/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
},
"devDependencies": {
"@starbeam-dev/build-support": "workspace:*",
"rollup": "^3.20.6"
"rollup": "^3.20.6",
"typescript": "^5.0.4"
}
}
128 changes: 72 additions & 56 deletions packages/x/vanilla/src/dom.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,77 @@
/**
* TODO:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added some todos from our convo the other day

* - 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";

export function Text(
text: Reactive<string>,
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<string>,
description?: string | Description
): ContentNode {
return ContentNode(({ into }) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed this to just Content, because the type overload was breaking my brain, especially since the two ContentType's weren't exactly referring to the same function

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"));
}

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"));
}

Expand All @@ -60,7 +80,7 @@ export function Attr<E extends Element>(
value: Reactive<string | null | boolean>,
description?: string | Description
): AttrNode<E> {
return ContentNode(({ into }) => {
return Content(({ into }) => {
const current = value.read();

if (typeof current === "string") {
Expand All @@ -69,22 +89,18 @@ export function Attr<E extends Element>(
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"));
}

Expand All @@ -100,7 +116,7 @@ export function Element<N extends string>(
},
description?: Description | string
): ContentNode {
return ContentNode(({ into, owner }) => {
return Content(({ into, owner }) => {
const element = into.document.createElement(tag);
const elementCursor = Cursor.appendTo(element);

Expand All @@ -113,13 +129,16 @@ export function Element<N extends string>(

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"));
}

Expand All @@ -129,12 +148,11 @@ function placeholder(document: Document): Text {
return document.createTextNode("");
}

type Rendered = FormulaFn<void>;
type Rendered = FormulaFn<unknown>;

interface OutputConstructor {
create: (options: { owner: object }) => Rendered;
create: (options: { owner: object }) => FormulaFn<unknown>;
}

type ContentNode = (into: Cursor) => OutputConstructor;
type AttrNode<E extends Element = Element> = (into: E) => OutputConstructor;

Expand All @@ -146,21 +164,19 @@ function poll(rendered: Rendered[] | Rendered): void {
}
}

function ContentNode<T extends Cursor | Element>(
create: (options: { into: T; owner: object }) => {
cleanup: () => void;
update: () => void;
},
type ContentConstructor<T extends Cursor | Element> = (options: { into: T, owner: object }) => ResourceBlueprint;

function Content<T extends Cursor | Element>(
create: ContentConstructor<T>,
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));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if this is right


return formula;
},
Expand Down
4 changes: 4 additions & 0 deletions packages/x/vanilla/tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,9 @@
"dependencies": {
"@starbeam/universal": "workspace:^",
"@starbeamx/vanilla": "workspace:^"
},
"devDependencies": {
"typescript": "^5.0.4",
"vitest": "^0.30.1"
}
}
61 changes: 56 additions & 5 deletions packages/x/vanilla/tests/vanilla.spec.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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");

Expand All @@ -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("<!--Hello World-->");

cell.set("Goodbye world");
render.read();

expect(body.innerHTML).toBe("<!--Goodbye world-->");

RUNTIME.finalize(owner);
render.read();

expect(body.innerHTML).toBe("");
});

test("it can render fragments", () => {
const { body, owner } = env();

Expand Down Expand Up @@ -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;

Expand All @@ -80,6 +103,34 @@ describe("Vanilla Renderer", () => {
`<div title="Hello World">Hello World - Goodbye World</div>`
);
});

// 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 {
Expand All @@ -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 {
Expand Down
10 changes: 10 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.