Skip to content
Closed
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
52 changes: 41 additions & 11 deletions src/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const isFunc = (x) => typeof x === "function";

const isStr = (x) => typeof x === "string";

const isNode = (x) => x instanceof Node;

const isObjectLiteral = (x) => Object.prototype.toString.call(x) === "[object Object]";

const isMark = (x) => x instanceof Mark;
Expand All @@ -32,6 +34,10 @@ function snake2kebab(str) {
return str.replace(/_/g, "-");
}

function markify(node) {
return new Mark(null, node);
}

// Ref: https://github.com/vanjs-org/van/blob/d09cfd1e1e3b5ea7cf8d0a9b5deacca4c0946fb4/src/van.js#L99
function set(dom, k, v) {
k = snake2kebab(k);
Expand All @@ -44,6 +50,32 @@ function set(dom, k, v) {
setter(v);
}

function createComponent(_, tag, attrs, d, i, array) {
const children = [];
const props = {};
for (const [k, v] of Object.entries(attrs)) props[k] = isFunc(v) ? v(d, i, array) : v;
return {
append: (...nodes) => {
for (const node of nodes) children.push(markify(node));
},
render: () => {
const nodes = [tag({...props, children})].flat();
const fragment = document.createDocumentFragment();
for (const child of nodes) if (child) fragment.append(renderMark(child));
return fragment;
},
};
}

function createDOM(ns, tag, attrs, d, i, array) {
const dom = ns ? document.createElementNS(ns, tag) : document.createElement(tag);
for (const [k, v] of Object.entries(attrs)) {
const val = k.startsWith("on") ? (e) => v(e, d, i, array) : isFunc(v) ? v(d, i, array) : v;
set(dom, k, val);
}
return dom;
}

class Mark {
constructor(ns, tag, data, options) {
if (isObjectLiteral(data)) (options = data), (data = undefined);
Expand All @@ -63,16 +95,12 @@ class Mark {

function renderNodes(mark) {
const {_ns: ns, _tag: tag, _data: data = [undefined], _options: options = {}} = mark;
if (!isStr(tag)) return null;
const isComponent = isFunc(tag);
if (isNode(tag)) return [tag];
if (!isStr(tag) && !isComponent) return null;
const {children = [], ...attrs} = options;
const nodes = data.map((d, i, array) => {
const dom = ns ? document.createElementNS(ns, tag) : document.createElement(tag);
for (const [k, v] of Object.entries(attrs)) {
const val = k.startsWith("on") ? (e) => v(e, d, i, array) : isFunc(v) ? v(d, i, array) : v;
set(dom, k, val);
}
return dom;
});
const creator = (isComponent ? createComponent : createDOM).bind(null, ns, tag, attrs);
const nodes = data.map(creator);
for (const child of children.filter(isTruthy).flat(Infinity)) {
const n = nodes.length;
if (!isMark(child)) {
Expand All @@ -99,7 +127,7 @@ function renderNodes(mark) {
for (let i = 0; i < n; i++) nodes[i].append(childNodes[i]);
}
}
return nodes;
return isComponent ? nodes.map((d) => d.render()) : nodes;
}

export const renderMark = (mark) => postprocess(renderNodes(mark));
Expand All @@ -108,6 +136,8 @@ export const render = (options) => renderMark(svg("svg", preprocess(options)));

export const tag = (ns) => (tag, data, options) => new Mark(ns, tag, data, options);

export const mark = tag(null);

export const svg = tag("http://www.w3.org/2000/svg");

export const html = tag(null);
export const html = mark;
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export {svg, html, tag, render, renderMark} from "./dom.js";
export {svg, html, tag, render, renderMark, mark} from "./dom.js";
11 changes: 11 additions & 0 deletions test/output/basicComponent.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<svg
height="100"
width="100"
>
<g>
<circle
fill="red"
r="10"
/>
</g>
</svg>
17 changes: 17 additions & 0 deletions test/output/basicComponentWithMultipleNodes.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<svg
height="100"
width="100"
>
<circle
cx="50"
cy="50"
fill="red"
r="20"
/>
<circle
cx="50"
cy="50"
fill="blue"
r="10"
/>
</svg>
26 changes: 26 additions & 0 deletions test/output/componentWidthDataDrivenChildren.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<div>
<div>
<h1>
Hello 1
</h1>
<p>
World 1
</p>
</div>
<div>
<h1>
Hello 2
</h1>
<p>
World 2
</p>
</div>
<div>
<h1>
Hello 3
</h1>
<p>
World 3
</p>
</div>
</div>
10 changes: 10 additions & 0 deletions test/output/componentWithChildren.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<div>
<div>
<h1>
Hello
</h1>
<p>
World
</p>
</div>
</div>
23 changes: 23 additions & 0 deletions test/output/dataDrivenComponent.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<svg
height="100"
width="100"
>
<g>
<circle
fill="red"
r="10"
/>
</g>
<g>
<circle
fill="red"
r="20"
/>
</g>
<g>
<circle
fill="red"
r="30"
/>
</g>
</svg>
4 changes: 4 additions & 0 deletions test/output/nullComponent.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<svg
height="100"
width="100"
/>
4 changes: 4 additions & 0 deletions test/output/nullsComponent.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<svg
height="100"
width="100"
/>
80 changes: 79 additions & 1 deletion test/snapshots.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {html, tag, svg, render, renderMark} from "../src/index.js";
import {html, tag, svg, mark, render, renderMark} from "../src/index.js";

export function strictNull() {
return renderMark(svg());
Expand Down Expand Up @@ -302,3 +302,81 @@ export function mathXL() {
]),
);
}

export function basicComponent() {
const redCircle = (props) => svg("g").with([svg("circle", {fill: "red", ...props})]);
return render({
width: 100,
height: 100,
marks: [mark(redCircle, {r: 10})],
});
}

export function nullComponent() {
const nullComponent = () => null;
return render({
width: 100,
height: 100,
marks: [mark(nullComponent, {r: 10})],
});
}

export function nullsComponent() {
const nullComponent = () => [null, null, null];
return render({
width: 100,
height: 100,
marks: [mark(nullComponent, {r: 10})],
});
}

export function basicComponentWithMultipleNodes() {
const ring = ({cx, cy}) => [
svg("circle", {cx, cy, fill: "red", r: 20}),
svg("circle", {cx, cy, fill: "blue", r: 10}),
];
return render({
width: 100,
height: 100,
marks: [mark(ring, {cx: 50, cy: 50})],
});
}

export function dataDrivenComponent() {
const redCircle = (props) => svg("g").with([svg("circle", {fill: "red", ...props})]);
return render({
width: 100,
height: 100,
marks: [mark(redCircle, [1, 2, 3], {r: (d) => d * 10})],
});
}

export function componentWithChildren() {
const withTitle = ({title, children}) => html("div").with([html("h1", {textContent: title}), ...children]);
return renderMark(
html("div").with([
mark(withTitle, {
title: "Hello",
}).with([
html("p", {
textContent: "World",
}),
]),
]),
);
}

export function componentWidthDataDrivenChildren() {
const withTitle = ({title, children}) => html("div").with([html("h1", {textContent: title}), ...children]);
return renderMark(
html("div").with([
mark(withTitle, [1, 2, 3], {
title: (d) => `Hello ${d}`,
}).with([
html("p", {
textContent: (d) => `World ${d}`,
}),
]),
]),
);
}