Skip to content

Commit 0e115eb

Browse files
author
Steven Silvester
authored
Merge pull request #29 from telamonian/vdom-pass-thru
Adds a "pass thru" virtual element
2 parents aea17f4 + a3336e6 commit 0e115eb

File tree

11 files changed

+419
-50
lines changed

11 files changed

+419
-50
lines changed

packages/virtualdom/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
"docs": "typedoc --options tdoptions.json src",
3737
"test": "npm run test:firefox",
3838
"test:chrome": "cd tests && karma start --browsers=Chrome",
39+
"test:chrome-headless": "cd tests && karma start --browsers=ChromeHeadless",
40+
"test:debug": "cd tests && karma start --browsers=Chrome --singleRun=false --debug=true --browserNoActivityTimeout=10000000 --browserDisconnectTimeout=10000000",
41+
"test:debug:chrome-headless": "cd tests && karma start --browsers=ChromeHeadless --singleRun=false --debug=true --browserNoActivityTimeout=10000000 --browserDisconnectTimeout=10000000",
3942
"test:firefox": "cd tests && karma start --browsers=Firefox",
4043
"test:ie": "cd tests && karma start --browsers=IE",
4144
"watch": "tsc --build --watch"

packages/virtualdom/src/index.ts

Lines changed: 182 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -750,11 +750,75 @@ class VirtualElement {
750750
}
751751

752752

753+
/**
754+
* A "pass thru" virtual node whose children are managed by a render and an
755+
* unrender callback. The intent of this flavor of virtual node is to make
756+
* it easy to blend other kinds of virtualdom (eg React) into Phosphor's
757+
* virtualdom.
758+
*
759+
* #### Notes
760+
* User code will not typically create a `VirtualElementPass` node directly.
761+
* Instead, the `hpass()` function will be used to create an element tree.
762+
*/
763+
export
764+
class VirtualElementPass{
765+
/**
766+
* The type of the node.
767+
*
768+
* This value can be used as a type guard for discriminating the
769+
* `VirtualNode` union type.
770+
*/
771+
readonly type: 'passthru' = 'passthru';
772+
773+
/**
774+
* Construct a new virtual element pass thru node.
775+
*
776+
* @param tag - the tag of the parent element of this node. Once the parent
777+
* element is rendered, it will be passed as an argument to
778+
* renderer.render
779+
*
780+
* @param attrs - attributes that will assigned to the
781+
* parent element
782+
*
783+
* @param renderer - an object with render and unrender
784+
* functions, each of which should take a single argument of type
785+
* HTMLElement and return nothing. If null, the parent element
786+
* will be rendered barren without any children.
787+
*/
788+
constructor(readonly tag: string, readonly attrs: ElementAttrs, readonly renderer: VirtualElementPass.IRenderer | null) {}
789+
790+
render(host: HTMLElement): void {
791+
// skip actual render if renderer is null
792+
if (this.renderer) {
793+
this.renderer.render(host);
794+
}
795+
}
796+
797+
unrender(host: HTMLElement): void {
798+
// skip actual unrender if renderer is null
799+
if (this.renderer) {
800+
this.renderer.unrender(host);
801+
}
802+
}
803+
}
804+
805+
806+
/**
807+
* The namespace for the VirtualElementPass class statics.
808+
*/
809+
export namespace VirtualElementPass {
810+
export type IRenderer = {
811+
render: (host: HTMLElement) => void,
812+
unrender: (host: HTMLElement) => void
813+
};
814+
}
815+
816+
753817
/**
754818
* A type alias for a general virtual node.
755819
*/
756820
export
757-
type VirtualNode = VirtualElement | VirtualText;
821+
type VirtualNode = VirtualElement | VirtualElementPass | VirtualText;
758822

759823

