Skip to content

Commit

Permalink
feat(hiccup): update deps & attrib handling
Browse files Browse the repository at this point in the history
- add @thi.ng/api dep
- add support for IDeref attrib vals
- add support for `class` attrib obj vals
- add `mergeClasses()` helper
- add IDeref support inside `class` & `style` attribs
- add support for nested data attribs
  • Loading branch information
postspectacular committed Jun 27, 2020
1 parent 7af448f commit 09ba171
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 29 deletions.
1 change: 1 addition & 0 deletions packages/hiccup/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"typescript": "^3.9.2"
},
"dependencies": {
"@thi.ng/api": "^6.11.2",
"@thi.ng/checks": "^2.7.2",
"@thi.ng/errors": "^1.2.15",
"tslib": "^1.12.0"
Expand Down
38 changes: 38 additions & 0 deletions packages/hiccup/src/classes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { deref } from "@thi.ng/api";
import { isString } from "@thi.ng/checks";

/**
* Takes a space separated string of existing CSS class names and merges
* it with `val`, which is either another string of class names, an
* object of booleans or an `IDeref` evaluating to either. Returns
* updated class string.
*
* @remarks
* If `val` evaluates to a string, it will be appended to `existing`.
*
* If `val` is an object, its keys are used as class names and their
* values indicate if the class should be added or removed from the
* existing set.
*
* @example
* ```ts
* mergeClasses("foo bar", { foo: false, baz: true })
* // "bar baz"
*
* mergeClasses("foo bar", "baz");
* // "baz"
* ```
*
* @param existing
* @param val
*/
export const mergeClasses = (existing: string, val: any) => {
val = deref(val);
if (val == null) return existing;
if (isString(val)) return existing + " " + val;
const classes = new Set(existing.split(" "));
for (let id in val) {
deref(val[id]) ? classes.add(id) : classes.delete(id);
}
return [...classes].join(" ");
};
11 changes: 5 additions & 6 deletions packages/hiccup/src/css.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { deref } from "@thi.ng/api";
import { isFunction } from "@thi.ng/checks";

export const css = (rules: any) => {
let css = "",
v;
let css = "";
let v: any;
for (let r in rules) {
v = rules[r];
if (isFunction(v)) {
v = v(rules);
}
v = deref(rules[r]);
isFunction(v) && (v = v(rules));
v != null && (css += `${r}:${v};`);
}
return css;
Expand Down
1 change: 1 addition & 0 deletions packages/hiccup/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./api";
export * from "./classes";
export * from "./css";
export * from "./deref";
export * from "./escape";
Expand Down
19 changes: 7 additions & 12 deletions packages/hiccup/src/normalize.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { deref } from "@thi.ng/api";
import { isPlainObject, isString } from "@thi.ng/checks";
import { illegalArgs } from "@thi.ng/errors";
import { RE_TAG } from "./api";
import { css } from "./css";
import { mergeClasses } from "./classes";

export const normalize = (tag: any[]) => {
let el = tag[0];
Expand All @@ -16,21 +17,15 @@ export const normalize = (tag: any[]) => {
el = match![1];
id = match![2];
clazz = match![3];
if (id) {
attribs.id = id;
}
id && (attribs.id = id);
let aclass = deref(attribs.class);
if (clazz) {
clazz = clazz.replace(/\./g, " ");
if (attribs.class) {
attribs.class += " " + clazz;
} else {
attribs.class = clazz;
}
attribs.class = aclass ? mergeClasses(clazz, aclass) : clazz;
} else if (aclass) {
attribs.class = isString(aclass) ? aclass : mergeClasses("", aclass);
}
if (tag.length > 1) {
if (isPlainObject(attribs.style)) {
attribs.style = css(attribs.style);
}
tag = tag.slice(hasAttribs ? 2 : 1).filter((x) => x != null);
if (tag.length > 0) {
return [el, attribs, tag];
Expand Down
28 changes: 24 additions & 4 deletions packages/hiccup/src/serialize.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { deref, isDeref } from "@thi.ng/api";
import {
implementsFunction,
isArray,
isFunction,
isNotStringAndIterable,
isPlainObject,
isString,
isArray,
} from "@thi.ng/checks";
import { illegalArgs } from "@thi.ng/errors";
import {
ATTRIB_JOIN_DELIMS,
COMMENT,
NO_SPANS,
PROC_TAGS,
VOID_TAGS,
ATTRIB_JOIN_DELIMS,
} from "./api";
import { css } from "./css";
import { escape } from "./escape";
import { normalize } from "./normalize";

Expand Down Expand Up @@ -175,7 +178,7 @@ const _serialize = (
if (implementsFunction(tree, "toHiccup")) {
return _serialize(tree.toHiccup(ctx), ctx, esc, span, keys, path);
}
if (implementsFunction(tree, "deref")) {
if (isDeref(tree)) {
return _serialize(tree.deref(), ctx, esc, span, keys, path);
}
if (isNotStringAndIterable(tree)) {
Expand Down Expand Up @@ -250,10 +253,17 @@ const serializeAttribs = (attribs: any, esc: boolean) => {
let res = "";
for (let a in attribs) {
if (a.startsWith("__")) continue;
let v = attribs[a];
let v = deref(attribs[a]);
if (v == null) continue;
if (isFunction(v) && (/^on\w+/.test(a) || (v = v(attribs)) == null))
continue;
if (a === "data") {
res += serializeDataAttribs(v, esc);
continue;
}
if (a === "style") {
isPlainObject(v) && (v = css(v));
}
if (v === true) {
res += " " + a;
} else if (v !== false) {
Expand All @@ -266,6 +276,16 @@ const serializeAttribs = (attribs: any, esc: boolean) => {
return res;
};

const serializeDataAttribs = (data: any, esc: boolean) => {
let res = "";
for (let id in data) {
let v = deref(data[id]);
isFunction(v) && (v = v(data));
v != null && (res += ` data-${id}="${esc ? escape(v) : v}"`);
}
return res;
};

const serializeBody = (
tag: string,
body: any[],
Expand Down
87 changes: 80 additions & 7 deletions packages/hiccup/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,43 @@ describe("serialize", () => {
check(
"div w/ class merging",
["div.foo", { class: "bar baz" }, "foo"],
`<div class="bar baz foo">foo</div>`
`<div class="foo bar baz">foo</div>`
);

check(
"div w/ class merging (deref)",
[
"div.foo",
{
class: {
deref() {
return "bar baz";
},
},
},
"foo",
],
`<div class="foo bar baz">foo</div>`
);

check(
"deref attribs",
[
"div",
{
id: {
deref() {
return "foo";
},
},
style: {
deref() {
return { color: "red" };
},
},
},
],
`<div id="foo" style="color:red;"></div>`
);

check("voidtag (br)", ["br"], `<br/>`);
Expand Down Expand Up @@ -121,6 +157,49 @@ describe("serialize", () => {

check("style empty", ["div", { style: {} }, "foo"], `<div>foo</div>`);

check(
"style fn attribs",
["div", { style: { a: (x: any) => x.b, b: 2 } }],
`<div style="a:2;b:2;"></div>`
);

check(
"style deref attribs",
[
"div",
{
style: {
a: {
deref() {
return 1;
},
},
},
},
],
`<div style="a:1;"></div>`
);

check(
"data attribs",
[
"div",
{
"data-xyz": "xyz",
data: {
foo: 1,
bar: (a: any) => a.foo + 1,
baz: {
deref() {
return 3;
},
},
},
},
],
`<div data-xyz="xyz" data-foo="1" data-bar="2" data-baz="3"></div>`
);

check(
"nested",
[
Expand Down Expand Up @@ -238,12 +317,6 @@ describe("serialize", () => {

check("deref fn result", ["a", () => new Atom(["b"])], `<a><b></b></a>`);

check(
"style fn attribs",
["div", { style: { a: (x: any) => x.b, b: 2 } }],
`<div style="a:2;b:2;"></div>`
);

check(
"__skip",
["a", ["b", { __skip: true }, "bb"], ["b", "bbb"]],
Expand Down

0 comments on commit 09ba171

Please sign in to comment.