feat: support DocumentFragment

Snabbdom now can handle `DocumentFragment` via `fragment` function.
https://developer.mozilla.org/docs/Web/API/DocumentFragment

ISSUES CLOSED: #560
pull/994/head
Ryota Kameoka 4 years ago committed by Jan van Brügge
parent e54012e811
commit 7e86386292
No known key found for this signature in database
GPG Key ID: 88E0BF7B7A546481

@ -86,3 +86,28 @@ export function h(sel: any, b?: any, c?: any): VNode {
}
return vnode(sel, data, children, text, undefined);
}
/**
* @experimental
*/
export function fragment(children: VNodeChildren): VNode {
let c: any;
let text: any;
if (is.array(children)) {
c = children;
} else if (is.primitive(c)) {
text = children;
} else if (c && c.sel) {
c = [children];
}
if (c !== undefined) {
for (let i = 0; i < c.length; ++i) {
if (is.primitive(c[i]))
c[i] = vnode(undefined, undefined, undefined, c[i], undefined);
}
}
return vnode(undefined, {}, c, text, undefined);
}

@ -8,6 +8,11 @@ export interface DOMAPI {
qualifiedName: string,
options?: ElementCreationOptions
) => Element;
/**
* @experimental
* @todo Make it required when the fragment is considered stable.
*/
createDocumentFragment?: () => DocumentFragment;
createTextNode: (text: string) => Text;
createComment: (text: string) => Comment;
insertBefore: (
@ -42,6 +47,10 @@ function createElementNS(
return document.createElementNS(namespaceURI, qualifiedName, options);
}
function createDocumentFragment(): DocumentFragment {
return document.createDocumentFragment();
}
function createTextNode(text: string): Text {
return document.createTextNode(text);
}
@ -102,6 +111,7 @@ export const htmlDomApi: DOMAPI = {
createElement,
createElementNS,
createTextNode,
createDocumentFragment,
createComment,
insertBefore,
removeChild,

@ -14,6 +14,7 @@ export {
ArrayOrElement,
VNodeChildren,
h,
fragment,
} from "./h";
// types

@ -24,8 +24,15 @@ function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
return isSameSel && isSameKey && isSameIs;
}
function isVnode(vnode: any): vnode is VNode {
return vnode.sel !== undefined;
/**
* @todo Remove this function when the document fragment is considered stable.
*/
function documentFragmentIsNotSupported(): never {
throw new Error("The document fragment is not supported on this platform.");
}
function isElement(api: DOMAPI, vnode: Element | VNode): vnode is Element {
return api.isElement(vnode as any);
}
type KeyToIndexMap = { [key: string]: number };
@ -60,7 +67,18 @@ const hooks: Array<keyof Module> = [
"post",
];
export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
// TODO Should `domApi` be put into this in the next major version bump?
type Options = {
experimental?: {
fragments?: boolean;
};
};
export function init(
modules: Array<Partial<Module>>,
domApi?: DOMAPI,
options?: Options
) {
const cbs: ModuleHooks = {
create: [],
update: [],
@ -159,6 +177,21 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
insertedVnodeQueue.push(vnode);
}
}
} else if (options?.experimental?.fragments && vnode.children) {
const children = vnode.children;
vnode.elm = (
api.createDocumentFragment ?? documentFragmentIsNotSupported
)();
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
for (i = 0; i < children.length; ++i) {
const ch = children[i];
if (ch != null) {
api.appendChild(
vnode.elm,
createElm(ch as VNode, insertedVnodeQueue)
);
}
}
} else {
vnode.elm = api.createTextNode(vnode.text!);
}
@ -366,7 +399,7 @@ export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
const insertedVnodeQueue: VNodeQueue = [];
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
if (!isVnode(oldVnode)) {
if (isElement(api, oldVnode)) {
oldVnode = emptyNodeAt(oldVnode);
}

@ -20,11 +20,16 @@ import {
DestroyHook,
UpdateHook,
Key,
fragment,
} from "../../src/index";
const hasSvgClassList = "classList" in SVGElement.prototype;
const patch = init([classModule, propsModule, eventListenersModule]);
const patch = init(
[classModule, propsModule, eventListenersModule],
undefined,
{ experimental: { fragments: true } }
);
function prop<T>(name: string) {
return function (obj: { [index: string]: T }) {
@ -268,6 +273,15 @@ describe("snabbdom", function () {
assert.strictEqual(elm.textContent, "test");
});
});
describe("created document fragment", function () {
it("is an instance of DocumentFragment", function () {
const vnode1 = fragment(["I am", h("span", [" a", " fragment"])]);
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.nodeType, document.DOCUMENT_FRAGMENT_NODE);
assert.strictEqual(elm.textContent, "I am a fragment");
});
});
describe("patching an element", function () {
it("changes the elements classes", function () {
const vnode1 = h("i", { class: { i: true, am: true, horse: true } });
@ -1070,6 +1084,24 @@ describe("snabbdom", function () {
});
});
});
describe("patching a fragment", function () {
it("can patch on document fragments", function () {
const vnode1 = fragment(["I am", h("span", [" a", " fragment"])]);
const vnode2 = h("div", ["I am an element"]);
const vnode3 = fragment(["fragment ", "again"]);
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.nodeType, document.DOCUMENT_FRAGMENT_NODE);
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.tagName, "DIV");
assert.strictEqual(elm.textContent, "I am an element");
elm = patch(vnode2, vnode3).elm;
assert.strictEqual(elm.nodeType, document.DOCUMENT_FRAGMENT_NODE);
assert.strictEqual(elm.textContent, "fragment again");
});
});
describe("hooks", function () {
describe("element hooks", function () {
it("calls `create` listener before inserted into parent but after children", function () {

Loading…
Cancel
Save