incremental-dom
Version:
An in-place virtual DOM library
372 lines (323 loc) • 10.3 kB
text/typescript
/**
* Copyright 2018 The Incremental DOM Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
assert,
assertCloseMatchesOpenTag,
assertInAttributes,
assertInPatch,
assertNotInAttributes,
assertNotInSkip,
setInAttributes
} from "./assertions";
import { updateAttribute } from "./attributes";
import {
getArgsBuilder,
getAttrsBuilder,
close,
open,
text as coreText,
currentElement
} from "./core";
import { DEBUG } from "./global";
import { getData, NodeData } from "./node_data";
import { Key, NameOrCtorDef, Statics } from "./types";
import { createMap, truncateArray } from "./util";
import { calculateDiff } from "./diff";
/**
* The offset in the virtual element declaration where the attributes are
* specified.
*/
const ATTRIBUTES_OFFSET = 3;
/**
* Used to keep track of the previous values when a 2-way diff is necessary.
* This object is reused.
* TODO(sparhamI) Scope this to a patch so you can call patch from an attribute
* update.
*/
const prevAttrsMap = createMap();
/**
* @param element The Element to diff the attrs for.
* @param data The NodeData associated with the Element.
*/
function diffAttrs(element: Element, data: NodeData) {
const attrsBuilder = getAttrsBuilder();
const prevAttrsArr = data.getAttrsArr(attrsBuilder.length);
calculateDiff(prevAttrsArr, attrsBuilder, element, updateAttribute);
truncateArray(attrsBuilder, 0);
}
/**
* Applies the statics. When importing an Element, any existing attributes that
* match a static are converted into a static attribute.
* @param node The Element to apply statics for.
* @param data The NodeData associated with the Element.
* @param statics The statics array.
*/
function diffStatics(node: Element, data: NodeData, statics: Statics) {
if (data.staticsApplied) {
return;
}
data.staticsApplied = true;
if (!statics || !statics.length) {
return;
}
if (data.hasEmptyAttrsArr()) {
for (let i = 0; i < statics.length; i += 2) {
updateAttribute(node, statics[i] as string, statics[i + 1]);
}
return;
}
for (let i = 0; i < statics.length; i += 2) {
prevAttrsMap[statics[i] as string] = i + 1;
}
const attrsArr = data.getAttrsArr(0);
let j = 0;
for (let i = 0; i < attrsArr.length; i += 2) {
const name = attrsArr[i];
const value = attrsArr[i + 1];
const staticsIndex = prevAttrsMap[name];
if (staticsIndex) {
// For any attrs that are static and have the same value, make sure we do
// not set them again.
if (statics[staticsIndex] === value) {
delete prevAttrsMap[name];
}
continue;
}
// For any attrs that are dynamic, move them up to the right place.
attrsArr[j] = name;
attrsArr[j + 1] = value;
j += 2;
}
// Anything after `j` was either moved up already or static.
truncateArray(attrsArr, j);
for (const name in prevAttrsMap) {
updateAttribute(node, name, statics[prevAttrsMap[name]]);
delete prevAttrsMap[name];
}
}
/**
* Declares a virtual Element at the current location in the document. This
* corresponds to an opening tag and a elementClose tag is required. This is
* like elementOpen, but the attributes are defined using the attr function
* rather than being passed as arguments. Must be folllowed by 0 or more calls
* to attr, then a call to elementOpenEnd.
* @param nameOrCtor The Element's tag or constructor.
* @param key The key used to identify this element. This can be an
* empty string, but performance may be better if a unique value is used
* when iterating over an array of items.
* @param statics An array of attribute name/value pairs of the static
* attributes for the Element. Attributes will only be set once when the
* Element is created.
*/
function elementOpenStart(
nameOrCtor: NameOrCtorDef,
key?: Key,
statics?: Statics
) {
const argsBuilder = getArgsBuilder();
if (DEBUG) {
assertNotInAttributes("elementOpenStart");
setInAttributes(true);
}
argsBuilder[0] = nameOrCtor;
argsBuilder[1] = key;
argsBuilder[2] = statics;
}
/**
* Allows you to define a key after an elementOpenStart. This is useful in
* templates that define key after an element has been opened ie
* `<div key('foo')></div>`.
* @param key The key to use for the next call.
*/
function key(key: string) {
const argsBuilder = getArgsBuilder();
if (DEBUG) {
assertInAttributes("key");
assert(argsBuilder);
}
argsBuilder[1] = key;
}
/**
* Buffers an attribute, which will get applied during the next call to
* `elementOpen`, `elementOpenEnd` or `applyAttrs`.
* @param name The of the attribute to buffer.
* @param value The value of the attribute to buffer.
*/
function attr(name: string, value: any) {
const attrsBuilder = getAttrsBuilder();
if (DEBUG) {
assertInPatch("attr");
}
attrsBuilder.push(name);
attrsBuilder.push(value);
}
/**
* Closes an open tag started with elementOpenStart.
* @return The corresponding Element.
*/
function elementOpenEnd(): HTMLElement {
const argsBuilder = getArgsBuilder();
if (DEBUG) {
assertInAttributes("elementOpenEnd");
setInAttributes(false);
}
const node = open(<NameOrCtorDef>argsBuilder[0], <Key>argsBuilder[1]);
const data = getData(node);
diffStatics(node, data, <Statics>argsBuilder[2]);
diffAttrs(node, data);
truncateArray(argsBuilder, 0);
return node;
}
/**
* @param nameOrCtor The Element's tag or constructor.
* @param key The key used to identify this element. This can be an
* empty string, but performance may be better if a unique value is used
* when iterating over an array of items.
* @param statics An array of attribute name/value pairs of the static
* attributes for the Element. Attributes will only be set once when the
* Element is created.
* @param varArgs, Attribute name/value pairs of the dynamic attributes
* for the Element.
* @return The corresponding Element.
*/
function elementOpen(
nameOrCtor: NameOrCtorDef,
key?: Key,
// Ideally we could tag statics and varArgs as an array where every odd
// element is a string and every even element is any, but this is hard.
statics?: Statics,
...varArgs: Array<any>
) {
if (DEBUG) {
assertNotInAttributes("elementOpen");
assertNotInSkip("elementOpen");
}
elementOpenStart(nameOrCtor, key, statics);
for (let i = ATTRIBUTES_OFFSET; i < arguments.length; i += 2) {
attr(arguments[i], arguments[i + 1]);
}
return elementOpenEnd();
}
/**
* Applies the currently buffered attrs to the currently open element. This
* clears the buffered attributes.
*/
function applyAttrs() {
const node = currentElement();
const data = getData(node);
diffAttrs(node, data);
}
/**
* Applies the current static attributes to the currently open element. Note:
* statics should be applied before calling `applyAtrs`.
* @param statics The statics to apply to the current element.
*/
function applyStatics(statics: Statics) {
const node = currentElement();
const data = getData(node);
diffStatics(node, data, statics);
}
/**
* Closes an open virtual Element.
*
* @param nameOrCtor The Element's tag or constructor.
* @return The corresponding Element.
*/
function elementClose(nameOrCtor: NameOrCtorDef): Element {
if (DEBUG) {
assertNotInAttributes("elementClose");
}
const node = close();
if (DEBUG) {
assertCloseMatchesOpenTag(getData(node).nameOrCtor, nameOrCtor);
}
return node;
}
/**
* Declares a virtual Element at the current location in the document that has
* no children.
* @param nameOrCtor The Element's tag or constructor.
* @param key The key used to identify this element. This can be an
* empty string, but performance may be better if a unique value is used
* when iterating over an array of items.
* @param statics An array of attribute name/value pairs of the static
* attributes for the Element. Attributes will only be set once when the
* Element is created.
* @param varArgs Attribute name/value pairs of the dynamic attributes
* for the Element.
* @return The corresponding Element.
*/
function elementVoid(
nameOrCtor: NameOrCtorDef,
key?: Key,
// Ideally we could tag statics and varArgs as an array where every odd
// element is a string and every even element is any, but this is hard.
statics?: Statics,
...varArgs: Array<any>
) {
elementOpen.apply(null, arguments as any);
return elementClose(nameOrCtor);
}
/**
* Declares a virtual Text at this point in the document.
*
* @param value The value of the Text.
* @param varArgs
* Functions to format the value which are called only when the value has
* changed.
* @return The corresponding text node.
*/
function text(
value: string | number | boolean,
...varArgs: Array<(a: {}) => string>
) {
if (DEBUG) {
assertNotInAttributes("text");
assertNotInSkip("text");
}
const node = coreText();
const data = getData(node);
if (data.text !== value) {
data.text = value as string;
let formatted = value;
for (let i = 1; i < arguments.length; i += 1) {
/*
* Call the formatter function directly to prevent leaking arguments.
* https://github.com/google/incremental-dom/pull/204#issuecomment-178223574
*/
const fn = arguments[i];
formatted = fn(formatted);
}
// Setting node.data resets the cursor in IE/Edge.
if (node.data !== formatted) {
node.data = formatted as string;
}
}
return node;
}
/** */
export {
applyAttrs,
applyStatics,
elementOpenStart,
elementOpenEnd,
elementOpen,
elementVoid,
elementClose,
text,
attr,
key
};