760824
/**
@@ -792,6 +856,8 @@ export function h(tag: string): VirtualElement {
792856
children.push(arg);
793857
} else if (arg instanceof VirtualElement) {
794858
children.push(arg);
859+
} else if (arg instanceof VirtualElementPass) {
860+
children.push(arg);
795861
} else if (arg instanceof Array) {
796862
extend(children, arg);
797863
} else if (i === 1 && arg && typeof arg === 'object') {
@@ -808,6 +874,8 @@ export function h(tag: string): VirtualElement {
808874
array.push(child);
809875
} else if (child instanceof VirtualElement) {
810876
array.push(child);
877+
} else if (child instanceof VirtualElementPass) {
878+
array.push(child);
811879
}
812880
}
813881
}
@@ -934,6 +1002,41 @@ namespace h {
9341002
}
9351003

9361004

1005+
/**
1006+
* Create a new "pass thru" virtual element node.
1007+
*
1008+
* @param tag - The tag name for the parent element.
1009+
*
1010+
* @param attrs - The attributes for the parent element, if any.
1011+
*
1012+
* @param renderer - an object with render and unrender functions, if any.
1013+
*
1014+
* @returns A new "pass thru" virtual element node for the given parameters.
1015+
*
1016+
*/
1017+
export function hpass(tag: string, renderer?: VirtualElementPass.IRenderer): VirtualElementPass;
1018+
export function hpass(tag: string, attrs: ElementAttrs, renderer?: VirtualElementPass.IRenderer): VirtualElementPass;
1019+
export function hpass(tag: string): VirtualElementPass {
1020+
let attrs: ElementAttrs = {};
1021+
let renderer: VirtualElementPass.IRenderer | null = null;
1022+
1023+
if (arguments.length === 2) {
1024+
const arg = arguments[1];
1025+
1026+
if ("render" in arg && "unrender" in arg) {
1027+
renderer = arg;
1028+
} else {
1029+
attrs = arg;
1030+
}
1031+
} else if (arguments.length === 3) {
1032+
attrs = arguments[1];
1033+
renderer = arguments[2];
1034+
}
1035+
1036+
return new VirtualElementPass(tag, attrs, renderer);
1037+
}
1038+
1039+
9371040
/**
9381041
* The namespace for the virtual DOM rendering functions.
9391042
*/
@@ -952,8 +1055,10 @@ namespace VirtualDOM {
9521055
*
9531056
* If virtual diffing is desired, use the `render` function instead.
9541057
*/
955-
export
956-
function realize(node: VirtualElement): HTMLElement {
1058+
export function realize(node: VirtualText): Text;
1059+
export function realize(node: VirtualElement): HTMLElement;
1060+
export function realize(node: VirtualElementPass): HTMLElement;
1061+
export function realize(node: VirtualNode): HTMLElement | Text {
9571062
return Private.createDOMNode(node);
9581063
}
9591064

@@ -990,14 +1095,12 @@ namespace Private {
9901095
/**
9911096
* A weak mapping of host element to virtual DOM content.
9921097
*/
993-
export
994-
const hostMap = new WeakMap<HTMLElement, ReadonlyArray<VirtualNode>>();
1098+
export const hostMap = new WeakMap<HTMLElement, ReadonlyArray<VirtualNode>>();
9951099

9961100
/**
9971101
* Cast a content value to a content array.
9981102
*/
999-
export
1000-
function asContentArray(value: VirtualNode | ReadonlyArray<VirtualNode> | null): ReadonlyArray<VirtualNode> {
1103+
export function asContentArray(value: VirtualNode | ReadonlyArray<VirtualNode> | null): ReadonlyArray<VirtualNode> {
10011104
if (!value) {
10021105
return [];
10031106
}
@@ -1010,32 +1113,42 @@ namespace Private {
10101113
/**
10111114
* Create a new DOM element for a virtual node.
10121115
*/
1013-
export
1014-
function createDOMNode(node: VirtualText): Text;
1015-
export
1016-
function createDOMNode(node: VirtualElement): HTMLElement;
1017-
export
1018-
function createDOMNode(node: VirtualNode): HTMLElement | Text;
1019-
export
1020-
function createDOMNode(node: VirtualNode): HTMLElement | Text {
1021-
// Create a text node for a virtual text node.
1022-
if (node.type === 'text') {
1023-
return document.createTextNode(node.content);
1024-
}
1116+
export function createDOMNode(node: VirtualText): Text;
1117+
export function createDOMNode(node: VirtualElement): HTMLElement;
1118+
export function createDOMNode(node: VirtualElementPass): HTMLElement;
1119+
export function createDOMNode(node: VirtualNode): HTMLElement | Text;
1120+
export function createDOMNode(node: VirtualNode, host: HTMLElement | null): HTMLElement | Text;
1121+
export function createDOMNode(node: VirtualNode, host: HTMLElement | null, before: Node | null): HTMLElement | Text;
1122+
export function createDOMNode(node: VirtualNode): HTMLElement | Text {
1123+
let host = arguments[1] || null;
1124+
const before = arguments[2] || null;
1125+
1126+
if (host) {
1127+
host.insertBefore(createDOMNode(node), before);
1128+
} else {
1129+
// Create a text node for a virtual text node.
1130+
if (node.type === 'text') {
1131+
return document.createTextNode(node.content);
1132+
}
10251133

1026-
// Create the HTML element with the specified tag.
1027-
let element = document.createElement(node.tag);
1134+
// Create the HTML element with the specified tag.
1135+
host = document.createElement(node.tag);
10281136

1029-
// Add the attributes for the new element.
1030-
addAttrs(element, node.attrs);
1137+
// Add the attributes for the new element.
1138+
addAttrs(host, node.attrs);
10311139

1032-
// Recursively populate the element with child content.
1033-
for (let i = 0, n = node.children.length; i < n; ++i) {
1034-
element.appendChild(createDOMNode(node.children[i]));
1140+
if (node.type === 'passthru') {
1141+
node.render(host);
1142+
return host;
1143+
}
1144+
1145+
// Recursively populate the element with child content.
1146+
for (let i = 0, n = node.children.length; i < n; ++i) {
1147+
createDOMNode(node.children[i], host);
1148+
}
10351149
}
10361150

1037-
// Return the populated element.
1038-
return element;
1151+
return host;
10391152
}
10401153

10411154
/**
@@ -1044,8 +1157,7 @@ namespace Private {
10441157
* This is the core "diff" algorithm. There is no explicit "patch"
10451158
* phase. The host is patched at each step as the diff progresses.
10461159
*/
1047-
export
1048-
function updateContent(host: HTMLElement, oldContent: ReadonlyArray<VirtualNode>, newContent: ReadonlyArray<VirtualNode>): void {
1160+
export function updateContent(host: HTMLElement, oldContent: ReadonlyArray<VirtualNode>, newContent: ReadonlyArray<VirtualNode>): void {
10491161
// Bail early if the content is identical.
10501162
if (oldContent === newContent) {
10511163
return;
@@ -1068,7 +1180,7 @@ namespace Private {
10681180

10691181
// If the old content is exhausted, create a new node.
10701182
if (i >= oldCopy.length) {
1071-
host.appendChild(createDOMNode(newContent[i]));
1183+
createDOMNode(newContent[i], host);
10721184
continue;
10731185
}
10741186

@@ -1089,11 +1201,19 @@ namespace Private {
10891201
continue;
10901202
}
10911203

1092-
// If the old or new node is a text node, the other node is now
1093-
// known to be an element node, so create and insert a new node.
1094-
if (oldVNode.type === 'text' || newVNode.type === 'text') {
1204+
// Handle the case of passthru update.
1205+
if (oldVNode.type === 'passthru' && newVNode.type === 'passthru') {
1206+
newVNode.render(currElem as HTMLElement);
1207+
currElem = currElem!.nextSibling;
1208+
continue;
1209+
}
1210+
1211+
// If the types of the old and new nodes differ,
1212+
// create and insert a new node.
1213+
if (oldVNode.type === 'text' || newVNode.type === 'text' ||
1214+
oldVNode.type === 'passthru' || newVNode.type === 'passthru') {
10951215
ArrayExt.insert(oldCopy, i, newVNode);
1096-
host.insertBefore(createDOMNode(newVNode), currElem);
1216+
createDOMNode(newVNode, host, currElem);
10971217
continue;
10981218
}
10991219

@@ -1126,14 +1246,14 @@ namespace Private {
11261246
let oldKey = oldVNode.attrs.key;
11271247
if (oldKey && oldKey !== newKey) {
11281248
ArrayExt.insert(oldCopy, i, newVNode);
1129-
host.insertBefore(createDOMNode(newVNode), currElem);
1249+
createDOMNode(newVNode, host, currElem);
11301250
continue;
11311251
}
11321252

11331253
// If the tags are different, create a new node.
11341254
if (oldVNode.tag !== newVNode.tag) {
11351255
ArrayExt.insert(oldCopy, i, newVNode);
1136-
host.insertBefore(createDOMNode(newVNode), currElem);
1256+
createDOMNode(newVNode, host, currElem);
11371257
continue;
11381258
}
11391259

@@ -1149,9 +1269,32 @@ namespace Private {
11491269
currElem = currElem!.nextSibling;
11501270
}
11511271

1272+
// Cleanup stale DOM
1273+
removeContent(host, oldCopy, newCount, true);
1274+
}
1275+
1276+
/**
1277+
* Handle cleanup of stale vdom and its associated DOM. Stale nodes are
1278+
* traversed recursively and any needed explicit cleanup is carried out (
1279+
* in particular, the unrender callback of VirtualElementPass nodes). The
1280+
* stale children of the top level node are removed using removeChild.
1281+
*/
1282+
function removeContent(host: HTMLElement, oldContent: ReadonlyArray<VirtualNode>, newCount: number, _sentinel = false) {
11521283
// Dispose of the old nodes pushed to the end of the host.
1153-
for (let i = oldCopy.length - newCount; i > 0; --i) {
1154-
host.removeChild(host.lastChild!);
1284+
for (let i = oldContent.length - 1; i >= newCount; --i) {
1285+
const oldNode = oldContent[i];
1286+
const child = (_sentinel ? host.lastChild : host.childNodes[i]) as HTMLElement;
1287+
1288+
// recursively clean up host children
1289+
if (oldNode.type === 'text') {} else if (oldNode.type === 'passthru') {
1290+
oldNode.unrender(child!);
1291+
} else {
1292+
removeContent(child!, oldNode.children, 0);
1293+
}
1294+
1295+
if (_sentinel) {
1296+
host.removeChild(child!);
1297+
}
11551298
}
11561299
}
11571300

0 commit comments

Comments
 (0)