Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds a "pass thru" virtual element #29

Merged
merged 18 commits into from
Dec 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5e30677
added `hpass` and `VirtualElementPass`
telamonian Oct 9, 2019
e4fcab9
hpass vdom elements are now functional; hpass was tested and is working
telamonian Oct 12, 2019
8c0087c
added `Title.iconPass` prop; takes a `{render: func(host)}` obj
telamonian Oct 12, 2019
6871093
cleaned up `updateContent`/reduced the need for 'passthru' special ha…
telamonian Oct 12, 2019
6206887
simplified createDOMNode
telamonian Oct 12, 2019
7245930
more simplification of createDOMNode
telamonian Oct 14, 2019
7e61d10
made implementation of hpass more robust; now prevents sibling clobbe…
telamonian Oct 14, 2019
eca800e
taking a stab at allowing recursive cleanup for hpass vdom elements
telamonian Oct 14, 2019
b5b3fe0
improved removeContent by skipping unneeded removeChild calls
telamonian Oct 15, 2019
bb6e456
added hpass/VirtualElementPass unittests
telamonian Oct 15, 2019
ef053b9
all new unittests for hpass/VirtualElement Pass now complete successf…
telamonian Oct 15, 2019
25844a2
fixed all typing/build errors in unittests
telamonian Oct 15, 2019
2773ad7
added docstrings to new stuff in virtualdom
telamonian Oct 15, 2019
0672d6d
added docstrings to the new iconRenderer stuff in Title
telamonian Oct 15, 2019
9922f3a
added test:debug scripts
telamonian Dec 14, 2019
e887aea
post-rebase cleanup
telamonian Dec 14, 2019
000a313
fixed syntax/semantics of hpass() to more closely resemble h()
telamonian Dec 14, 2019
a3336e6
fix bug in tabbar caused by updated hpass() syntax
telamonian Dec 14, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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