UNPKG

13.1 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 assertInPatch,
19 assertNoChildrenDeclaredYet,
20 assertNotInAttributes,
21 assertNoUnclosedTags,
22 assertPatchElementNoExtras,
23 assertPatchOuterHasParentNode,
24 assertVirtualAttributesClosed,
25 setInAttributes,
26 setInSkip,
27 updatePatchContext
28} from "./assertions";
29import { Context } from "./context";
30import { getFocusedPath, moveBefore } from "./dom_util";
31import { DEBUG } from "./global";
32import { getData } from "./node_data";
33import { createElement, createText } from "./nodes";
34import {
35 Key,
36 MatchFnDef,
37 NameOrCtorDef,
38 PatchConfig,
39 PatchFunction
40} from "./types";
41
42/**
43 * The default match function to use, if one was not specified when creating
44 * the patcher.
45 * @param matchNode The node to match against, unused.
46 * @param nameOrCtor The name or constructor as declared.
47 * @param expectedNameOrCtor The name or constructor of the existing node.
48 * @param key The key as declared.
49 * @param expectedKey The key of the existing node.
50 * @returns True if the node matches, false otherwise.
51 */
52function defaultMatchFn(
53 matchNode: Node,
54 nameOrCtor: NameOrCtorDef,
55 expectedNameOrCtor: NameOrCtorDef,
56 key: Key,
57 expectedKey: Key
58): boolean {
59 // Key check is done using double equals as we want to treat a null key the
60 // same as undefined. This should be okay as the only values allowed are
61 // strings, null and undefined so the == semantics are not too weird.
62 return nameOrCtor == expectedNameOrCtor && key == expectedKey;
63}
64
65let context: Context | null = null;
66
67let currentNode: Node | null = null;
68
69let currentParent: Node | null = null;
70
71let doc: Document | null = null;
72
73let focusPath: Array<Node> = [];
74
75let matchFn: MatchFnDef = defaultMatchFn;
76
77/**
78 * Used to build up call arguments. Each patch call gets a separate copy, so
79 * this works with nested calls to patch.
80 */
81let argsBuilder: Array<{} | null | undefined> = [];
82
83/**
84 * Used to build up attrs for the an element.
85 */
86let attrsBuilder: Array<any> = [];
87
88/**
89 * TODO(sparhami) We should just export argsBuilder directly when Closure
90 * Compiler supports ES6 directly.
91 * @returns The Array used for building arguments.
92 */
93function getArgsBuilder(): Array<any> {
94 return argsBuilder;
95}
96
97/**
98 * TODO(sparhami) We should just export attrsBuilder directly when Closure
99 * Compiler supports ES6 directly.
100 * @returns The Array used for building arguments.
101 */
102function getAttrsBuilder(): Array<any> {
103 return attrsBuilder;
104}
105
106/**
107 * Checks whether or not the current node matches the specified nameOrCtor and
108 * key. This uses the specified match function when creating the patcher.
109 * @param matchNode A node to match the data to.
110 * @param nameOrCtor The name or constructor to check for.
111 * @param key The key used to identify the Node.
112 * @return True if the node matches, false otherwise.
113 */
114function matches(
115 matchNode: Node,
116 nameOrCtor: NameOrCtorDef,
117 key: Key
118): boolean {
119 const data = getData(matchNode, key);
120
121 return matchFn(matchNode, nameOrCtor, data.nameOrCtor, key, data.key);
122}
123
124/**
125 * Finds the matching node, starting at `node` and looking at the subsequent
126 * siblings if a key is used.
127 * @param matchNode The node to start looking at.
128 * @param nameOrCtor The name or constructor for the Node.
129 * @param key The key used to identify the Node.
130 * @returns The matching Node, if any exists.
131 */
132function getMatchingNode(
133 matchNode: Node | null,
134 nameOrCtor: NameOrCtorDef,
135 key: Key
136): Node | null {
137 if (!matchNode) {
138 return null;
139 }
140
141 let cur: Node | null = matchNode;
142
143 do {
144 if (matches(cur, nameOrCtor, key)) {
145 return cur;
146 }
147 } while (key && (cur = cur.nextSibling));
148
149 return null;
150}
151/**
152 * Clears out any unvisited Nodes in a given range.
153 * @param maybeParentNode
154 * @param startNode The node to start clearing from, inclusive.
155 * @param endNode The node to clear until, exclusive.
156 */
157function clearUnvisitedDOM(
158 maybeParentNode: Node | null,
159 startNode: Node | null,
160 endNode: Node | null
161) {
162 const parentNode = maybeParentNode!;
163 let child = startNode;
164
165 while (child !== endNode) {
166 const next = child!.nextSibling;
167 parentNode.removeChild(child!);
168 context!.markDeleted(child!);
169 child = next;
170 }
171}
172
173/**
174 * @return The next Node to be patched.
175 */
176function getNextNode(): Node | null {
177 if (currentNode) {
178 return currentNode.nextSibling;
179 } else {
180 return currentParent!.firstChild;
181 }
182}
183
184/**
185 * Changes to the first child of the current node.
186 */
187function enterNode() {
188 currentParent = currentNode;
189 currentNode = null;
190}
191
192/**
193 * Changes to the parent of the current node, removing any unvisited children.
194 */
195function exitNode() {
196 clearUnvisitedDOM(currentParent, getNextNode(), null);
197
198 currentNode = currentParent;
199 currentParent = currentParent!.parentNode;
200}
201
202/**
203 * Changes to the next sibling of the current node.
204 */
205function nextNode() {
206 currentNode = getNextNode();
207}
208
209/**
210 * Creates a Node and marking it as created.
211 * @param nameOrCtor The name or constructor for the Node.
212 * @param key The key used to identify the Node.
213 * @return The newly created node.
214 */
215function createNode(nameOrCtor: NameOrCtorDef, key: Key): Node {
216 let node;
217
218 if (nameOrCtor === "#text") {
219 node = createText(doc!);
220 } else {
221 node = createElement(doc!, currentParent!, nameOrCtor, key);
222 }
223
224 context!.markCreated(node);
225
226 return node;
227}
228
229/**
230 * Aligns the virtual Node definition with the actual DOM, moving the
231 * corresponding DOM node to the correct location or creating it if necessary.
232 * @param nameOrCtor The name or constructor for the Node.
233 * @param key The key used to identify the Node.
234 */
235function alignWithDOM(nameOrCtor: NameOrCtorDef, key: Key) {
236 nextNode();
237 const existingNode = getMatchingNode(currentNode, nameOrCtor, key);
238 const node = existingNode || createNode(nameOrCtor, key);
239
240 // If we are at the matching node, then we are done.
241 if (node === currentNode) {
242 return;
243 }
244
245 // Re-order the node into the right position, preserving focus if either
246 // node or currentNode are focused by making sure that they are not detached
247 // from the DOM.
248 if (focusPath.indexOf(node) >= 0) {
249 // Move everything else before the node.
250 moveBefore(currentParent!, node, currentNode);
251 } else {
252 currentParent!.insertBefore(node, currentNode);
253 }
254
255 currentNode = node;
256}
257
258/**
259 * Makes sure that the current node is an Element with a matching nameOrCtor and
260 * key.
261 *
262 * @param nameOrCtor The tag or constructor for the Element.
263 * @param key The key used to identify this element. This can be an
264 * empty string, but performance may be better if a unique value is used
265 * when iterating over an array of items.
266 * @return The corresponding Element.
267 */
268function open(nameOrCtor: NameOrCtorDef, key?: Key): HTMLElement {
269 alignWithDOM(nameOrCtor, key);
270 enterNode();
271 return currentParent as HTMLElement;
272}
273
274/**
275 * Closes the currently open Element, removing any unvisited children if
276 * necessary.
277 * @returns The Element that was just closed.
278 */
279function close(): Element {
280 if (DEBUG) {
281 setInSkip(false);
282 }
283
284 exitNode();
285 return currentNode as Element;
286}
287
288/**
289 * Makes sure the current node is a Text node and creates a Text node if it is
290 * not.
291 * @returns The Text node that was aligned or created.
292 */
293function text(): Text {
294 alignWithDOM("#text", null);
295 return currentNode as Text;
296}
297
298/**
299 * @returns The current Element being patched.
300 */
301function currentElement(): Element {
302 if (DEBUG) {
303 assertInPatch("currentElement");
304 assertNotInAttributes("currentElement");
305 }
306 return currentParent as Element;
307}
308
309/**
310 * @return The Node that will be evaluated for the next instruction.
311 */
312function currentPointer(): Node {
313 if (DEBUG) {
314 assertInPatch("currentPointer");
315 assertNotInAttributes("currentPointer");
316 }
317 // TODO(tomnguyen): assert that this is not null
318 return getNextNode()!;
319}
320
321/**
322 * Skips the children in a subtree, allowing an Element to be closed without
323 * clearing out the children.
324 */
325function skip() {
326 if (DEBUG) {
327 assertNoChildrenDeclaredYet("skip", currentNode);
328 setInSkip(true);
329 }
330 currentNode = currentParent!.lastChild;
331}
332
333/**
334 * Returns a patcher function that sets up and restores a patch context,
335 * running the run function with the provided data.
336 * @param run The function that will run the patch.
337 * @param patchConfig The configuration to use for the patch.
338 * @returns The created patch function.
339 */
340function createPatcher<T, R>(
341 run: PatchFunction<T, R>,
342 patchConfig: PatchConfig = {}
343): PatchFunction<T, R> {
344 const { matches = defaultMatchFn } = patchConfig;
345
346 const f: PatchFunction<T, R> = (node, fn, data) => {
347 const prevContext = context;
348 const prevDoc = doc;
349 const prevFocusPath = focusPath;
350 const prevArgsBuilder = argsBuilder;
351 const prevAttrsBuilder = attrsBuilder;
352 const prevCurrentNode = currentNode;
353 const prevCurrentParent = currentParent;
354 const prevMatchFn = matchFn;
355 let previousInAttributes = false;
356 let previousInSkip = false;
357
358 doc = node.ownerDocument;
359 context = new Context();
360 matchFn = matches;
361 argsBuilder = [];
362 attrsBuilder = [];
363 currentNode = null;
364 currentParent = node.parentNode;
365 focusPath = getFocusedPath(node, currentParent);
366
367 if (DEBUG) {
368 previousInAttributes = setInAttributes(false);
369 previousInSkip = setInSkip(false);
370 updatePatchContext(context);
371 }
372
373 try {
374 const retVal = run(node, fn, data);
375 if (DEBUG) {
376 assertVirtualAttributesClosed();
377 }
378
379 return retVal;
380 } finally {
381 context.notifyChanges();
382
383 doc = prevDoc;
384 context = prevContext;
385 matchFn = prevMatchFn;
386 argsBuilder = prevArgsBuilder;
387 attrsBuilder = prevAttrsBuilder;
388 currentNode = prevCurrentNode;
389 currentParent = prevCurrentParent;
390 focusPath = prevFocusPath;
391
392 // Needs to be done after assertions because assertions rely on state
393 // from these methods.
394 if (DEBUG) {
395 setInAttributes(previousInAttributes);
396 setInSkip(previousInSkip);
397 updatePatchContext(context);
398 }
399 }
400 };
401 return f;
402}
403
404/**
405 * Creates a patcher that patches the document starting at node with a
406 * provided function. This function may be called during an existing patch operation.
407 * @param patchConfig The config to use for the patch.
408 * @returns The created function for patching an Element's children.
409 */
410function createPatchInner<T>(
411 patchConfig?: PatchConfig
412): PatchFunction<T, Node> {
413 return createPatcher((node, fn, data) => {
414 currentNode = node;
415
416 enterNode();
417 fn(data);
418 exitNode();
419
420 if (DEBUG) {
421 assertNoUnclosedTags(currentNode, node);
422 }
423
424 return node;
425 }, patchConfig);
426}
427
428/**
429 * Creates a patcher that patches an Element with the the provided function.
430 * Exactly one top level element call should be made corresponding to `node`.
431 * @param patchConfig The config to use for the patch.
432 * @returns The created function for patching an Element.
433 */
434function createPatchOuter<T>(
435 patchConfig?: PatchConfig
436): PatchFunction<T, Node | null> {
437 return createPatcher((node, fn, data) => {
438 const startNode = ({ nextSibling: node } as any) as Element;
439 let expectedNextNode: Node | null = null;
440 let expectedPrevNode: Node | null = null;
441
442 if (DEBUG) {
443 expectedNextNode = node.nextSibling;
444 expectedPrevNode = node.previousSibling;
445 }
446
447 currentNode = startNode;
448 fn(data);
449
450 if (DEBUG) {
451 assertPatchOuterHasParentNode(currentParent);
452 assertPatchElementNoExtras(
453 startNode,
454 currentNode,
455 expectedNextNode,
456 expectedPrevNode
457 );
458 }
459
460 if (currentParent) {
461 clearUnvisitedDOM(currentParent, getNextNode(), node.nextSibling);
462 }
463
464 return startNode === currentNode ? null : currentNode;
465 }, patchConfig);
466}
467
468const patchInner: <T>(
469 node: Element | DocumentFragment,
470 template: (a: T | undefined) => void,
471 data?: T | undefined
472) => Node = createPatchInner();
473const patchOuter: <T>(
474 node: Element | DocumentFragment,
475 template: (a: T | undefined) => void,
476 data?: T | undefined
477) => Node | null = createPatchOuter();
478
479export {
480 alignWithDOM,
481 getArgsBuilder,
482 getAttrsBuilder,
483 text,
484 createPatchInner,
485 createPatchOuter,
486 patchInner,
487 patchOuter,
488 open,
489 close,
490 currentElement,
491 currentPointer,
492 skip,
493 nextNode as skipNode
494};