Skip to content

Commit

Permalink
Merge pull request #29 from telamonian/vdom-pass-thru
Browse files Browse the repository at this point in the history
Adds a "pass thru" virtual element
  • Loading branch information
Steven Silvester authored Dec 17, 2019
2 parents aea17f4 + a3336e6 commit 0e115eb
Show file tree
Hide file tree
Showing 11 changed files with 419 additions and 50 deletions.
3 changes: 3 additions & 0 deletions packages/virtualdom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
"docs": "typedoc --options tdoptions.json src",
"test": "npm run test:firefox",
"test:chrome": "cd tests && karma start --browsers=Chrome",
"test:chrome-headless": "cd tests && karma start --browsers=ChromeHeadless",
"test:debug": "cd tests && karma start --browsers=Chrome --singleRun=false --debug=true --browserNoActivityTimeout=10000000 --browserDisconnectTimeout=10000000",
"test:debug:chrome-headless": "cd tests && karma start --browsers=ChromeHeadless --singleRun=false --debug=true --browserNoActivityTimeout=10000000 --browserDisconnectTimeout=10000000",
"test:firefox": "cd tests && karma start --browsers=Firefox",
"test:ie": "cd tests && karma start --browsers=IE",
"watch": "tsc --build --watch"
Expand Down
221 changes: 182 additions & 39 deletions packages/virtualdom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -750,11 +750,75 @@ class VirtualElement {
}


/**
* A "pass thru" virtual node whose children are managed by a render and an
* unrender callback. The intent of this flavor of virtual node is to make
* it easy to blend other kinds of virtualdom (eg React) into Phosphor's
* virtualdom.
*
* #### Notes
* User code will not typically create a `VirtualElementPass` node directly.
* Instead, the `hpass()` function will be used to create an element tree.
*/
export
class VirtualElementPass{
/**
* The type of the node.
*
* This value can be used as a type guard for discriminating the
* `VirtualNode` union type.
*/
readonly type: 'passthru' = 'passthru';

/**
* Construct a new virtual element pass thru node.
*
* @param tag - the tag of the parent element of this node. Once the parent
* element is rendered, it will be passed as an argument to
* renderer.render
*
* @param attrs - attributes that will assigned to the
* parent element
*
* @param renderer - an object with render and unrender
* functions, each of which should take a single argument of type
* HTMLElement and return nothing. If null, the parent element
* will be rendered barren without any children.
*/
constructor(readonly tag: string, readonly attrs: ElementAttrs, readonly renderer: VirtualElementPass.IRenderer | null) {}

render(host: HTMLElement): void {
// skip actual render if renderer is null
if (this.renderer) {
this.renderer.render(host);
}
}

unrender(host: HTMLElement): void {
// skip actual unrender if renderer is null
if (this.renderer) {
this.renderer.unrender(host);
}
}
}


/**
* The namespace for the VirtualElementPass class statics.
*/
export namespace VirtualElementPass {
export type IRenderer = {
render: (host: HTMLElement) => void,
unrender: (host: HTMLElement) => void
};
}


/**
* A type alias for a general virtual node.
*/
export
type VirtualNode = VirtualElement | VirtualText;
type VirtualNode = VirtualElement | VirtualElementPass | VirtualText;


/**
Expand Down Expand Up @@ -792,6 +856,8 @@ export function h(tag: string): VirtualElement {
children.push(arg);
} else if (arg instanceof VirtualElement) {
children.push(arg);
} else if (arg instanceof VirtualElementPass) {
children.push(arg);
} else if (arg instanceof Array) {
extend(children, arg);
} else if (i === 1 && arg && typeof arg === 'object') {
Expand All @@ -808,6 +874,8 @@ export function h(tag: string): VirtualElement {
array.push(child);
} else if (child instanceof VirtualElement) {
array.push(child);
} else if (child instanceof VirtualElementPass) {
array.push(child);
}
}
}
Expand Down Expand Up @@ -934,6 +1002,41 @@ namespace h {
}


/**
* Create a new "pass thru" virtual element node.
*
* @param tag - The tag name for the parent element.
*
* @param attrs - The attributes for the parent element, if any.
*
* @param renderer - an object with render and unrender functions, if any.
*
* @returns A new "pass thru" virtual element node for the given parameters.
*
*/
export function hpass(tag: string, renderer?: VirtualElementPass.IRenderer): VirtualElementPass;
export function hpass(tag: string, attrs: ElementAttrs, renderer?: VirtualElementPass.IRenderer): VirtualElementPass;
export function hpass(tag: string): VirtualElementPass {
let attrs: ElementAttrs = {};
let renderer: VirtualElementPass.IRenderer | null = null;

if (arguments.length === 2) {
const arg = arguments[1];

if ("render" in arg && "unrender" in arg) {
renderer = arg;
} else {
attrs = arg;
}
} else if (arguments.length === 3) {
attrs = arguments[1];
renderer = arguments[2];
}

return new VirtualElementPass(tag, attrs, renderer);
}


