Skip to content

Commit

Permalink
refactor(hiccup): fix #19, add support for context object
Browse files Browse the repository at this point in the history
BREAKING CHANGE: component functions now take a global context object as
first argument (like w/ @thi.ng/hdom)

- update serialize() to accept & pass optional context
- add support for component objects
- add/update tests
  • Loading branch information
postspectacular committed May 12, 2018
1 parent 4b8caec commit feca566
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 36 deletions.
66 changes: 37 additions & 29 deletions packages/hiccup/src/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@ import { TAG_REGEXP, VOID_TAGS } from "./api";
import { css } from "./css";

/**
* Recursively normalizes and serializes given tree as HTML/SVG/XML string.
* Expands any embedded component functions with their results. Each node of the
* input tree can have one of the following input forms:
* Recursively normalizes and serializes given tree as HTML/SVG/XML
* string. Expands any embedded component functions with their results.
* Each node of the input tree can have one of the following input
* forms:
*
* ```js
* ["tag", ...]
* ["tag#id.class1.class2", ...]
* ["tag", {other: "attrib"}, ...]
* ["tag", {...}, "body", function, ...]
* [function, arg1, arg2, ...]
* [iterable]
* [{render: (ctx,...) => [...]}, args...]
* iterable
* ```
*
* Tags can be defined in "Zencoding" convention, e.g.
Expand All @@ -28,10 +30,12 @@ import { css } from "./css";
* ```
*
* The presence of the attributes object (2nd array index) is optional.
* Any attribute values, incl. functions are allowed. If the latter,
* the function is called with the full attribs object as argument and
* MUST return a string. This allows for the dynamic creation of attrib
* values based on other attribs.
* Any attribute values, incl. functions are allowed. If the latter, the
* function is called with the full attribs object as argument and the
* return value is used for the attribute. This allows for the dynamic
* creation of attrib values based on other attribs. The only exception
* to this are event attributes, i.e. attribute names starting with
* "on".
*
* ```js
* ["div#foo", {bar: (attribs) => attribs.id + "-bar"}]
Expand All @@ -51,30 +55,31 @@ import { css } from "./css";
* Any `null` or `undefined` array values (other than in head position)
* will be removed, unless a function is in head position.
*
* A function in head position of a node acts as composition & delayed
* execution mechanism and the function will only be executed at
* serialization time. In this case all other elements of that node /
* array are passed as arguments when that function is called.
* The return value the function MUST be a valid new tree
* (or `undefined`).
* A function in head position of a node acts as a mechanism for
* component composition & delayed execution. The function will only be
* executed at serialization time. In this case the optional global
* context object and all other elements of that node / array are passed
* as arguments when that function is called. The return value the
* function MUST be a valid new tree (or `undefined`).
*
* ```js
* const foo = (a, b) => ["div#" + a, b];
* const foo = (ctx, a, b) => ["div#" + a, ctx.foo, b];
*
* [foo, "id", "body"] // <div id="id">body</div>
* serialize([foo, "id", "body"], {foo: {class: "black"}})
* // <div id="id" class="black">body</div>
* ```
*
* Functions located in other positions are called **without** args
* and can return any (serializable) value (i.e. new trees, strings,
* numbers, iterables or any type with a suitable `.toString()`
* Functions located in other positions are called ONLY with the global
* context arg and can return any (serializable) value (i.e. new trees,
* strings, numbers, iterables or any type with a suitable `.toString()`
* implementation).
*
* @param tree elements / component tree
* @param escape auto-escape entities
*/
export const serialize = (tree: any[], escape = false) => _serialize(tree, escape);
export const serialize = (tree: any[], ctx?: any, escape = false) => _serialize(tree, ctx, escape);

