async-directive.js 10 KB

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