1 | /**
|
2 | * @license
|
3 | * Copyright 2017 Google LLC
|
4 | * SPDX-License-Identifier: BSD-3-Clause
|
5 | */
|
6 | import { isSingleExpression } from './directive-helpers.js';
|
7 | import { Directive, PartType } from './directive.js';
|
8 | export * from './directive.js';
|
9 | const 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 | */
|
17 | const 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 | */
|
43 | const 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 | };
|
54 | const 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 | */
|
78 | function 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 | */
|
109 | function 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 | */
|
140 | const 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 | */
|
164 | export 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 |