UNPKG

18.6 kBSource Map (JSON)View Raw
1{"version":3,"file":"async-directive.js","sources":["src/async-directive.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */\n\n/**\n * Overview:\n *\n * This module is designed to add support for an async `setValue` API and\n * `disconnected` callback to directives with the least impact on the core\n * runtime or payload when that feature is not used.\n *\n * The strategy is to introduce a `AsyncDirective` subclass of\n * `Directive` that climbs the \"parent\" tree in its constructor to note which\n * branches of lit-html's \"logical tree\" of data structures contain such\n * directives and thus need to be crawled when a subtree is being cleared (or\n * manually disconnected) in order to run the `disconnected` callback.\n *\n * The \"nodes\" of the logical tree include Parts, TemplateInstances (for when a\n * TemplateResult is committed to a value of a ChildPart), and Directives; these\n * all implement a common interface called `DisconnectableChild`. Each has a\n * `_$parent` reference which is set during construction in the core code, and a\n * `_$disconnectableChildren` field which is initially undefined.\n *\n * The sparse tree created by means of the `AsyncDirective` constructor\n * crawling up the `_$parent` tree and placing a `_$disconnectableChildren` Set\n * on each parent that includes each child that contains a\n * `AsyncDirective` directly or transitively via its children. In order to\n * notify connection state changes and disconnect (or reconnect) a tree, the\n * `_$notifyConnectionChanged` API is patched onto ChildParts as a directive\n * climbs the parent tree, which is called by the core when clearing a part if\n * it exists. When called, that method iterates over the sparse tree of\n * Set<DisconnectableChildren> built up by AsyncDirectives, and calls\n * `_$notifyDirectiveConnectionChanged` on any directives that are encountered\n * in that tree, running the required callbacks.\n *\n * A given \"logical tree\" of lit-html data-structures might look like this:\n *\n * ChildPart(N1) _$dC=[D2,T3]\n * ._directive\n * AsyncDirective(D2)\n * ._value // user value was TemplateResult\n * TemplateInstance(T3) _$dC=[A4,A6,N10,N12]\n * ._parts[]\n * AttributePart(A4) _$dC=[D5]\n * ._directives[]\n * AsyncDirective(D5)\n * AttributePart(A6) _$dC=[D7,D8]\n * ._directives[]\n * AsyncDirective(D7)\n * Directive(D8) _$dC=[D9]\n * ._directive\n * AsyncDirective(D9)\n * ChildPart(N10) _$dC=[D11]\n * ._directive\n * AsyncDirective(D11)\n * ._value\n * string\n * ChildPart(N12) _$dC=[D13,N14,N16]\n * ._directive\n * AsyncDirective(D13)\n * ._value // user value was iterable\n * Array<ChildPart>\n * ChildPart(N14) _$dC=[D15]\n * ._value\n * string\n * ChildPart(N16) _$dC=[D17,T18]\n * ._directive\n * AsyncDirective(D17)\n * ._value // user value was TemplateResult\n * TemplateInstance(T18) _$dC=[A19,A21,N25]\n * ._parts[]\n * AttributePart(A19) _$dC=[D20]\n * ._directives[]\n * AsyncDirective(D20)\n * AttributePart(A21) _$dC=[22,23]\n * ._directives[]\n * AsyncDirective(D22)\n * Directive(D23) _$dC=[D24]\n * ._directive\n * AsyncDirective(D24)\n * ChildPart(N25) _$dC=[D26]\n * ._directive\n * AsyncDirective(D26)\n * ._value\n * string\n *\n * Example 1: The directive in ChildPart(N12) updates and returns `nothing`. The\n * ChildPart will _clear() itself, and so we need to disconnect the \"value\" of\n * the ChildPart (but not its directive). In this case, when `_clear()` calls\n * `_$notifyConnectionChanged()`, we don't iterate all of the\n * _$disconnectableChildren, rather we do a value-specific disconnection: i.e.\n * since the _value was an Array<ChildPart> (because an iterable had been\n * committed), we iterate the array of ChildParts (N14, N16) and run\n * `setConnected` on them (which does recurse down the full tree of\n * `_$disconnectableChildren` below it, and also removes N14 and N16 from N12's\n * `_$disconnectableChildren`). Once the values have been disconnected, we then\n * check whether the ChildPart(N12)'s list of `_$disconnectableChildren` is empty\n * (and would remove it from its parent TemplateInstance(T3) if so), but since\n * it would still contain its directive D13, it stays in the disconnectable\n * tree.\n *\n * Example 2: In the course of Example 1, `setConnected` will reach\n * ChildPart(N16); in this case the entire part is being disconnected, so we\n * simply iterate all of N16's `_$disconnectableChildren` (D17,T18) and\n * recursively run `setConnected` on them. Note that we only remove children\n * from `_$disconnectableChildren` for the top-level values being disconnected\n * on a clear; doing this bookkeeping lower in the tree is wasteful since it's\n * all being thrown away.\n *\n * Example 3: If the LitElement containing the entire tree above becomes\n * disconnected, it will run `childPart.setConnected()` (which calls\n * `childPart._$notifyConnectionChanged()` if it exists); in this case, we\n * recursively run `setConnected()` over the entire tree, without removing any\n * children from `_$disconnectableChildren`, since this tree is required to\n * re-connect the tree, which does the same operation, simply passing\n * `isConnectd: true` down the tree, signaling which callback to run.\n */\n\nimport {AttributePart, ChildPart, Disconnectable, Part} from './lit-html.js';\nimport {isSingleExpression} from './directive-helpers.js';\nimport {Directive, PartInfo, PartType} from './directive.js';\nexport {directive} from './directive.js';\n\nconst DEV_MODE = true;\n\n/**\n * Recursively walks down the tree of Parts/TemplateInstances/Directives to set\n * the connected state of directives and run `disconnected`/ `reconnected`\n * callbacks.\n *\n * @return True if there were children to disconnect; false otherwise\n */\nconst notifyChildrenConnectedChanged = (\n parent: Disconnectable,\n isConnected: boolean\n): boolean => {\n const children = parent._$disconnectableChildren;\n if (children === undefined) {\n return false;\n }\n for (const obj of children) {\n // The existence of `_$notifyDirectiveConnectionChanged` is used as a \"brand\" to\n // disambiguate AsyncDirectives from other DisconnectableChildren\n // (as opposed to using an instanceof check to know when to call it); the\n // redundancy of \"Directive\" in the API name is to avoid conflicting with\n // `_$notifyConnectionChanged`, which exists `ChildParts` which are also in\n // this list\n // Disconnect Directive (and any nested directives contained within)\n // This property needs to remain unminified.\n (obj as AsyncDirective)['_$notifyDirectiveConnectionChanged']?.(\n isConnected,\n false\n );\n // Disconnect Part/TemplateInstance\n notifyChildrenConnectedChanged(obj, isConnected);\n }\n return true;\n};\n\n/**\n * Removes the given child from its parent list of disconnectable children, and\n * if the parent list becomes empty as a result, removes the parent from its\n * parent, and so forth up the tree when that causes subsequent parent lists to\n * become empty.\n */\nconst removeDisconnectableFromParent = (obj: Disconnectable) => {\n let parent, children;\n do {\n if ((parent = obj._$parent) === undefined) {\n break;\n }\n children = parent._$disconnectableChildren!;\n children.delete(obj);\n obj = parent;\n } while (children?.size === 0);\n};\n\nconst addDisconnectableToParent = (obj: Disconnectable) => {\n // Climb the parent tree, creating a sparse tree of children needing\n // disconnection\n for (let parent; (parent = obj._$parent); obj = parent) {\n let children = parent._$disconnectableChildren;\n if (children === undefined) {\n parent._$disconnectableChildren = children = new Set();\n } else if (children.has(obj)) {\n // Once we've reached a parent that already contains this child, we\n // can short-circuit\n break;\n }\n children.add(obj);\n installDisconnectAPI(parent);\n }\n};\n\n/**\n * Changes the parent reference of the ChildPart, and updates the sparse tree of\n * Disconnectable children accordingly.\n *\n * Note, this method will be patched onto ChildPart instances and called from\n * the core code when parts are moved between different parents.\n */\nfunction reparentDisconnectables(this: ChildPart, newParent: Disconnectable) {\n if (this._$disconnectableChildren !== undefined) {\n removeDisconnectableFromParent(this);\n this._$parent = newParent;\n addDisconnectableToParent(this);\n } else {\n this._$parent = newParent;\n }\n}\n\n/**\n * Sets the connected state on any directives contained within the committed\n * value of this part (i.e. within a TemplateInstance or iterable of\n * ChildParts) and runs their `disconnected`/`reconnected`s, as well as within\n * any directives stored on the ChildPart (when `valueOnly` is false).\n *\n * `isClearingValue` should be passed as `true` on a top-level part that is\n * clearing itself, and not as a result of recursively disconnecting directives\n * as part of a `clear` operation higher up the tree. This both ensures that any\n * directive on this ChildPart that produced a value that caused the clear\n * operation is not disconnected, and also serves as a performance optimization\n * to avoid needless bookkeeping when a subtree is going away; when clearing a\n * subtree, only the top-most part need to remove itself from the parent.\n *\n * `fromPartIndex` is passed only in the case of a partial `_clear` running as a\n * result of truncating an iterable.\n *\n * Note, this method will be patched onto ChildPart instances and called from the\n * core code when parts are cleared or the connection state is changed by the\n * user.\n */\nfunction notifyChildPartConnectedChanged(\n this: ChildPart,\n isConnected: boolean,\n isClearingValue = false,\n fromPartIndex = 0\n) {\n const value = this._$committedValue;\n const children = this._$disconnectableChildren;\n if (children === undefined || children.size === 0) {\n return;\n }\n if (isClearingValue) {\n if (Array.isArray(value)) {\n // Iterable case: Any ChildParts created by the iterable should be\n // disconnected and removed from this ChildPart's disconnectable\n // children (starting at `fromPartIndex` in the case of truncation)\n for (let i = fromPartIndex; i < value.length; i++) {\n notifyChildrenConnectedChanged(value[i], false);\n removeDisconnectableFromParent(value[i]);\n }\n } else if (value != null) {\n // TemplateInstance case: If the value has disconnectable children (will\n // only be in the case that it is a TemplateInstance), we disconnect it\n // and remove it from this ChildPart's disconnectable children\n notifyChildrenConnectedChanged(value as Disconnectable, false);\n removeDisconnectableFromParent(value as Disconnectable);\n }\n } else {\n notifyChildrenConnectedChanged(this, isConnected);\n }\n}\n\n/**\n * Patches disconnection API onto ChildParts.\n */\nconst installDisconnectAPI = (obj: Disconnectable) => {\n if ((obj as ChildPart).type == PartType.CHILD) {\n (obj as ChildPart)._$notifyConnectionChanged ??=\n notifyChildPartConnectedChanged;\n (obj as ChildPart)._$reparentDisconnectables ??= reparentDisconnectables;\n }\n};\n\n/**\n * An abstract `Directive` base class whose `disconnected` method will be\n * called when the part containing the directive is cleared as a result of\n * re-rendering, or when the user calls `part.setConnected(false)` on\n * a part that was previously rendered containing the directive (as happens\n * when e.g. a LitElement disconnects from the DOM).\n *\n * If `part.setConnected(true)` is subsequently called on a\n * containing part, the directive's `reconnected` method will be called prior\n * to its next `update`/`render` callbacks. When implementing `disconnected`,\n * `reconnected` should also be implemented to be compatible with reconnection.\n *\n * Note that updates may occur while the directive is disconnected. As such,\n * directives should generally check the `this.isConnected` flag during\n * render/update to determine whether it is safe to subscribe to resources\n * that may prevent garbage collection.\n */\nexport abstract class AsyncDirective extends Directive {\n // As opposed to other Disconnectables, AsyncDirectives always get notified\n // when the RootPart connection changes, so the public `isConnected`\n // is a locally stored variable initialized via its part's getter and synced\n // via `_$notifyDirectiveConnectionChanged`. This is cheaper than using\n // the _$isConnected getter, which has to look back up the tree each time.\n /**\n * The connection state for this Directive.\n */\n isConnected!: boolean;\n\n // @internal\n override _$disconnectableChildren?: Set<Disconnectable> = undefined;\n /**\n * Initialize the part with internal fields\n * @param part\n * @param parent\n * @param attributeIndex\n */\n override _$initialize(\n part: Part,\n parent: Disconnectable,\n attributeIndex: number | undefined\n ) {\n super._$initialize(part, parent, attributeIndex);\n addDisconnectableToParent(this);\n this.isConnected = part._$isConnected;\n }\n // This property needs to remain unminified.\n /**\n * Called from the core code when a directive is going away from a part (in\n * which case `shouldRemoveFromParent` should be true), and from the\n * `setChildrenConnected` helper function when recursively changing the\n * connection state of a tree (in which case `shouldRemoveFromParent` should\n * be false).\n *\n * @param isConnected\n * @param isClearingDirective - True when the directive itself is being\n * removed; false when the tree is being disconnected\n * @internal\n */\n override ['_$notifyDirectiveConnectionChanged'](\n isConnected: boolean,\n isClearingDirective = true\n ) {\n if (isConnected !== this.isConnected) {\n this.isConnected = isConnected;\n if (isConnected) {\n this.reconnected?.();\n } else {\n this.disconnected?.();\n }\n }\n if (isClearingDirective) {\n notifyChildrenConnectedChanged(this, isConnected);\n removeDisconnectableFromParent(this);\n }\n }\n\n /**\n * Sets the value of the directive's Part outside the normal `update`/`render`\n * lifecycle of a directive.\n *\n * This method should not be called synchronously from a directive's `update`\n * or `render`.\n *\n * @param directive The directive to update\n * @param value The value to set\n */\n setValue(value: unknown) {\n if (isSingleExpression(this.__part as unknown as PartInfo)) {\n this.__part._$setValue(value, this);\n } else {\n // this.__attributeIndex will be defined in this case, but\n // assert it in dev mode\n if (DEV_MODE && this.__attributeIndex === undefined) {\n throw new Error(`Expected this.__attributeIndex to be a number`);\n }\n const newValues = [...(this.__part._$committedValue as Array<unknown>)];\n newValues[this.__attributeIndex!] = value;\n (this.__part as AttributePart)._$setValue(newValues, this, 0);\n }\n }\n\n /**\n * User callbacks for implementing logic to release any resources/subscriptions\n * that may have been retained by this directive. Since directives may also be\n * re-connected, `reconnected` should also be implemented to restore the\n * working state of the directive prior to the next render.\n */\n protected disconnected() {}\n protected reconnected() {}\n}\n"],"names":["notifyChildrenConnectedChanged","parent","isConnected","children","_$disconnectableChildren","undefined","obj","removeDisconnectableFromParent","_$parent","delete","size","addDisconnectableToParent","Set","has","add","installDisconnectAPI","reparentDisconnectables","newParent","this","notifyChildPartConnectedChanged","isClearingValue","fromPartIndex","value","_$committedValue","Array","isArray","i","length","type","PartType","CHILD","_$notifyConnectionChanged","_$reparentDisconnectables","AsyncDirective","Directive","constructor","_$initialize","part","attributeIndex","super","_$isConnected","_$AO","isClearingDirective","reconnected","disconnected","setValue","isSingleExpression","__part","_$setValue","newValues","__attributeIndex"],"mappings":";;;;;GAsIA,MAAMA,EAAiC,CACrCC,EACAC,aAEA,MAAMC,EAAWF,EAAOG,KACxB,QAAiBC,IAAbF,EACF,OAAO,EAET,IAAK,MAAMG,KAAOH,eASfG,GAA2D,4BAC1DJ,GACA,GAGFF,EAA+BM,EAAKJ,GAEtC,OAAO,GASHK,EAAkCD,IACtC,IAAIL,EAAQE,EACZ,EAAG,CACD,QAAgCE,KAA3BJ,EAASK,EAAIE,MAChB,MAEFL,EAAWF,EAAOG,KAClBD,EAASM,OAAOH,GAChBA,EAAML,QACoB,KAAnBE,MAAAA,SAAAA,EAAUO,QAGfC,EAA6BL,IAGjC,IAAK,IAAIL,EAASA,EAASK,EAAIE,KAAWF,EAAML,EAAQ,CACtD,IAAIE,EAAWF,EAAOG,KACtB,QAAiBC,IAAbF,EACFF,EAAOG,KAA2BD,EAAW,IAAIS,SAC5C,GAAIT,EAASU,IAAIP,GAGtB,MAEFH,EAASW,IAAIR,GACbS,EAAqBd,KAWzB,SAASe,EAAyCC,QACVZ,IAAlCa,KAAKd,MACPG,EAA+BW,MAC/BA,KAAKV,KAAWS,EAChBN,EAA0BO,OAE1BA,KAAKV,KAAWS,EAyBpB,SAASE,EAEPjB,EACAkB,GAAkB,EAClBC,EAAgB,GAEhB,MAAMC,EAAQJ,KAAKK,KACbpB,EAAWe,KAAKd,KACtB,QAAiBC,IAAbF,GAA4C,IAAlBA,EAASO,KAGvC,GAAIU,EACF,GAAII,MAAMC,QAAQH,GAIhB,IAAK,IAAII,EAAIL,EAAeK,EAAIJ,EAAMK,OAAQD,IAC5C1B,EAA+BsB,EAAMI,IAAI,GACzCnB,EAA+Be,EAAMI,SAErB,MAATJ,IAITtB,EAA+BsB,GAAyB,GACxDf,EAA+Be,SAGjCtB,EAA+BkB,KAAMhB,GAOzC,MAAMa,EAAwBT,gBACvBA,EAAkBsB,MAAQC,EAASC,qBACrCxB,GAAkByB,sBAAAA,KACjBZ,gBACDb,GAAkB0B,sBAAAA,KAA8BhB,WAqB/BiB,UAAuBC,EAA7CC,kCAYWjB,eAAiDb,EAOjD+B,KACPC,EACApC,EACAqC,GAEAC,MAAMH,KAAaC,EAAMpC,EAAQqC,GACjC3B,EAA0BO,MAC1BA,KAAKhB,YAAcmC,EAAKG,KAejBC,KACPvC,EACAwC,GAAsB,WAElBxC,IAAgBgB,KAAKhB,cACvBgB,KAAKhB,YAAcA,EACfA,YACFgB,KAAKyB,iCAALzB,gBAEAA,KAAK0B,kCAAL1B,OAGAwB,IACF1C,EAA+BkB,KAAMhB,GACrCK,EAA+BW,OAcnC2B,SAASvB,GACP,GAAIwB,EAAmB5B,KAAK6B,MAC1B7B,KAAK6B,KAAOC,KAAW1B,EAAOJ,UACzB,CAML,MAAM+B,EAAY,IAAK/B,KAAK6B,KAAOxB,MACnC0B,EAAU/B,KAAKgC,MAAqB5B,EACnCJ,KAAK6B,KAAyBC,KAAWC,EAAW/B,KAAM,IAUrD0B,gBACAD"}
\No newline at end of file