feat: support JSX fragments

ISSUES CLOSED: #560
pull/974/head
Ryota Kameoka 4 years ago
parent ce95a24e73
commit ffc7c513c1

@ -780,6 +780,9 @@ significant computational time to generate.
## JSX
Note that JSX fragments are still experimental and must be opted in.
See [`fragment`](#fragment-experimental) section for details.
### TypeScript
Add the following options to your `tsconfig.json`:
@ -788,21 +791,29 @@ Add the following options to your `tsconfig.json`:
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "jsx"
"jsxFactory": "jsx",
"jsxFragmentFactory": "Fragment"
}
}
```
Then make sure that you use the `.tsx` file extension and import the `jsx` function at the top of the file:
Then make sure that you use the `.tsx` file extension and import the `jsx` function and the `Fragment` function at the top of the file:
```tsx
import { jsx, VNode } from "snabbdom";
import { Fragment, jsx, VNode } from "snabbdom";
const node: VNode = (
<div>
<span>I was created with JSX</span>
</div>
);
const fragment: VNode = (
<>
<span>JSX fragments</span>
are experimentally supported
</>
);
```
### Babel
@ -815,23 +826,31 @@ Add the following options to your babel configuration:
[
"@babel/plugin-transform-react-jsx",
{
"pragma": "jsx"
"pragma": "jsx",
"pragmaFrag": "Fragment"
}
]
]
}
```
Then import the `jsx` function at the top of the file:
Then import the `jsx` function and the `Fragment` function at the top of the file:
```jsx
import { jsx } from "snabbdom";
import { Fragment, jsx } from "snabbdom";
const node = (
<div>
<span>I was created with JSX</span>
</div>
);
const fragment = (
<>
<span>JSX fragments</span>
are experimentally supported
</>
);
```
## Virtual Node

@ -30,4 +30,10 @@ export { Props, propsModule } from "./modules/props";
export { VNodeStyle, styleModule } from "./modules/style";
// JSX
export { JsxVNodeChild, JsxVNodeChildren, FunctionComponent, jsx } from "./jsx";
export {
JsxVNodeChild,
JsxVNodeChildren,
FunctionComponent,
jsx,
Fragment,
} from "./jsx";

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-namespace, import/export */
import { vnode, VNode, VNodeData } from "./vnode";
import { Key, vnode, VNode, VNodeData } from "./vnode";
import { h, ArrayOrElement } from "./h";
// See https://www.typescriptlang.org/docs/handbook/jsx.html#type-checking
@ -25,6 +25,30 @@ export type FunctionComponent = (
children?: VNode[]
) => VNode;
export function Fragment(
data: { key?: Key } | null,
...children: JsxVNodeChildren[]
): VNode {
const flatChildren = flattenAndFilter(children, []);
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 vnode(
undefined,
undefined,
undefined,
flatChildren[0].text,
undefined
);
} else {
return vnode(undefined, data ?? {}, flatChildren, undefined, undefined);
}
}
function flattenAndFilter(
children: JsxVNodeChildren[],
flattened: VNode[]

@ -3,7 +3,8 @@
"compilerOptions": {
"esModuleInterop": true,
"jsx": "react",
"jsxFactory": "jsx"
"jsxFactory": "jsx",
"jsxFragmentFactory": "Fragment"
},
"include": ["./**/*.ts", "./unit/*.tsx", "../src/index.ts"]
}