/**
* The namespace for the virtual DOM rendering functions.
*/
Expand All @@ -952,8 +1055,10 @@ namespace VirtualDOM {
*
* If virtual diffing is desired, use the `render` function instead.
*/
export
function realize(node: VirtualElement): HTMLElement {
export function realize(node: VirtualText): Text;
export function realize(node: VirtualElement): HTMLElement;
export function realize(node: VirtualElementPass): HTMLElement;
export function realize(node: VirtualNode): HTMLElement | Text {
return Private.createDOMNode(node);
}

Expand Down Expand Up @@ -990,14 +1095,12 @@ namespace Private {
/**
* A weak mapping of host element to virtual DOM content.
*/
export
const hostMap = new WeakMap<HTMLElement, ReadonlyArray<VirtualNode>>();
export const hostMap = new WeakMap<HTMLElement, ReadonlyArray<VirtualNode>>();

/**
* Cast a content value to a content array.
*/
export
function asContentArray(value: VirtualNode | ReadonlyArray<VirtualNode> | null): ReadonlyArray<VirtualNode> {
export function asContentArray(value: VirtualNode | ReadonlyArray<VirtualNode> | null): ReadonlyArray<VirtualNode> {
if (!value) {
return [];
}
Expand All @@ -1010,32 +1113,42 @@ namespace Private {
/**
* Create a new DOM element for a virtual node.
*/
export
function createDOMNode(node: VirtualText): Text;
export
function createDOMNode(node: VirtualElement): HTMLElement;
export
function createDOMNode(node: VirtualNode): HTMLElement | Text;
export
function createDOMNode(node: VirtualNode): HTMLElement | Text {
// Create a text node for a virtual text node.
if (node.type === 'text') {
return document.createTextNode(node.content);
}
export function createDOMNode(node: VirtualText): Text;
export function createDOMNode(node: VirtualElement): HTMLElement;
export function createDOMNode(node: VirtualElementPass): HTMLElement;
export function createDOMNode(node: VirtualNode): HTMLElement | Text;
export function createDOMNode(node: VirtualNode, host: HTMLElement | null): HTMLElement | Text;
export function createDOMNode(node: VirtualNode, host: HTMLElement | null, before: Node | null): HTMLElement | Text;
export function createDOMNode(node: VirtualNode): HTMLElement | Text {
let host = arguments[1] || null;
const before = arguments[2] || null;

if (host) {
host.insertBefore(createDOMNode(node), before);
} else {
// Create a text node for a virtual text node.
if (node.type === 'text') {
return document.createTextNode(node.content);
}

// Create the HTML element with the specified tag.
let element = document.createElement(node.tag);
// Create the HTML element with the specified tag.
host = document.createElement(node.tag);

// Add the attributes for the new element.
addAttrs(element, node.attrs);
// Add the attributes for the new element.
addAttrs(host, node.attrs);

// Recursively populate the element with child content.
for (let i = 0, n = node.children.length; i < n; ++i) {
element.appendChild(createDOMNode(node.children[i]));
if (node.type === 'passthru') {
node.render(host);
return host;
}

// Recursively populate the element with child content.
for (let i = 0, n = node.children.length; i < n; ++i) {
createDOMNode(node.children[i], host);
}
}

// Return the populated element.
return element;
return host;
}

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

// If the old content is exhausted, create a new node.
if (i >= oldCopy.length) {
host.appendChild(createDOMNode(newContent[i]));
createDOMNode(newContent[i], host);
continue;
}

Expand All @@ -1089,11 +1201,19 @@ namespace Private {
continue;
}

// If the old or new node is a text node, the other node is now
// known to be an element node, so create and insert a new node.
if (oldVNode.type === 'text' || newVNode.type === 'text') {
// Handle the case of passthru update.
if (oldVNode.type === 'passthru' && newVNode.type === 'passthru') {
newVNode.render(currElem as HTMLElement);
currElem = currElem!.nextSibling;
continue;
}

// If the types of the old and new nodes differ,
// create and insert a new node.
if (oldVNode.type === 'text' || newVNode.type === 'text' ||
oldVNode.type === 'passthru' || newVNode.type === 'passthru') {
ArrayExt.insert(oldCopy, i, newVNode);
host.insertBefore(createDOMNode(newVNode), currElem);
createDOMNode(newVNode, host, currElem);
continue;
}

Expand Down Expand Up @@ -1126,14 +1246,14 @@ namespace Private {
let oldKey = oldVNode.attrs.key;
if (oldKey && oldKey !== newKey) {
ArrayExt.insert(oldCopy, i, newVNode);
host.insertBefore(createDOMNode(newVNode), currElem);
createDOMNode(newVNode, host, currElem);
continue;
}

// If the tags are different, create a new node.
if (oldVNode.tag !== newVNode.tag) {
ArrayExt.insert(oldCopy, i, newVNode);
host.insertBefore(createDOMNode(newVNode), currElem);
createDOMNode(newVNode, host, currElem);
continue;
}

Expand All @@ -1149,9 +1269,32 @@ namespace Private {
currElem = currElem!.nextSibling;
}

// Cleanup stale DOM
removeContent(host, oldCopy, newCount, true);
}

/**
* Handle cleanup of stale vdom and its associated DOM. Stale nodes are
* traversed recursively and any needed explicit cleanup is carried out (
* in particular, the unrender callback of VirtualElementPass nodes). The
* stale children of the top level node are removed using removeChild.
*/
function removeContent(host: HTMLElement, oldContent: ReadonlyArray<VirtualNode>, newCount: number, _sentinel = false) {
// Dispose of the old nodes pushed to the end of the host.
for (let i = oldCopy.length - newCount; i > 0; --i) {
host.removeChild(host.lastChild!);
for (let i = oldContent.length - 1; i >= newCount; --i) {
const oldNode = oldContent[i];
const child = (_sentinel ? host.lastChild : host.childNodes[i]) as HTMLElement;

// recursively clean up host children
if (oldNode.type === 'text') {} else if (oldNode.type === 'passthru') {
oldNode.unrender(child!);
} else {
removeContent(child!, oldNode.children, 0);
}

if (_sentinel) {
host.removeChild(child!);
}
}
}

Expand Down
Loading

0 comments on commit 0e115eb

Please sign in to comment.