incremental-dom
Version:
An in-place virtual DOM library
268 lines (245 loc) • 7.09 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 { DEBUG } from "./global";
import { NameOrCtorDef } from "./types";
/**
* Keeps track whether or not we are in an attributes declaration (after
* elementOpenStart, but before elementOpenEnd).
*/
let inAttributes = false;
/**
* Keeps track whether or not we are in an element that should not have its
* children cleared.
*/
let inSkip = false;
/**
* Keeps track of whether or not we are in a patch.
*/
let inPatch = false;
/**
* Asserts that a value exists and is not null or undefined. goog.asserts
* is not used in order to avoid dependencies on external code.
* @param val The value to assert is truthy.
* @returns The value.
*/
function assert<T extends {}>(val: T | null | undefined): T {
if (DEBUG && !val) {
throw new Error("Expected value to be defined");
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return val!;
}
/**
* Makes sure that there is a current patch context.
* @param functionName The name of the caller, for the error message.
*/
function assertInPatch(functionName: string) {
if (!inPatch) {
throw new Error("Cannot call " + functionName + "() unless in patch.");
}
}
/**
* Makes sure that a patch closes every node that it opened.
* @param openElement
* @param root
*/
function assertNoUnclosedTags(
openElement: Node | null,
root: Node | DocumentFragment
) {
if (openElement === root) {
return;
}
let currentElement = openElement;
const openTags: Array<string> = [];
while (currentElement && currentElement !== root) {
openTags.push(currentElement.nodeName.toLowerCase());
currentElement = currentElement.parentNode;
}
throw new Error("One or more tags were not closed:\n" + openTags.join("\n"));
}
/**
* Makes sure that node being outer patched has a parent node.
* @param parent
*/
function assertPatchOuterHasParentNode(parent: Node | null) {
if (!parent) {
console.warn(
"patchOuter requires the node have a parent if there is a key."
);
}
}
/**
* Makes sure that the caller is not where attributes are expected.
* @param functionName The name of the caller, for the error message.
*/
function assertNotInAttributes(functionName: string) {
if (inAttributes) {
throw new Error(
functionName +
"() can not be called between " +
"elementOpenStart() and elementOpenEnd()."
);
}
}
/**
* Makes sure that the caller is not inside an element that has declared skip.
* @param functionName The name of the caller, for the error message.
*/
function assertNotInSkip(functionName: string) {
if (inSkip) {
throw new Error(
functionName +
"() may not be called inside an element " +
"that has called skip()."
);
}
}
/**
* Makes sure that the caller is where attributes are expected.
* @param functionName The name of the caller, for the error message.
*/
function assertInAttributes(functionName: string) {
if (!inAttributes) {
throw new Error(
functionName +
"() can only be called after calling " +
"elementOpenStart()."
);
}
}
/**
* Makes sure the patch closes virtual attributes call
*/
function assertVirtualAttributesClosed() {
if (inAttributes) {
throw new Error(
"elementOpenEnd() must be called after calling " + "elementOpenStart()."
);
}
}
/**
* Makes sure that tags are correctly nested.
* @param currentNameOrCtor
* @param nameOrCtor
*/
function assertCloseMatchesOpenTag(
currentNameOrCtor: NameOrCtorDef,
nameOrCtor: NameOrCtorDef
) {
if (currentNameOrCtor !== nameOrCtor) {
throw new Error(
'Received a call to close "' +
nameOrCtor +
'" but "' +
currentNameOrCtor +
'" was open.'
);
}
}
/**
* Makes sure that no children elements have been declared yet in the current
* element.
* @param functionName The name of the caller, for the error message.
* @param previousNode
*/
function assertNoChildrenDeclaredYet(
functionName: string,
previousNode: Node | null
) {
if (previousNode !== null) {
throw new Error(
functionName +
"() must come before any child " +
"declarations inside the current element."
);
}
}
/**
* Checks that a call to patchOuter actually patched the element.
* @param maybeStartNode The value for the currentNode when the patch
* started.
* @param maybeCurrentNode The currentNode when the patch finished.
* @param expectedNextNode The Node that is expected to follow the
* currentNode after the patch;
* @param expectedPrevNode The Node that is expected to preceed the
* currentNode after the patch.
*/
function assertPatchElementNoExtras(
maybeStartNode: Node | null,
maybeCurrentNode: Node | null,
expectedNextNode: Node | null,
expectedPrevNode: Node | null
) {
const startNode = assert(maybeStartNode);
const currentNode = assert(maybeCurrentNode);
const wasUpdated =
currentNode.nextSibling === expectedNextNode &&
currentNode.previousSibling === expectedPrevNode;
const wasChanged =
currentNode.nextSibling === startNode.nextSibling &&
currentNode.previousSibling === expectedPrevNode;
const wasRemoved = currentNode === startNode;
if (!wasUpdated && !wasChanged && !wasRemoved) {
throw new Error(
"There must be exactly one top level call corresponding " +
"to the patched element."
);
}
}
/**
* @param newContext The current patch context.
*/
function updatePatchContext(newContext: {} | null) {
inPatch = newContext != null;
}
/**
* Updates the state of being in an attribute declaration.
* @param value Whether or not the patch is in an attribute declaration.
* @return the previous value.
*/
function setInAttributes(value: boolean) {
const previous = inAttributes;
inAttributes = value;
return previous;
}
/**
* Updates the state of being in a skip element.
* @param value Whether or not the patch is skipping the children of a
* parent node.
* @return the previous value.
*/
function setInSkip(value: boolean) {
const previous = inSkip;
inSkip = value;
return previous;
}
export {
assert,
assertInPatch,
assertNoUnclosedTags,
assertNotInAttributes,
assertInAttributes,
assertCloseMatchesOpenTag,
assertVirtualAttributesClosed,
assertNoChildrenDeclaredYet,
assertNotInSkip,
assertPatchElementNoExtras,
assertPatchOuterHasParentNode,
setInAttributes,
setInSkip,
updatePatchContext
};