UNPKG

10.3 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 {
18 assert,
19 assertCloseMatchesOpenTag,
20 assertInAttributes,
21 assertInPatch,
22 assertNotInAttributes,
23 assertNotInSkip,
24 setInAttributes
25} from "./assertions";
26import { updateAttribute } from "./attributes";
27import {
28 getArgsBuilder,
29 getAttrsBuilder,
30 close,
31 open,
32 text as coreText,
33 currentElement
34} from "./core";
35import { DEBUG } from "./global";
36import { getData, NodeData } from "./node_data";
37import { Key, NameOrCtorDef, Statics } from "./types";
38import { createMap, truncateArray } from "./util";
39import { calculateDiff } from "./diff";
40
41/**
42 * The offset in the virtual element declaration where the attributes are
43 * specified.
44 */
45const ATTRIBUTES_OFFSET = 3;
46
47/**
48 * Used to keep track of the previous values when a 2-way diff is necessary.
49 * This object is reused.
50 * TODO(sparhamI) Scope this to a patch so you can call patch from an attribute
51 * update.
52 */
53const prevAttrsMap = createMap();
54
55/**
56 * @param element The Element to diff the attrs for.
57 * @param data The NodeData associated with the Element.
58 */
59function diffAttrs(element: Element, data: NodeData) {
60 const attrsBuilder = getAttrsBuilder();
61 const prevAttrsArr = data.getAttrsArr(attrsBuilder.length);
62
63 calculateDiff(prevAttrsArr, attrsBuilder, element, updateAttribute);
64 truncateArray(attrsBuilder, 0);
65}
66
67/**
68 * Applies the statics. When importing an Element, any existing attributes that
69 * match a static are converted into a static attribute.
70 * @param node The Element to apply statics for.
71 * @param data The NodeData associated with the Element.
72 * @param statics The statics array.
73 */
74function diffStatics(node: Element, data: NodeData, statics: Statics) {
75 if (data.staticsApplied) {
76 return;
77 }
78
79 data.staticsApplied = true;
80
81 if (!statics || !statics.length) {
82 return;
83 }
84
85 if (data.hasEmptyAttrsArr()) {
86 for (let i = 0; i < statics.length; i += 2) {
87 updateAttribute(node, statics[i] as string, statics[i + 1]);
88 }
89 return;
90 }
91
92 for (let i = 0; i < statics.length; i += 2) {
93 prevAttrsMap[statics[i] as string] = i + 1;
94 }
95
96 const attrsArr = data.getAttrsArr(0);
97 let j = 0;
98 for (let i = 0; i < attrsArr.length; i += 2) {
99 const name = attrsArr[i];
100 const value = attrsArr[i + 1];
101 const staticsIndex = prevAttrsMap[name];
102
103 if (staticsIndex) {
104 // For any attrs that are static and have the same value, make sure we do
105 // not set them again.
106 if (statics[staticsIndex] === value) {
107 delete prevAttrsMap[name];
108 }
109
110 continue;
111 }
112
113 // For any attrs that are dynamic, move them up to the right place.
114 attrsArr[j] = name;
115 attrsArr[j + 1] = value;
116 j += 2;
117 }
118 // Anything after `j` was either moved up already or static.
119 truncateArray(attrsArr, j);
120
121 for (const name in prevAttrsMap) {
122 updateAttribute(node, name, statics[prevAttrsMap[name]]);
123 delete prevAttrsMap[name];
124 }
125}
126
127/**
128 * Declares a virtual Element at the current location in the document. This
129 * corresponds to an opening tag and a elementClose tag is required. This is
130 * like elementOpen, but the attributes are defined using the attr function
131 * rather than being passed as arguments. Must be folllowed by 0 or more calls
132 * to attr, then a call to elementOpenEnd.
133 * @param nameOrCtor The Element's tag or constructor.
134 * @param key The key used to identify this element. This can be an
135 * empty string, but performance may be better if a unique value is used
136 * when iterating over an array of items.
137 * @param statics An array of attribute name/value pairs of the static
138 * attributes for the Element. Attributes will only be set once when the
139 * Element is created.
140 */
141function elementOpenStart(
142 nameOrCtor: NameOrCtorDef,
143 key?: Key,
144 statics?: Statics
145) {
146 const argsBuilder = getArgsBuilder();
147
148 if (DEBUG) {
149 assertNotInAttributes("elementOpenStart");
150 setInAttributes(true);
151 }
152
153 argsBuilder[0] = nameOrCtor;
154 argsBuilder[1] = key;
155 argsBuilder[2] = statics;
156}
157
158/**
159 * Allows you to define a key after an elementOpenStart. This is useful in
160 * templates that define key after an element has been opened ie
161 * `<div key('foo')></div>`.
162 * @param key The key to use for the next call.
163 */
164function key(key: string) {
165 const argsBuilder = getArgsBuilder();
166
167 if (DEBUG) {
168 assertInAttributes("key");
169 assert(argsBuilder);
170 }
171 argsBuilder[1] = key;
172}
173
174/**
175 * Buffers an attribute, which will get applied during the next call to
176 * `elementOpen`, `elementOpenEnd` or `applyAttrs`.
177 * @param name The of the attribute to buffer.
178 * @param value The value of the attribute to buffer.
179 */
180function attr(name: string, value: any) {
181 const attrsBuilder = getAttrsBuilder();
182
183 if (DEBUG) {
184 assertInPatch("attr");
185 }
186
187 attrsBuilder.push(name);
188 attrsBuilder.push(value);
189}
190
191/**
192 * Closes an open tag started with elementOpenStart.
193 * @return The corresponding Element.
194 */
195function elementOpenEnd(): HTMLElement {
196 const argsBuilder = getArgsBuilder();
197
198 if (DEBUG) {
199 assertInAttributes("elementOpenEnd");
200 setInAttributes(false);
201 }
202
203 const node = open(<NameOrCtorDef>argsBuilder[0], <Key>argsBuilder[1]);
204 const data = getData(node);
205
206 diffStatics(node, data, <Statics>argsBuilder[2]);
207 diffAttrs(node, data);
208 truncateArray(argsBuilder, 0);
209
210 return node;
211}
212
213/**
214 * @param nameOrCtor The Element's tag or constructor.
215 * @param key The key used to identify this element. This can be an
216 * empty string, but performance may be better if a unique value is used
217 * when iterating over an array of items.
218 * @param statics An array of attribute name/value pairs of the static
219 * attributes for the Element. Attributes will only be set once when the
220 * Element is created.
221 * @param varArgs, Attribute name/value pairs of the dynamic attributes
222 * for the Element.
223 * @return The corresponding Element.
224 */
225function elementOpen(
226 nameOrCtor: NameOrCtorDef,
227 key?: Key,
228 // Ideally we could tag statics and varArgs as an array where every odd
229 // element is a string and every even element is any, but this is hard.
230 statics?: Statics,
231 ...varArgs: Array<any>
232) {
233 if (DEBUG) {
234 assertNotInAttributes("elementOpen");
235 assertNotInSkip("elementOpen");
236 }
237
238 elementOpenStart(nameOrCtor, key, statics);
239
240 for (let i = ATTRIBUTES_OFFSET; i < arguments.length; i += 2) {
241 attr(arguments[i], arguments[i + 1]);
242 }
243
244 return elementOpenEnd();
245}
246
247/**
248 * Applies the currently buffered attrs to the currently open element. This
249 * clears the buffered attributes.
250 */
251function applyAttrs() {
252 const node = currentElement();
253 const data = getData(node);
254
255 diffAttrs(node, data);
256}
257
258/**
259 * Applies the current static attributes to the currently open element. Note:
260 * statics should be applied before calling `applyAtrs`.
261 * @param statics The statics to apply to the current element.
262 */
263function applyStatics(statics: Statics) {
264 const node = currentElement();
265 const data = getData(node);
266
267 diffStatics(node, data, statics);
268}
269
270/**
271 * Closes an open virtual Element.
272 *
273 * @param nameOrCtor The Element's tag or constructor.
274 * @return The corresponding Element.
275 */
276function elementClose(nameOrCtor: NameOrCtorDef): Element {
277 if (DEBUG) {
278 assertNotInAttributes("elementClose");
279 }
280
281 const node = close();
282
283 if (DEBUG) {
284 assertCloseMatchesOpenTag(getData(node).nameOrCtor, nameOrCtor);
285 }
286
287 return node;
288}
289
290/**
291 * Declares a virtual Element at the current location in the document that has
292 * no children.
293 * @param nameOrCtor The Element's tag or constructor.
294 * @param key The key used to identify this element. This can be an
295 * empty string, but performance may be better if a unique value is used
296 * when iterating over an array of items.
297 * @param statics An array of attribute name/value pairs of the static
298 * attributes for the Element. Attributes will only be set once when the
299 * Element is created.
300 * @param varArgs Attribute name/value pairs of the dynamic attributes
301 * for the Element.
302 * @return The corresponding Element.
303 */
304function elementVoid(
305 nameOrCtor: NameOrCtorDef,
306 key?: Key,
307 // Ideally we could tag statics and varArgs as an array where every odd
308 // element is a string and every even element is any, but this is hard.
309 statics?: Statics,
310 ...varArgs: Array<any>
311) {
312 elementOpen.apply(null, arguments as any);
313 return elementClose(nameOrCtor);
314}
315
316/**
317 * Declares a virtual Text at this point in the document.
318 *
319 * @param value The value of the Text.
320 * @param varArgs
321 * Functions to format the value which are called only when the value has
322 * changed.
323 * @return The corresponding text node.
324 */
325function text(
326 value: string | number | boolean,
327 ...varArgs: Array<(a: {}) => string>
328) {
329 if (DEBUG) {
330 assertNotInAttributes("text");
331 assertNotInSkip("text");
332 }
333
334 const node = coreText();
335 const data = getData(node);
336
337 if (data.text !== value) {
338 data.text = value as string;
339
340 let formatted = value;
341 for (let i = 1; i < arguments.length; i += 1) {
342 /*
343 * Call the formatter function directly to prevent leaking arguments.
344 * https://github.com/google/incremental-dom/pull/204#issuecomment-178223574
345 */
346 const fn = arguments[i];
347 formatted = fn(formatted);
348 }
349
350 // Setting node.data resets the cursor in IE/Edge.
351 if (node.data !== formatted) {
352 node.data = formatted as string;
353 }
354 }
355
356 return node;
357}
358
359/** */
360export {
361 applyAttrs,
362 applyStatics,
363 elementOpenStart,
364 elementOpenEnd,
365 elementOpen,
366 elementVoid,
367 elementClose,
368 text,
369 attr,
370 key
371};