UNPKG

6.03 kBPlain TextView Raw
1/**
2 * Copyright 2018 The Incremental DOM Authors. All Rights Reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS-IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import { Key, NameOrCtorDef } from "./types";
18import { assert } from "./assertions";
19import { createArray } from "./util";
20import { isElement } from "./dom_util";
21import { getKeyAttributeName } from "./global";
22
23declare global {
24 interface Node {
25 __incrementalDOMData: NodeData | null;
26 }
27}
28
29/**
30 * Keeps track of information needed to perform diffs for a given DOM node.
31 */
32export class NodeData {
33 /**
34 * An array of attribute name/value pairs, used for quickly diffing the
35 * incomming attributes to see if the DOM node's attributes need to be
36 * updated.
37 */
38 private _attrsArr: Array<any> | null = null;
39
40 /**
41 * Whether or not the statics have been applied for the node yet.
42 */
43 public staticsApplied = false;
44
45 /**
46 * The key used to identify this node, used to preserve DOM nodes when they
47 * move within their parent.
48 */
49 public readonly key: Key;
50
51 /**
52 * The previous text value, for Text nodes.
53 */
54 public text: string | undefined;
55
56 /**
57 * The nodeName or contructor for the Node.
58 */
59 public readonly nameOrCtor: NameOrCtorDef;
60
61 public constructor(
62 nameOrCtor: NameOrCtorDef,
63 key: Key,
64 text: string | undefined
65 ) {
66 this.nameOrCtor = nameOrCtor;
67 this.key = key;
68 this.text = text;
69 }
70
71 public hasEmptyAttrsArr(): boolean {
72 const attrs = this._attrsArr;
73 return !attrs || !attrs.length;
74 }
75
76 public getAttrsArr(length: number): Array<any> {
77 return this._attrsArr || (this._attrsArr = createArray(length));
78 }
79}
80
81/**
82 * Initializes a NodeData object for a Node.
83 * @param node The Node to initialized data for.
84 * @param nameOrCtor The NameOrCtorDef to use when diffing.
85 * @param key The Key for the Node.
86 * @param text The data of a Text node, if importing a Text node.
87 * @returns A NodeData object with the existing attributes initialized.
88 */
89function initData(
90 node: Node,
91 nameOrCtor: NameOrCtorDef,
92 key: Key,
93 text?: string | undefined
94): NodeData {
95 const data = new NodeData(nameOrCtor, key, text);
96 node["__incrementalDOMData"] = data;
97 return data;
98}
99
100/**
101 * @param node The node to check.
102 * @returns True if the NodeData already exists, false otherwise.
103 */
104function isDataInitialized(node: Node): boolean {
105 return Boolean(node["__incrementalDOMData"]);
106}
107
108/**
109 * Records the element's attributes.
110 * @param node The Element that may have attributes
111 * @param data The Element's data
112 */
113function recordAttributes(node: Element, data: NodeData) {
114 const attributes = node.attributes;
115 const length = attributes.length;
116 if (!length) {
117 return;
118 }
119
120 const attrsArr = data.getAttrsArr(length);
121
122 // Use a cached length. The attributes array is really a live NamedNodeMap,
123 // which exists as a DOM "Host Object" (probably as C++ code). This makes the
124 // usual constant length iteration very difficult to optimize in JITs.
125 for (let i = 0, j = 0; i < length; i += 1, j += 2) {
126 const attr = attributes[i];
127 const name = attr.name;
128 const value = attr.value;
129
130 attrsArr[j] = name;
131 attrsArr[j + 1] = value;
132 }
133}
134
135/**
136 * Imports single node and its subtree, initializing caches, if it has not
137 * already been imported.
138 * @param node The node to import.
139 * @param fallbackKey A key to use if importing and no key was specified.
140 * Useful when not transmitting keys from serverside render and doing an
141 * immediate no-op diff.
142 * @returns The NodeData for the node.
143 */
144function importSingleNode(node: Node, fallbackKey?: Key): NodeData {
145 if (node["__incrementalDOMData"]) {
146 return node["__incrementalDOMData"];
147 }
148
149 const nodeName = isElement(node) ? node.localName : node.nodeName;
150 const keyAttrName = getKeyAttributeName();
151 const keyAttr =
152 isElement(node) && keyAttrName != null
153 ? node.getAttribute(keyAttrName)
154 : null;
155 const key = isElement(node) ? keyAttr || fallbackKey : null;
156 const data = initData(node, nodeName, key);
157
158 if (isElement(node)) {
159 recordAttributes(node, data);
160 }
161
162 return data;
163}
164
165/**
166 * Imports node and its subtree, initializing caches.
167 * @param node The Node to import.
168 */
169function importNode(node: Node) {
170 importSingleNode(node);
171
172 for (
173 let child: Node | null = node.firstChild;
174 child;
175 child = child.nextSibling
176 ) {
177 importNode(child);
178 }
179}
180
181/**
182 * Retrieves the NodeData object for a Node, creating it if necessary.
183 * @param node The node to get data for.
184 * @param fallbackKey A key to use if importing and no key was specified.
185 * Useful when not transmitting keys from serverside render and doing an
186 * immediate no-op diff.
187 * @returns The NodeData for the node.
188 */
189function getData(node: Node, fallbackKey?: Key) {
190 return importSingleNode(node, fallbackKey);
191}
192
193/**
194 * Gets the key for a Node. note that the Node should have been imported
195 * by now.
196 * @param node The node to check.
197 * @returns The key used to create the node.
198 */
199function getKey(node: Node) {
200 assert(node["__incrementalDOMData"]);
201 return getData(node).key;
202}
203
204/**
205 * Clears all caches from a node and all of its children.
206 * @param node The Node to clear the cache for.
207 */
208function clearCache(node: Node) {
209 node["__incrementalDOMData"] = null;
210
211 for (
212 let child: Node | null = node.firstChild;
213 child;
214 child = child.nextSibling
215 ) {
216 clearCache(child);
217 }
218}
219
220export { getData, getKey, initData, importNode, isDataInitialized, clearCache };