UNPKG

9.82 kBJavaScriptView Raw
1/**
2 * @license
3 * Copyright 2017 Google LLC
4 * SPDX-License-Identifier: BSD-3-Clause
5 */
6import { isSingleExpression } from './directive-helpers.js';
7import { Directive, PartType } from './directive.js';
8export * from './directive.js';
9const DEV_MODE = true;
10/**
11 * Recursively walks down the tree of Parts/TemplateInstances/Directives to set
12 * the connected state of directives and run `disconnected`/ `reconnected`
13 * callbacks.
14 *
15 * @return True if there were children to disconnect; false otherwise
16 */
17const notifyChildrenConnectedChanged = (parent, isConnected) => {
18 const children = parent._$disconnectableChildren;
19 if (children === undefined) {
20 return false;
21 }
22 for (const obj of children) {
23 // The existence of `_$notifyDirectiveConnectionChanged` is used as a "brand" to
24 // disambiguate AsyncDirectives from other DisconnectableChildren
25 // (as opposed to using an instanceof check to know when to call it); the
26 // redundancy of "Directive" in the API name is to avoid conflicting with
27 // `_$notifyConnectionChanged`, which exists `ChildParts` which are also in
28 // this list
29 // Disconnect Directive (and any nested directives contained within)
30 // This property needs to remain unminified.
31 obj['_$notifyDirectiveConnectionChanged']?.(isConnected, false);
32 // Disconnect Part/TemplateInstance
33 notifyChildrenConnectedChanged(obj, isConnected);
34 }
35 return true;
36};
37/**
38 * Removes the given child from its parent list of disconnectable children, and
39 * if the parent list becomes empty as a result, removes the parent from its
40 * parent, and so forth up the tree when that causes subsequent parent lists to
41 * become empty.
42 */
43const removeDisconnectableFromParent = (obj) => {
44 let parent, children;
45 do {
46 if ((parent = obj._$parent) === undefined) {
47 break;
48 }
49 children = parent._$disconnectableChildren;
50 children.delete(obj);
51 obj = parent;
52 } while (children?.size === 0);
53};
54const addDisconnectableToParent = (obj) => {
55 // Climb the parent tree, creating a sparse tree of children needing
56 // disconnection
57 for (let parent; (parent = obj._$parent); obj = parent) {
58 let children = parent._$disconnectableChildren;
59 if (children === undefined) {
60 parent._$disconnectableChildren = children = new Set();
61 }
62 else if (children.has(obj)) {
63 // Once we've reached a parent that already contains this child, we
64 // can short-circuit
65 break;
66 }
67 children.add(obj);
68 installDisconnectAPI(parent);
69 }
70};
71/**
72 * Changes the parent reference of the ChildPart, and updates the sparse tree of
73 * Disconnectable children accordingly.
74 *
75 * Note, this method will be patched onto ChildPart instances and called from
76 * the core code when parts are moved between different parents.
77 */
78function reparentDisconnectables(newParent) {
79 if (this._$disconnectableChildren !== undefined) {
80 removeDisconnectableFromParent(this);
81 this._$parent = newParent;
82 addDisconnectableToParent(this);
83 }
84 else {
85 this._$parent = newParent;
86 }
87}
88/**
89 * Sets the connected state on any directives contained within the committed
90 * value of this part (i.e. within a TemplateInstance or iterable of
91 * ChildParts) and runs their `disconnected`/`reconnected`s, as well as within
92 * any directives stored on the ChildPart (when `valueOnly` is false).
93 *
94 * `isClearingValue` should be passed as `true` on a top-level part that is
95 * clearing itself, and not as a result of recursively disconnecting directives
96 * as part of a `clear` operation higher up the tree. This both ensures that any
97 * directive on this ChildPart that produced a value that caused the clear
98 * operation is not disconnected, and also serves as a performance optimization
99 * to avoid needless bookkeeping when a subtree is going away; when clearing a
100 * subtree, only the top-most part need to remove itself from the parent.
101 *
102 * `fromPartIndex` is passed only in the case of a partial `_clear` running as a
103 * result of truncating an iterable.
104 *
105 * Note, this method will be patched onto ChildPart instances and called from the
106 * core code when parts are cleared or the connection state is changed by the
107 * user.
108 */
109function notifyChildPartConnectedChanged(isConnected, isClearingValue = false, fromPartIndex = 0) {
110 const value = this._$committedValue;
111 const children = this._$disconnectableChildren;
112 if (children === undefined || children.size === 0) {
113 return;
114 }
115 if (isClearingValue) {
116 if (Array.isArray(value)) {
117 // Iterable case: Any ChildParts created by the iterable should be
118 // disconnected and removed from this ChildPart's disconnectable
119 // children (starting at `fromPartIndex` in the case of truncation)
120 for (let i = fromPartIndex; i < value.length; i++) {
121 notifyChildrenConnectedChanged(value[i], false);
122 removeDisconnectableFromParent(value[i]);
123 }
124 }
125 else if (value != null) {
126 // TemplateInstance case: If the value has disconnectable children (will
127 // only be in the case that it is a TemplateInstance), we disconnect it
128 // and remove it from this ChildPart's disconnectable children
129 notifyChildrenConnectedChanged(value, false);
130 removeDisconnectableFromParent(value);
131 }
132 }
133 else {
134 notifyChildrenConnectedChanged(this, isConnected);
135 }
136}
137/**
138 * Patches disconnection API onto ChildParts.
139 */
140const installDisconnectAPI = (obj) => {
141 if (obj.type == PartType.CHILD) {
142 obj._$notifyConnectionChanged ??=
143 notifyChildPartConnectedChanged;
144 obj._$reparentDisconnectables ??= reparentDisconnectables;
145 }
146};
147/**
148 * An abstract `Directive` base class whose `disconnected` method will be
149 * called when the part containing the directive is cleared as a result of
150 * re-rendering, or when the user calls `part.setConnected(false)` on
151 * a part that was previously rendered containing the directive (as happens
152 * when e.g. a LitElement disconnects from the DOM).
153 *
154 * If `part.setConnected(true)` is subsequently called on a
155 * containing part, the directive's `reconnected` method will be called prior
156 * to its next `update`/`render` callbacks. When implementing `disconnected`,
157 * `reconnected` should also be implemented to be compatible with reconnection.
158 *
159 * Note that updates may occur while the directive is disconnected. As such,
160 * directives should generally check the `this.isConnected` flag during
161 * render/update to determine whether it is safe to subscribe to resources
162 * that may prevent garbage collection.
163 */
164export class AsyncDirective extends Directive {
165 constructor() {
166 super(...arguments);
167 // @internal
168 this._$disconnectableChildren = undefined;
169 }
170 /**
171 * Initialize the part with internal fields
172 * @param part
173 * @param parent
174 * @param attributeIndex
175 */
176 _$initialize(part, parent, attributeIndex) {
177 super._$initialize(part, parent, attributeIndex);
178 addDisconnectableToParent(this);
179 this.isConnected = part._$isConnected;
180 }
181 // This property needs to remain unminified.
182 /**
183 * Called from the core code when a directive is going away from a part (in
184 * which case `shouldRemoveFromParent` should be true), and from the
185 * `setChildrenConnected` helper function when recursively changing the
186 * connection state of a tree (in which case `shouldRemoveFromParent` should
187 * be false).
188 *
189 * @param isConnected
190 * @param isClearingDirective - True when the directive itself is being
191 * removed; false when the tree is being disconnected
192 * @internal
193 */
194 ['_$notifyDirectiveConnectionChanged'](isConnected, isClearingDirective = true) {
195 if (isConnected !== this.isConnected) {
196 this.isConnected = isConnected;
197 if (isConnected) {
198 this.reconnected?.();
199 }
200 else {
201 this.disconnected?.();
202 }
203 }
204 if (isClearingDirective) {
205 notifyChildrenConnectedChanged(this, isConnected);
206 removeDisconnectableFromParent(this);
207 }
208 }
209 /**
210 * Sets the value of the directive's Part outside the normal `update`/`render`
211 * lifecycle of a directive.
212 *
213 * This method should not be called synchronously from a directive's `update`
214 * or `render`.
215 *
216 * @param directive The directive to update
217 * @param value The value to set
218 */
219 setValue(value) {
220 if (isSingleExpression(this.__part)) {
221 this.__part._$setValue(value, this);
222 }
223 else {
224 // this.__attributeIndex will be defined in this case, but
225 // assert it in dev mode
226 if (DEV_MODE && this.__attributeIndex === undefined) {
227 throw new Error(`Expected this.__attributeIndex to be a number`);
228 }
229 const newValues = [...this.__part._$committedValue];
230 newValues[this.__attributeIndex] = value;
231 this.__part._$setValue(newValues, this, 0);
232 }
233 }
234 /**
235 * User callbacks for implementing logic to release any resources/subscriptions
236 * that may have been retained by this directive. Since directives may also be
237 * re-connected, `reconnected` should also be implemented to restore the
238 * working state of the directive prior to the next render.
239 */
240 disconnected() { }
241 reconnected() { }
242}
243//# sourceMappingURL=async-directive.js.map
\No newline at end of file