@ -1,5 +1,5 @@
import { assert } from "chai";
import { jsx } from "../../src/index";
import { jsx, Fragment, init } from "../../src/index";
describe("snabbdom", function () {
describe("jsx", function () {
@ -272,4 +272,328 @@ describe("snabbdom", function () {
});
});
});
describe("Fragment", function () {
it("can be used as a jsxFragmentFactory method", function () {
const vnode = <>Hello World</>;
assert.deepStrictEqual(vnode, {
sel: undefined,
data: undefined,
children: undefined,
elm: undefined,
text: "Hello World",
key: undefined,
});
});
it("creates text property for text only child", function () {
const vnode = <>foo bar</>;
assert.deepStrictEqual(vnode, {
sel: undefined,
data: undefined,
children: undefined,
elm: undefined,
text: "foo bar",
key: undefined,
});
});
it("creates an array of children for multiple children", function () {
const vnode = (
<>
{"foo"}
{"bar"}
</>
);
assert.deepStrictEqual(vnode, {
sel: undefined,
data: {},
children: [
{
sel: undefined,
data: undefined,
children: undefined,
elm: undefined,
text: "foo",
key: undefined,
},
{
sel: undefined,
data: undefined,
children: undefined,
elm: undefined,
text: "bar",
key: undefined,
},
],
elm: undefined,
text: undefined,
key: undefined,
});
});
it("flattens children", function () {
const vnode = (
<>
<h1>A Heading</h1>
some description
{["part1", "part2"].map((part) => (
<span>{part}</span>
))}
</>
);
assert.deepStrictEqual(vnode, {
sel: undefined,
data: {},
children: [
{
sel: "h1",
data: {},
children: undefined,
elm: undefined,
text: "A Heading",
key: undefined,
},
{
sel: undefined,
data: undefined,
children: undefined,
elm: undefined,
text: "some description",
key: undefined,
},
{
sel: "span",
data: {},
children: undefined,
elm: undefined,
text: "part1",
key: undefined,
},
{
sel: "span",
data: {},
children: undefined,
elm: undefined,
text: "part2",
key: undefined,
},
],
elm: undefined,
text: undefined,
key: undefined,
});
});
it("removes falsey children", function () {
const showLogin = false;
const showCaptcha = false;
const loginAttempts = 0;
const userName = "";
const profilePic = undefined;
const isLoggedIn = true;
const vnode = (
<>
Login Form
{showLogin && <login-form />}
{showCaptcha ? <captcha-form /> : null}
{userName}
{profilePic}
Login Attempts: {loginAttempts}
Logged In: {isLoggedIn}
</>
);
assert.deepStrictEqual(vnode, {
sel: undefined,
data: {},
children: [
{
sel: undefined,
data: undefined,
children: undefined,
elm: undefined,
text: "Login Form",
key: undefined,
},
{
sel: undefined,
data: undefined,
children: undefined,
elm: undefined,
text: "Login Attempts: ",
key: undefined,
},
{
sel: undefined,
data: undefined,
children: undefined,
elm: undefined,
text: "0",
key: undefined,
},
{
sel: undefined,
data: undefined,
children: undefined,
elm: undefined,
text: "Logged In: ",
key: undefined,
},
{
sel: undefined,
data: undefined,
children: undefined,
elm: undefined,
text: "true",
key: undefined,
},
],
elm: undefined,
text: undefined,
key: undefined,
});
});
it("works with a function component", function () {
// workaround linter issue
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const Part = ({ part }: { part: string }) => <>{part}</>;
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.deepStrictEqual(vnode, {
sel: "div",
data: {},
children: [
{
sel: "a",
data: { attrs: { href: "https://github.com/snabbdom/snabbdom" } },
children: undefined,
elm: undefined,
text: "Snabbdom",
key: undefined,
},
{
sel: undefined,
data: undefined,
children: undefined,
elm: undefined,
text: "and tsx",
key: undefined,
},
{
sel: undefined,
data: undefined,
children: undefined,
elm: undefined,
text: "work",
key: undefined,
},
{
sel: undefined,
data: undefined,
children: undefined,
elm: undefined,
text: "like",
key: undefined,
},
{
sel: undefined,
data: undefined,
children: undefined,
elm: undefined,
text: "a",
key: undefined,
},
{
sel: undefined,
data: undefined,
children: undefined,
elm: undefined,
text: "charm!",
key: undefined,
},
{
sel: undefined,
data: undefined,
children: undefined,
elm: undefined,
text: "💃🕺🎉",
key: undefined,
},
],
elm: undefined,
text: undefined,
key: undefined,
});
});
it("can correctly be patched", function () {
const patch = init([], undefined, {
experimental: {
fragments: true,
},
});
const container = document.createElement("div");
const vnode0 = (
<>
<span>Nested </span>
<>
children
<> will be flattened</>
</>
</>
);
patch(container, vnode0);
assert.strictEqual(vnode0.elm?.nodeType, document.DOCUMENT_FRAGMENT_NODE);
assert.strictEqual(
vnode0.elm?.textContent,
"Nested children will be flattened"
);
const vnode1 = (
<div>
<span>Nested </span>
<>
child nodes
{[" will", " be", " patched"]}
</>
</div>
);
patch(vnode0, vnode1);
assert.strictEqual((vnode1.elm as HTMLElement).tagName, "DIV");
assert.strictEqual(
vnode1.elm?.textContent,
"Nested child nodes will be patched"
);
const vnode2 = (
<Fragment key="foo">
And <>fragment again!</>
</Fragment>
);
patch(vnode1, vnode2);
assert.strictEqual(vnode2.elm?.nodeType, document.DOCUMENT_FRAGMENT_NODE);
assert.strictEqual(vnode2.elm?.textContent, "And fragment again!");
});
});
});

Loading…
Cancel
Save