snabbdom-jsx-lite
Version:
Write snabbdom templates in .jsx or .tsx (JSX for TypeScript)
94 lines (84 loc) • 3.71 kB
text/typescript
import {vnode, VNode, VNodeData} from 'snabbdom';
// for conditional rendering we support boolean child element e.g cond && <tag />
export type JsxVNode = VNode;
export type JsxVNodeChild = VNode | string | number | boolean | undefined | null;
export type JsxVNodeChildren = JsxVNodeChild | JsxVNodeChild[];
export interface JsxVNodeProps extends VNodeData {
/** css selector shorthand e.g <div sel="#id.class1.class1" /> */
sel?: string;
}
export type FunctionComponent = (props: {[prop: string]: any} | null, children?: VNode[]) => VNode;
/** Equivalent of <> (React.Fragment) that that wraps children without a containing dom element */
export function Fragment(_props: {}, children?: Array<string | VNode>): VNode {
return vnode(undefined, undefined, children, undefined, undefined);
}
function flattenAndFilterFalsey(children: JsxVNodeChildren[], flattened: VNode[]): VNode[] {
for (const child of children) {
// filter out falsey children, except 0 since zero can be a valid value e.g inside a chart
if (child !== undefined && child !== null && child !== false && child !== ``) {
if (Array.isArray(child)) {
flattenAndFilterFalsey(child, flattened);
} else if (typeof child === `string` || typeof child === `number` || typeof child === `boolean`) {
flattened.push(vnode(undefined, undefined, undefined, String(child), undefined));
} else if (child.sel === undefined && child.text === undefined) {
// vnode from Fragment
if (Array.isArray(child.children)) {
flattenAndFilterFalsey(child.children, flattened);
}
} else {
flattened.push(child);
}
}
}
return flattened;
}
function addSvgNs(sel: string, data: VNodeData | undefined, children: VNode[] | undefined): void {
data.ns = `http://www.w3.org/2000/svg`;
if (sel !== `foreignObject` && children !== undefined) {
for (const child of children) {
if (child.data !== undefined) {
addSvgNs(child.sel, child.data, child.children as VNode[]);
}
}
}
}
/**
* jsx/tsx + hyperscript compatible vnode factory function
* see: https://www.typescriptlang.org/docs/handbook/jsx.html#factory-functions
*/
export function jsx(
tag: string | FunctionComponent | null,
data: JsxVNodeProps | null,
...children: JsxVNodeChildren[]
): JsxVNode {
// don't call flattenAndFilterFalsey if no children
const flattenedChildren = children.length > 0 ? flattenAndFilterFalsey(children, []) : (children as []);
if (typeof tag === `function`) {
// tag is a function component
return tag(data, flattenedChildren);
} else {
if (tag === null) {
// Fragment via jsxFragmentFactory: 'null' compiler option
// change to undefined since snabbdom expects `tag: string | undefined`
tag = undefined as string; // `as string` so we don't get TS typeguard error
} else if (tag === `svg`) {
// svg elements need recursive namespace
addSvgNs(tag, data, flattenedChildren);
}
// append sel css selector to tag to support equivalent of h('div.foo.bar')
data = data ?? {};
if (data.sel) {
tag += data.sel;
data.sel = undefined;
}
const numFlattenedChildren = flattenedChildren.length;
if (numFlattenedChildren === 0) {
return vnode(tag, data, undefined, undefined, undefined);
} else if (numFlattenedChildren === 1 && flattenedChildren[0].sel === undefined && flattenedChildren[0].text) {
// only child is a simple text node, return as simple text node
return vnode(tag, data, undefined, flattenedChildren[0].text, undefined);
} else {
return vnode(tag, data, flattenedChildren, undefined, undefined);
}
}
}