import { isSingleExpression } from './directive-helpers.js'; import { Directive, PartType } from './directive.js'; export { Directive, PartType, directive } from './directive.js'; /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ /** * Recursively walks down the tree of Parts/TemplateInstances/Directives to set * the connected state of directives and run `disconnected`/ `reconnected` * callbacks. * * @return True if there were children to disconnect; false otherwise */ const notifyChildrenConnectedChanged = (parent, isConnected) => { const children = parent._$disconnectableChildren; if (children === undefined) { return false; } for (const obj of children) { // The existence of `_$notifyDirectiveConnectionChanged` is used as a "brand" to // disambiguate AsyncDirectives from other DisconnectableChildren // (as opposed to using an instanceof check to know when to call it); the // redundancy of "Directive" in the API name is to avoid conflicting with // `_$notifyConnectionChanged`, which exists `ChildParts` which are also in // this list // Disconnect Directive (and any nested directives contained within) // This property needs to remain unminified. obj['_$notifyDirectiveConnectionChanged']?.(isConnected, false); // Disconnect Part/TemplateInstance notifyChildrenConnectedChanged(obj, isConnected); } return true; }; /** * Removes the given child from its parent list of disconnectable children, and * if the parent list becomes empty as a result, removes the parent from its * parent, and so forth up the tree when that causes subsequent parent lists to * become empty. */ const removeDisconnectableFromParent = (obj) => { let parent, children; do { if ((parent = obj._$parent) === undefined) { break; } children = parent._$disconnectableChildren; children.delete(obj); obj = parent; } while (children?.size === 0); }; const addDisconnectableToParent = (obj) => { // Climb the parent tree, creating a sparse tree of children needing // disconnection for (let parent; (parent = obj._$parent); obj = parent) { let children = parent._$disconnectableChildren; if (children === undefined) { parent._$disconnectableChildren = children = new Set(); } else if (children.has(obj)) { // Once we've reached a parent that already contains this child, we // can short-circuit break; } children.add(obj); installDisconnectAPI(parent); } }; /** * Changes the parent reference of the ChildPart, and updates the sparse tree of * Disconnectable children accordingly. * * Note, this method will be patched onto ChildPart instances and called from * the core code when parts are moved between different parents. */ function reparentDisconnectables(newParent) { if (this._$disconnectableChildren !== undefined) { removeDisconnectableFromParent(this); this._$parent = newParent; addDisconnectableToParent(this); } else { this._$parent = newParent; } } /** * Sets the connected state on any directives contained within the committed * value of this part (i.e. within a TemplateInstance or iterable of * ChildParts) and runs their `disconnected`/`reconnected`s, as well as within * any directives stored on the ChildPart (when `valueOnly` is false). * * `isClearingValue` should be passed as `true` on a top-level part that is * clearing itself, and not as a result of recursively disconnecting directives * as part of a `clear` operation higher up the tree. This both ensures that any * directive on this ChildPart that produced a value that caused the clear * operation is not disconnected, and also serves as a performance optimization * to avoid needless bookkeeping when a subtree is going away; when clearing a * subtree, only the top-most part need to remove itself from the parent. * * `fromPartIndex` is passed only in the case of a partial `_clear` running as a * result of truncating an iterable. * * Note, this method will be patched onto ChildPart instances and called from the * core code when parts are cleared or the connection state is changed by the * user. */ function notifyChildPartConnectedChanged(isConnected, isClearingValue = false, fromPartIndex = 0) { const value = this._$committedValue; const children = this._$disconnectableChildren; if (children === undefined || children.size === 0) { return; } if (isClearingValue) { if (Array.isArray(value)) { // Iterable case: Any ChildParts created by the iterable should be // disconnected and removed from this ChildPart's disconnectable // children (starting at `fromPartIndex` in the case of truncation) for (let i = fromPartIndex; i < value.length; i++) { notifyChildrenConnectedChanged(value[i], false); removeDisconnectableFromParent(value[i]); } } else if (value != null) { // TemplateInstance case: If the value has disconnectable children (will // only be in the case that it is a TemplateInstance), we disconnect it // and remove it from this ChildPart's disconnectable children notifyChildrenConnectedChanged(value, false); removeDisconnectableFromParent(value); } } else { notifyChildrenConnectedChanged(this, isConnected); } } /** * Patches disconnection API onto ChildParts. */ const installDisconnectAPI = (obj) => { if (obj.type == PartType.CHILD) { obj._$notifyConnectionChanged ??= notifyChildPartConnectedChanged; obj._$reparentDisconnectables ??= reparentDisconnectables; } }; /** * An abstract `Directive` base class whose `disconnected` method will be * called when the part containing the directive is cleared as a result of * re-rendering, or when the user calls `part.setConnected(false)` on * a part that was previously rendered containing the directive (as happens * when e.g. a LitElement disconnects from the DOM). * * If `part.setConnected(true)` is subsequently called on a * containing part, the directive's `reconnected` method will be called prior * to its next `update`/`render` callbacks. When implementing `disconnected`, * `reconnected` should also be implemented to be compatible with reconnection. * * Note that updates may occur while the directive is disconnected. As such, * directives should generally check the `this.isConnected` flag during * render/update to determine whether it is safe to subscribe to resources * that may prevent garbage collection. */ class AsyncDirective extends Directive { constructor() { super(...arguments); // @internal this._$disconnectableChildren = undefined; } /** * Initialize the part with internal fields * @param part * @param parent * @param attributeIndex */ _$initialize(part, parent, attributeIndex) { super._$initialize(part, parent, attributeIndex); addDisconnectableToParent(this); this.isConnected = part._$isConnected; } // This property needs to remain unminified. /** * Called from the core code when a directive is going away from a part (in * which case `shouldRemoveFromParent` should be true), and from the * `setChildrenConnected` helper function when recursively changing the * connection state of a tree (in which case `shouldRemoveFromParent` should * be false). * * @param isConnected * @param isClearingDirective - True when the directive itself is being * removed; false when the tree is being disconnected * @internal */ ['_$notifyDirectiveConnectionChanged'](isConnected, isClearingDirective = true) { if (isConnected !== this.isConnected) { this.isConnected = isConnected; if (isConnected) { this.reconnected?.(); } else { this.disconnected?.(); } } if (isClearingDirective) { notifyChildrenConnectedChanged(this, isConnected); removeDisconnectableFromParent(this); } } /** * Sets the value of the directive's Part outside the normal `update`/`render` * lifecycle of a directive. * * This method should not be called synchronously from a directive's `update` * or `render`. * * @param directive The directive to update * @param value The value to set */ setValue(value) { if (isSingleExpression(this.__part)) { this.__part._$setValue(value, this); } else { // this.__attributeIndex will be defined in this case, but // assert it in dev mode if (this.__attributeIndex === undefined) { throw new Error(`Expected this.__attributeIndex to be a number`); } const newValues = [...this.__part._$committedValue]; newValues[this.__attributeIndex] = value; this.__part._$setValue(newValues, this, 0); } } /** * User callbacks for implementing logic to release any resources/subscriptions * that may have been retained by this directive. Since directives may also be * re-connected, `reconnected` should also be implemented to restore the * working state of the directive prior to the next render. */ disconnected() { } reconnected() { } } export { AsyncDirective }; //# sourceMappingURL=async-directive.js.map