VueA custom renderer inside one SFC: Vue renders a scene graph of plain objects
<script setup>
import { createRenderer, defineComponent, h, ref, shallowRef } from "vue";
const log = shallowRef([]);
const record = (msg) => { log.value = [...log.value, msg]; };
const { createApp: createSceneApp } = createRenderer({
createElement: (type) => { record(`createElement ${type}`); return { type, props: {}, children: [], parent: null }; },
createText: (text) => ({ type: "#text", text, parent: null }),
createComment: (text) => ({ type: "#comment", text, parent: null }),
setText: (node, text) => { record(`setText "${text}"`); node.text = text; },
setElementText: (node, text) => { record(`setElementText "${text}"`); node.children = [{ type: "#text", text, parent: node }]; },
insert: (child, parent, anchor) => {
const i = anchor ? parent.children.indexOf(anchor) : -1;
if (child.parent) child.parent.children.splice(child.parent.children.indexOf(child), 1);
i === -1 ? parent.children.push(child) : parent.children.splice(i, 0, child);
child.parent = parent;
record(`insert ${child.type} into ${parent.type}`);
},
remove: (child) => { const p = child.parent; if (p) p.children.splice(p.children.indexOf(child), 1); record(`remove ${child.type}`); },
patchProp: (node, key, prev, next) => { record(`patchProp ${node.type}.${key} = ${JSON.stringify(next)}`); node.props[key] = next; },
parentNode: (node) => node.parent,
nextSibling: (node) => { const s = node.parent?.children; return s ? s[s.indexOf(node) + 1] ?? null : null; },
});
const radius = ref(10);
const Scene = defineComponent({
setup: () => () => h("group", { name: "root" }, [
h("circle", { r: radius.value, fill: radius.value > 20 ? "red" : "blue" }),
h("label", null, `r = ${radius.value}`),
]),
});
const container = { type: "scene", props: {}, children: [], parent: null };
createSceneApp(Scene).mount(container);
const strip = (n) => (n.type === "#text" ? n.text : { type: n.type, ...(Object.keys(n.props).length ? { props: n.props } : {}), children: n.children.filter((c) => c.type !== "#comment").map(strip) });
const tree = ref(JSON.stringify(strip(container.children[0]), null, 1));
const grow = async () => {
log.value = [];
radius.value += 15;
await Promise.resolve();
await Promise.resolve();
tree.value = JSON.stringify(strip(container.children[0]), null, 1);
};
</script>
<template>
<button @click="grow">grow the circle</button>
<h4>Operations the renderer was asked to do</h4>
<ol><li v-for="(op, i) in log" :key="i">{{ op }}</li></ol>
<h4>The custom tree (not DOM)</h4>
<pre>{{ tree }}</pre>
</template>
A terminal renderer with @vue/runtime-core (run with Node)
import { createRenderer, h, ref, nextTick } from "@vue/runtime-core";
const el = (type) => ({ type, props: {}, children: [], parent: null });
const { createApp } = createRenderer({
createElement: el,
createText: (text) => ({ type: "text", text, parent: null }),
createComment: () => ({ type: "comment", parent: null }),
setText: (node, text) => { node.text = text; },
setElementText: (node, text) => { node.children = [{ type: "text", text, parent: node }]; },
insert(child, parent, anchor) {
if (child.parent) child.parent.children.splice(child.parent.children.indexOf(child), 1);
const i = anchor ? parent.children.indexOf(anchor) : -1;
i < 0 ? parent.children.push(child) : parent.children.splice(i, 0, child);
child.parent = parent;
},
remove(child) { const s = child.parent?.children; if (s) s.splice(s.indexOf(child), 1); },
patchProp(node, key, prev, next) { node.props[key] = next; },
parentNode: (node) => node.parent,
nextSibling(node) { const s = node.parent?.children; return s ? s[s.indexOf(node) + 1] ?? null : null; },
});
const paint = (n, depth = 0) => {
const pad = " ".repeat(depth);
if (n.type === "text") return pad + n.text;
if (n.type === "comment") return null;
if (n.type === "bar") return pad + "[" + "#".repeat(n.props.value) + ".".repeat(10 - n.props.value) + "] " + n.props.value * 10 + "%";
const inner = (d) => n.children.map((c) => paint(c, d)).filter((x) => x !== null);
return n.type === "box" ? [pad + "+-- " + (n.props.title ?? ""), ...inner(depth + 1), pad + "+--"].join("\n") : inner(depth).join("\n");
};
const progress = ref(3);
const App = { setup: () => () => h("box", { title: "Deploy" }, [h("label", null, `step ${progress.value} of 10`), h("bar", { value: progress.value })]) };
const screen = el("screen");
createApp(App).mount(screen);
console.log(paint(screen.children[0]));
progress.value = 7;
await nextTick();
console.log(paint(screen.children[0]));