const _serialize = (tree: any, esc: boolean) => {
const _serialize = (tree: any, ctx: any, esc: boolean) => {
if (tree == null) {
return "";
}
Expand All @@ -84,7 +89,10 @@ const _serialize = (tree: any, esc: boolean) => {
}
let tag = tree[0];
if (isFunction(tag)) {
return _serialize(tag.apply(null, tree.slice(1)), esc);
return _serialize(tag.apply(null, [ctx, ...tree.slice(1)]), ctx, esc);
}
if (implementsFunction(tag, "render")) {
return _serialize(tag.render.apply(null, [ctx, ...tree.slice(1)]), ctx, esc);
}
if (isString(tag)) {
tree = normalize(tree);
Expand Down Expand Up @@ -118,7 +126,7 @@ const _serialize = (tree: any, esc: boolean) => {
}
res += ">";
for (let i = 0, n = body.length; i < n; i++) {
res += _serialize(body[i], esc);
res += _serialize(body[i], ctx, esc);
}
return res += `</${tag}>`;
} else if (!VOID_TAGS[tag]) {
Expand All @@ -127,26 +135,26 @@ const _serialize = (tree: any, esc: boolean) => {
return res += "/>";
}
if (iter(tree)) {
return _serializeIter(tree, esc);
return _serializeIter(tree, ctx, esc);
}
illegalArgs(`invalid tree node: ${tree}`);
}
if (isFunction(tree)) {
return _serialize(tree(), esc);
return _serialize(tree(ctx), ctx, esc);
}
if (implementsFunction(tree, "deref")) {
return _serialize(tree.deref(), esc);
return _serialize(tree.deref(), ctx, esc);
}
if (iter(tree)) {
return _serializeIter(tree, esc);
return _serializeIter(tree, ctx, esc);
}
return esc ? escape(tree.toString()) : tree;
};

const _serializeIter = (iter: Iterable<any>, esc: boolean) => {
const _serializeIter = (iter: Iterable<any>, ctx: any, esc: boolean) => {
const res = [];
for (let i of iter) {
res.push(_serialize(i, esc));
res.push(_serialize(i, ctx, esc));
}
return res.join("");
}
Expand Down
29 changes: 22 additions & 7 deletions packages/hiccup/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ describe("serialize", () => {
`<div foo="23"></div>`
);

check(
"attr fn (derived)",
["div", { foo: (attribs) => `${attribs.x}px`, x: 42 }],
`<div foo="42px" x="42"></div>`
);

check(
"attr fn (null)",
["div", { foo: () => null }],
Expand Down Expand Up @@ -169,7 +175,7 @@ describe("serialize", () => {

check(
"comp fn args",
[(id, body) => ["div#" + id, body], "foo", "bar"],
[(_, id, body) => ["div#" + id, body], "foo", "bar"],
`<div id="foo">bar</div>`
);

Expand All @@ -181,13 +187,13 @@ describe("serialize", () => {

check(
"comp fn in body w/ args",
["div", [(id, body) => ["div#" + id, body], "foo", "bar"], "bar2"],
["div", [(_, id, body) => ["div#" + id, body], "foo", "bar"], "bar2"],
`<div><div id="foo">bar</div>bar2</div>`
);

check(
"comp fn in body apply",
["div", [([id, body]) => ["div#" + id, body], ["foo", "bar"]], "bar2"],
["div", [(_, [id, body]) => ["div#" + id, body], ["foo", "bar"]], "bar2"],
`<div><div id="foo">bar</div>bar2</div>`
);

Expand All @@ -200,9 +206,9 @@ describe("serialize", () => {
it("components nested", () => {
const dlItem = ([def, desc]) => [["dt", def], ["dd", desc]];
const ulItem = (i) => ["li", i];
const list = (f, items) => items.map(f);
const dlList = (attribs, items) => ["dl", attribs, [list, dlItem, items]];
const ulList = (attribs, items) => ["ul", attribs, [list, ulItem, items]];
const list = (_, f, items) => items.map(f);
const dlList = (_, attribs, items) => ["dl", attribs, [list, dlItem, items]];
const ulList = (_, attribs, items) => ["ul", attribs, [list, ulItem, items]];

const items = [["a", "foo"], ["b", "bar"]];

Expand All @@ -213,9 +219,18 @@ describe("serialize", () => {
_check(widget2, `<ul id="foo"><li>foo</li><li>bar</li></ul>`);
});

it("comp object", () => {
const foo = (ctx, body) => ["div", ctx.foo, body];
const bar = { render: (_, id) => [foo, id] };
assert.equal(
serialize(["section", [bar, "a"], [bar, "b"]], { foo: { class: "foo" } }),
`<section><div class="foo">a</div><div class="foo">b</div></section>`
);
});

check(
"iterators",
["ul", [(items) => items.map((i) => ["li", i]), ["a", "b"]]],
["ul", [(_, items) => items.map((i) => ["li", i]), ["a", "b"]]],
`<ul><li>a</li><li>b</li></ul>`
);

Expand Down

0 comments on commit feca566

Please sign in to comment.