Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resource based vanilla renderer #99

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 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"
}
}
27 changes: 25 additions & 2 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
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.