Merge pull request #451 from nojvek/nojvek-h-tsx
snabbdom can be used with jsx/tsx via jsxFactory: jsxpull/978/head
commit
f49c793eb4
@ -0,0 +1,14 @@
|
||||
import {VNode, VNodeData} from './vnode';
|
||||
|
||||
declare global {
|
||||
/**
|
||||
* opt-in jsx intrinsic global interfaces
|
||||
* see: https://www.typescriptlang.org/docs/handbook/jsx.html#type-checking
|
||||
*/
|
||||
namespace JSX {
|
||||
type Element = VNode;
|
||||
interface IntrinsicElements {
|
||||
[elemName: string]: VNodeData;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
import {vnode, VNode, VNodeData} from './vnode';
|
||||
import {h, ArrayOrElement} from './h';
|
||||
|
||||
// for conditional rendering we support boolean child element e.g cond && <tag />
|
||||
export type JsxVNodeChild = VNode | string | number | boolean | undefined | null;
|
||||
export type JsxVNodeChildren = ArrayOrElement<JsxVNodeChild>
|
||||
|
||||
export type FunctionComponent = (props: {[prop: string]: any} | null, children?: VNode[]) => VNode;
|
||||
|
||||
function flattenAndFilter(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)) {
|
||||
flattenAndFilter(child, flattened);
|
||||
} else if (typeof child === 'string' || typeof child === 'number' || typeof child === 'boolean') {
|
||||
flattened.push(vnode(undefined, undefined, undefined, String(child), undefined));
|
||||
} else{
|
||||
flattened.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
return flattened;
|
||||
}
|
||||
|
||||
/**
|
||||
* jsx/tsx compatible factory function
|
||||
* see: https://www.typescriptlang.org/docs/handbook/jsx.html#factory-functions
|
||||
*/
|
||||
export function jsx(tag: string | FunctionComponent, data: VNodeData | null, ...children: JsxVNodeChildren[]): VNode {
|
||||
const flatChildren = flattenAndFilter(children, []);
|
||||
if (typeof tag === 'function') {
|
||||
// tag is a function component
|
||||
return tag(data, flatChildren);
|
||||
} else {
|
||||
if (flatChildren.length == 1 && !flatChildren[0].sel && flatChildren[0].text) {
|
||||
// only child is a simple text node, pass as text for a simpler vtree
|
||||
return h(tag, data, flatChildren[0].text);
|
||||
} else {
|
||||
return h(tag, data, flatChildren);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
import * as assert from 'assert';
|
||||
import {jsx} from '../src/jsx';
|
||||
|
||||
describe('snabbdom', function() {
|
||||
describe('jsx', function() {
|
||||
it('can be used as a jsxFactory method', function() {
|
||||
const vnode = <div title="Hello World">Hello World</div>;
|
||||
|
||||
assert.equal(JSON.stringify(vnode), JSON.stringify({
|
||||
sel: 'div',
|
||||
data: {title: 'Hello World'},
|
||||
text: 'Hello World',
|
||||
}));
|
||||
});
|
||||
|
||||
it('creates text property for text only child', function() {
|
||||
const vnode = <div>foo bar</div>;
|
||||
|
||||
assert.equal(JSON.stringify(vnode), JSON.stringify({
|
||||
sel: 'div',
|
||||
data: {},
|
||||
text: 'foo bar',
|
||||
}));
|
||||
});
|
||||
|
||||
it('creates an array of children for multiple children', function() {
|
||||
const vnode = <div>{'foo'}{'bar'}</div>;
|
||||
|
||||
assert.equal(JSON.stringify(vnode), JSON.stringify({
|
||||
sel: 'div',
|
||||
data: {},
|
||||
children: [
|
||||
{text: 'foo'},
|
||||
{text: 'bar'},
|
||||
]
|
||||
}));
|
||||
});
|
||||
|
||||
it('flattens children', function() {
|
||||
const vnode = (
|
||||
<section>
|
||||
<h1>A Heading</h1>
|
||||
some description
|
||||
{['part1', 'part2'].map(part => <span>{part}</span>)}
|
||||
</section>
|
||||
);
|
||||
|
||||
assert.equal(JSON.stringify(vnode), JSON.stringify({
|
||||
sel: 'section',
|
||||
data: {},
|
||||
children: [
|
||||
{sel: 'h1', data: {}, text: 'A Heading'},
|
||||
{text: 'some description'},
|
||||
{sel: 'span', data: {}, text: 'part1'},
|
||||
{sel: 'span', data: {}, text: 'part2'},
|
||||
],
|
||||
}));
|
||||
});
|
||||
|
||||
it('removes falsey children', function() {
|
||||
const showLogin = false;
|
||||
const showCaptcha = false;
|
||||
const loginAttempts = 0;
|
||||
const userName = ``;
|
||||
const profilePic = undefined;
|
||||
const isLoggedIn = true;
|
||||
const vnode = (
|
||||
<div>
|
||||
Login Form
|
||||
{showLogin && <login-form />}
|
||||
{showCaptcha ? <captcha-form /> : null}
|
||||
{userName}
|
||||
{profilePic}
|
||||
Login Attempts: {loginAttempts}
|
||||
Logged In: {isLoggedIn}
|
||||
</div>
|
||||
);
|
||||
|
||||
assert.equal(JSON.stringify(vnode), JSON.stringify({
|
||||
sel: 'div',
|
||||
data: {},
|
||||
children: [
|
||||
{text: 'Login Form'},
|
||||
{text: 'Login Attempts: '},
|
||||
{text: '0'},
|
||||
{text: 'Logged In: '},
|
||||
{text: 'true'},
|
||||
],
|
||||
}));
|
||||
});
|
||||
|
||||
it('works with a function component', function() {
|
||||
const Part = ({part}: {part: string}) => <span>{part}</span>
|
||||
const vnode = (
|
||||
<div>
|
||||
<a attrs={{href: 'https://github.com/snabbdom/snabbdom'}}>Snabbdom</a>
|
||||
and tsx
|
||||
{['work', 'like', 'a', 'charm!'].map(part => <Part part={part}></Part>)}
|
||||
{'💃🕺🎉'}
|
||||
</div>
|
||||
);
|
||||
|
||||
assert.equal(JSON.stringify(vnode), JSON.stringify({
|
||||
sel: 'div',
|
||||
data: {},
|
||||
children: [
|
||||
{sel: 'a', data: {attrs: {href: 'https://github.com/snabbdom/snabbdom'}}, text: 'Snabbdom'},
|
||||
{text: 'and tsx'},
|
||||
{sel: 'span', data: {}, text: 'work'},
|
||||
{sel: 'span', data: {}, text: 'like'},
|
||||
{sel: 'span', data: {}, text: 'a'},
|
||||
{sel: 'span', data: {}, text: 'charm!'},
|
||||
{text: '💃🕺🎉'},
|
||||
],
|
||||
}));
|
||||
})
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue