UNPKG

7.09 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 { DEBUG } from "./global";
18import { NameOrCtorDef } from "./types";
19
20/**
21 * Keeps track whether or not we are in an attributes declaration (after
22 * elementOpenStart, but before elementOpenEnd).
23 */
24let inAttributes = false;
25
26/**
27 * Keeps track whether or not we are in an element that should not have its
28 * children cleared.
29 */
30let inSkip = false;
31
32/**
33 * Keeps track of whether or not we are in a patch.
34 */
35let inPatch = false;
36
37/**
38 * Asserts that a value exists and is not null or undefined. goog.asserts
39 * is not used in order to avoid dependencies on external code.
40 * @param val The value to assert is truthy.
41 * @returns The value.
42 */
43function assert<T extends {}>(val: T | null | undefined): T {
44 if (DEBUG && !val) {
45 throw new Error("Expected value to be defined");
46 }
47 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
48 return val!;
49}
50
51/**
52 * Makes sure that there is a current patch context.
53 * @param functionName The name of the caller, for the error message.
54 */
55function assertInPatch(functionName: string) {
56 if (!inPatch) {
57 throw new Error("Cannot call " + functionName + "() unless in patch.");
58 }
59}
60
61/**
62 * Makes sure that a patch closes every node that it opened.
63 * @param openElement
64 * @param root
65 */
66function assertNoUnclosedTags(
67 openElement: Node | null,
68 root: Node | DocumentFragment
69) {
70 if (openElement === root) {
71 return;
72 }
73
74 let currentElement = openElement;
75 const openTags: Array<string> = [];
76 while (currentElement && currentElement !== root) {
77 openTags.push(currentElement.nodeName.toLowerCase());
78 currentElement = currentElement.parentNode;
79 }
80
81 throw new Error("One or more tags were not closed:\n" + openTags.join("\n"));
82}
83
84/**
85 * Makes sure that node being outer patched has a parent node.
86 * @param parent
87 */
88function assertPatchOuterHasParentNode(parent: Node | null) {
89 if (!parent) {
90 console.warn(
91 "patchOuter requires the node have a parent if there is a key."
92 );
93 }
94}
95
96/**
97 * Makes sure that the caller is not where attributes are expected.
98 * @param functionName The name of the caller, for the error message.
99 */
100function assertNotInAttributes(functionName: string) {
101 if (inAttributes) {
102 throw new Error(
103 functionName +
104 "() can not be called between " +
105 "elementOpenStart() and elementOpenEnd()."
106 );
107 }
108}
109
110/**
111 * Makes sure that the caller is not inside an element that has declared skip.
112 * @param functionName The name of the caller, for the error message.
113 */
114function assertNotInSkip(functionName: string) {
115 if (inSkip) {
116 throw new Error(
117 functionName +
118 "() may not be called inside an element " +
119 "that has called skip()."
120 );
121 }
122}
123
124/**
125 * Makes sure that the caller is where attributes are expected.
126 * @param functionName The name of the caller, for the error message.
127 */
128function assertInAttributes(functionName: string) {
129 if (!inAttributes) {
130 throw new Error(
131 functionName +
132 "() can only be called after calling " +
133 "elementOpenStart()."
134 );
135 }
136}
137
138/**
139 * Makes sure the patch closes virtual attributes call
140 */
141function assertVirtualAttributesClosed() {
142 if (inAttributes) {
143 throw new Error(
144 "elementOpenEnd() must be called after calling " + "elementOpenStart()."
145 );
146 }
147}
148
149/**
150 * Makes sure that tags are correctly nested.
151 * @param currentNameOrCtor
152 * @param nameOrCtor
153 */
154function assertCloseMatchesOpenTag(
155 currentNameOrCtor: NameOrCtorDef,
156 nameOrCtor: NameOrCtorDef
157) {
158 if (currentNameOrCtor !== nameOrCtor) {
159 throw new Error(
160 'Received a call to close "' +
161 nameOrCtor +
162 '" but "' +
163 currentNameOrCtor +
164 '" was open.'
165 );
166 }
167}
168
169/**
170 * Makes sure that no children elements have been declared yet in the current
171 * element.
172 * @param functionName The name of the caller, for the error message.
173 * @param previousNode
174 */
175function assertNoChildrenDeclaredYet(
176 functionName: string,
177 previousNode: Node | null
178) {
179 if (previousNode !== null) {
180 throw new Error(
181 functionName +
182 "() must come before any child " +
183 "declarations inside the current element."
184 );
185 }
186}
187
188/**
189 * Checks that a call to patchOuter actually patched the element.
190 * @param maybeStartNode The value for the currentNode when the patch
191 * started.
192 * @param maybeCurrentNode The currentNode when the patch finished.
193 * @param expectedNextNode The Node that is expected to follow the
194 * currentNode after the patch;
195 * @param expectedPrevNode The Node that is expected to preceed the
196 * currentNode after the patch.
197 */
198function assertPatchElementNoExtras(
199 maybeStartNode: Node | null,
200 maybeCurrentNode: Node | null,
201 expectedNextNode: Node | null,
202 expectedPrevNode: Node | null
203) {
204 const startNode = assert(maybeStartNode);
205 const currentNode = assert(maybeCurrentNode);
206 const wasUpdated =
207 currentNode.nextSibling === expectedNextNode &&
208 currentNode.previousSibling === expectedPrevNode;
209 const wasChanged =
210 currentNode.nextSibling === startNode.nextSibling &&
211 currentNode.previousSibling === expectedPrevNode;
212 const wasRemoved = currentNode === startNode;
213
214 if (!wasUpdated && !wasChanged && !wasRemoved) {
215 throw new Error(
216 "There must be exactly one top level call corresponding " +
217 "to the patched element."
218 );
219 }
220}
221
222/**
223 * @param newContext The current patch context.
224 */
225function updatePatchContext(newContext: {} | null) {
226 inPatch = newContext != null;
227}
228
229/**
230 * Updates the state of being in an attribute declaration.
231 * @param value Whether or not the patch is in an attribute declaration.
232 * @return the previous value.
233 */
234function setInAttributes(value: boolean) {
235 const previous = inAttributes;
236 inAttributes = value;
237 return previous;
238}
239
240/**
241 * Updates the state of being in a skip element.
242 * @param value Whether or not the patch is skipping the children of a
243 * parent node.
244 * @return the previous value.
245 */
246function setInSkip(value: boolean) {
247 const previous = inSkip;
248 inSkip = value;
249 return previous;
250}
251
252export {
253 assert,
254 assertInPatch,
255 assertNoUnclosedTags,
256 assertNotInAttributes,
257 assertInAttributes,
258 assertCloseMatchesOpenTag,
259 assertVirtualAttributesClosed,
260 assertNoChildrenDeclaredYet,
261 assertNotInSkip,
262 assertPatchElementNoExtras,
263 assertPatchOuterHasParentNode,
264 setInAttributes,
265 setInSkip,
266 updatePatchContext
267};