Skip to content
This repository has been archived by the owner on Nov 6, 2019. It is now read-only.

Adds a "pass thru" virtual element #437

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/virtualdom/package.json
Expand Up @@ -35,6 +35,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
141 changes: 102 additions & 39 deletions packages/virtualdom/src/index.ts
Expand Up @@ -747,12 +747,34 @@ class VirtualElement {
}
}

export
telamonian marked this conversation as resolved.
Show resolved Hide resolved
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 render - a function that takes a host HTMLElement and returns void
*/
constructor(readonly renderer: VirtualElementPass.IRenderer, readonly tag: string, readonly attrs: ElementAttrs) {}
}

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 @@ -790,6 +812,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 @@ -806,6 +830,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 @@ -931,6 +957,9 @@ namespace h {
export const wbr: IFactory = h.bind(undefined, 'wbr');
}

export function hpass(render: VirtualElementPass.IRenderer, tag: string, attrs: ElementAttrs = {}): VirtualElementPass {
return new VirtualElementPass(render, tag, attrs);
}

/**
* The namespace for the virtual DOM rendering functions.
Expand All @@ -950,8 +979,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 @@ -988,14 +1019,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 @@ -1008,32 +1037,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.renderer.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 @@ -1042,8 +1081,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 @@ -1066,7 +1104,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 @@ -1087,11 +1125,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.renderer.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 @@ -1124,14 +1170,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 @@ -1147,9 +1193,26 @@ namespace Private {
currElem = currElem!.nextSibling;
}

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

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.renderer.unrender!(child!);
} else {
removeContent(child!, oldNode.children, 0);
}

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

Expand Down