Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 5 additions & 1 deletion src/dom/html.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import {tag} from "./tag.js";

export const html = tag(null);
export const html = tag((string) => {
const div = document.createElement("div");
div.innerHTML = string;
return div;
});
1 change: 0 additions & 1 deletion src/dom/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export {tag} from "./tag.js";
export {svg} from "./svg.js";
export {html} from "./html.js";
export {attr} from "./attr.js";
6 changes: 5 additions & 1 deletion src/dom/svg.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import {tag} from "./tag.js";

export const svg = tag("http://www.w3.org/2000/svg");
export const svg = tag((string) => {
const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
g.innerHTML = string;
return g;
});
177 changes: 115 additions & 62 deletions src/dom/tag.js
Original file line number Diff line number Diff line change
@@ -1,49 +1,58 @@
import {set} from "./attr.js";

const isFunc = (x) => typeof x === "function";
const TYPE_NODE = 1;

const isTruthy = (x) => x != null && x !== false;
const TYPE_TEXT = 3;

const isString = (x) => typeof x === "string";
const isFunc = (x) => typeof x === "function";

const isNode = (x) => x?.nodeType;

export const tag = (ns) => (name, options) => {
if (!isString(name)) return null;
const isArray = Array.isArray;

// Normalize arguments.
if (Array.isArray(options)) options = {children: options};
if (options === undefined) options = {};
const isPlainObject = (x) => typeof x === "object" && x !== null && !Array.isArray(x);

// Create node and store parameters.
const node = ns ? document.createElementNS(ns, name) : document.createElement(name);
node.__options__ = options;
node.__name__ = name;
function postprocess(node) {
if (node.firstChild === node.lastChild) {
return node.firstChild;
}
return node;
}

const {data, ...rest} = options;
function cloneNode(node) {
const cloned = node.cloneNode(true);
cloned.__context__ = node.__context__;
return cloned;
}

// Nested groups.
if (isFunc(data)) return node;
function contextOf(node) {
let p = node.parentNode;
while (p) {
if (p.__context__) return p.__context__;
p = p.parentNode;
}
return null;
}

function hydrate(node, value) {
if (!isPlainObject(value)) return;

const {data, ...attrs} = value;
const context = contextOf(node);

// Non data driven node.
if (!data) {
const {children = [], ...attrs} = rest;
if (!data && !context) {
for (const [k, v] of Object.entries(attrs)) {
const val = k.startsWith("on") ? v : isFunc(v) ? v(undefined, undefined, undefined, node) : v;
const val = k.startsWith("on") ? v : isFunc(v) ? v(node) : v;
set(node, k, val);
}
for (const c of children.filter(isTruthy).flat(Infinity)) {
const child = isNode(c) ? c : document.createTextNode("" + c);
node.append(child);
}
return node;
return;
}

// Data driven node.
const {children = [], ...attrs} = rest;
const nodes = data.map((d, i, array) => {
const node = ns ? document.createElementNS(ns, name) : document.createElement(name);
// Non data driven node in a data driven parent.
if (!data && context) {
for (const [k, v] of Object.entries(attrs)) {
const [d, i, array] = context;
if (k.startsWith("on")) {
const [l, o] = Array.isArray(v) ? v : [v];
const val = (e) => l(e, node, d, i, array);
Expand All @@ -53,44 +62,88 @@ export const tag = (ns) => (name, options) => {
set(node, k, val);
}
}
return node;
});
return;
}

for (const c of children.filter(isTruthy).flat(Infinity)) {
const n = nodes.length;
const __data__ = c?.__options__?.data;
if (isFunc(c)) {
// Data driven children, evaluate on parent data.
for (let i = 0; i < n; i++) {
let child = c(data[i], i, data, nodes[i]);
if (isTruthy(child)) {
child = isNode(child) ? child : document.createTextNode("" + child);
nodes[i].append(child);
}
}
} else if (__data__) {
// Nested groups, evaluate on derived data from parent data.
for (let i = 0; i < n; i++) {
const childData = isFunc(__data__) ? __data__(data[i], i, data, nodes[i]) : __data__;
const childNode = tag(ns)(c.__name__, {...c.__options__, data: childData});
if (childNode) nodes[i].append(childNode);
}
} else {
// Appended groups, evaluate on parent data.
let childNodes = [];
if (isNode(c)) {
const fragment = tag(ns)(c.__name__, {...c.__options__, data});
if (fragment) childNodes = Array.from(fragment.childNodes);
// Data driven node.
const nodeData = context && isFunc(data) ? data(context[0], context[1], context[2], node) : data;
const nodes = nodeData.map((d, i, array) => {
const cloned = cloneNode(node);
for (const [k, v] of Object.entries(attrs)) {
if (k.startsWith("on")) {
const [l, o] = Array.isArray(v) ? v : [v];
const val = (e) => l(e, cloned, d, i, array);
set(cloned, k, [val, o]);
} else {
childNodes = data.map(() => document.createTextNode("" + c));
const val = isFunc(v) ? v(d, i, array, node) : v;
set(cloned, k, val);
}
for (let i = 0; i < n; i++) nodes[i].append(childNodes[i]);
}
cloned.__context__ = [d, i, array, cloned];
return cloned;
});
const temp = document.createDocumentFragment();
temp.append(...nodes);
node.parentNode.insertBefore(temp, node.nextSibling);

return node;
}

function append(node, value) {
const children = [isArray(value) ? value : [value]].flat(Infinity);
const temp = document.createDocumentFragment();
const c = contextOf(node);
for (const child of children) {
const n = isFunc(child) && c ? child(c[0], c[1], c[2]) : child;
if (isNode(n)) temp.append(n);
}
node.parentNode.insertBefore(temp, node);
}

const root = document.createDocumentFragment();
root.append(...nodes);
root.__options__ = options;
root.__name__ = name;
return root;
};
export function tag(render) {
return function ({raw: strings}) {
let string = "";
for (let j = 0, m = strings.length; j < m; j++) {
const input = strings[j];
if (j > 0) string += "::" + j;
string += input;
}
const root = render(string);
const walker = document.createTreeWalker(root); // DFS walker.
const removeNodes = [];
while (walker.nextNode()) {
const node = walker.currentNode;
switch (node.nodeType) {
case TYPE_NODE: {
const attributes = node.attributes;
for (let i = 0, n = attributes.length; i < n; i++) {
const {name} = attributes[i];
if (/^::/.test(name)) {
const value = arguments[+name.slice(2)];
node.removeAttribute(name);
const removed = hydrate(node, value);
if (removed) {
removeNodes.push(removed);
walker.nextSibling(); // Skip the children of the removed node.
}
}
}
break;
}
case TYPE_TEXT: {
const data = node.data.trim();
if (/^::/.test(data)) {
const value = arguments[+data.slice(2)];
append(node, value);
removeNodes.push(node);
}
break;
}
default:
break;
}
}
for (const node of removeNodes) node.remove();
return postprocess(root);
};
}
6 changes: 3 additions & 3 deletions test/event.spec.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import {test, expect, vi} from "vitest";
import {svg} from "../src/index.js";
import * as cm from "../src/index.js";

test("svg(tag, options) should set events", () => {
const click = vi.fn();
const root = svg("svg", {onclick: click});
const root = cm.svg`<svg ${{onclick: click}} />`;
const event = new Event("click");
root.dispatchEvent(event);
expect(click).toHaveBeenCalledWith(event, root, undefined, undefined, undefined);
});

test("svg(tag, options) should pass datum to event handler", () => {
const click = vi.fn();
const root = svg("svg", {data: [1, 2, 3], onclick: click});
const root = cm.svg`<svg ${{data: [1, 2, 3], onclick: click}} />`;
const el = root.children[0];
const event = new Event("click");
el.dispatchEvent(event);
Expand Down
4 changes: 2 additions & 2 deletions test/output/fragmentRoot.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<DocumentFragment>
<g>
<circle
cx="20"
cy="50"
Expand All @@ -14,4 +14,4 @@
cy="50"
r="10"
/>
</DocumentFragment>
</g>
4 changes: 2 additions & 2 deletions test/output/setCallbackChildren.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<DocumentFragment>
<g>
<g
transform="translate(50, 0)"
>
Expand Down Expand Up @@ -26,4 +26,4 @@
r="20"
/>
</g>
</DocumentFragment>
</g>
4 changes: 2 additions & 2 deletions test/output/setDataDrivenFunctionWithNode.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<DocumentFragment>
<g>
<circle
r="0"
/>
Expand All @@ -8,4 +8,4 @@
<circle
r="0"
/>
</DocumentFragment>
</g>
2 changes: 1 addition & 1 deletion test/output/setNonMarkChildren.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
<span>
world
</span>
[object Object]
{key: "foo"}
</div>
Loading
Loading