246 lines
9.6 KiB
JavaScript
246 lines
9.6 KiB
JavaScript
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
|