From 11649d7211424c5587c1f9239e1f53c09f72f9da Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Wed, 20 Jan 2021 11:53:52 -0500 Subject: [PATCH 1/9] Fire callbacks when targets are added or removed Closes [hotwired/stimulus#336][] --- Implements the `TargetObserver` to monitor when elements declaring `[data-${identifier}-target]` are added or removed from a `Context` _after_ being connected and _before_ being disconnected. In support of iterating through target tokens, export the `TokenListObserver` module's `parseTokenString` function. [hotwired/stimulus#336]: https://github.com/hotwired/stimulus/issues/336 --- docs/reference/targets.md | 23 +++++ packages/@stimulus/core/src/context.ts | 7 ++ packages/@stimulus/core/src/controller.ts | 1 + .../@stimulus/core/src/target_observer.ts | 50 +++++++++++ .../@stimulus/core/src/target_properties.ts | 5 +- .../tests/controllers/target_controller.ts | 20 +++++ .../core/src/tests/modules/target_tests.ts | 90 ++++++++++++++++++- .../controllers/content_loader_controller.js | 9 ++ packages/@stimulus/examples/server.js | 2 +- .../src/token_list_observer.ts | 2 +- 10 files changed, 204 insertions(+), 5 deletions(-) create mode 100644 packages/@stimulus/core/src/target_observer.ts diff --git a/docs/reference/targets.md b/docs/reference/targets.md index af055cf4..2ebab372 100644 --- a/docs/reference/targets.md +++ b/docs/reference/targets.md @@ -88,6 +88,29 @@ if (this.hasResultsTarget) { } ``` +## Addition and Removal Callbacks + +Target _element callbacks_ let you respond whenever a target element is added or +removed within the controller's element. + +Define a method `[name]TargetAdded` or `[name]TargetRemoved` in the controller, where `[name]` is the name of the target you want to observe for additions or removals. The method receives the element as the first argument. + +Stimulus invokes each element callback any time its target elements are added or removed after `connect()` and before `disconnect()` lifecycle hooks. + +```js +export default class extends Controller { + static targets = [ "input" ] + + inputTargetAdded(element) { + element.classList.add("added-animation") + } + + inputTargetRemoved(element) { + element.classList.add("removed-animation") + } +} +``` + ## Naming Conventions Always use camelCase to specify target names, since they map directly to properties on your controller. diff --git a/packages/@stimulus/core/src/context.ts b/packages/@stimulus/core/src/context.ts index 20ca701f..1af34422 100644 --- a/packages/@stimulus/core/src/context.ts +++ b/packages/@stimulus/core/src/context.ts @@ -7,6 +7,7 @@ import { Module } from "./module" import { Schema } from "./schema" import { Scope } from "./scope" import { ValueObserver } from "./value_observer" +import { TargetObserver } from "./target_observer" export class Context implements ErrorHandler { readonly module: Module @@ -14,6 +15,7 @@ export class Context implements ErrorHandler { readonly controller: Controller private bindingObserver: BindingObserver private valueObserver: ValueObserver + private targetObserver: TargetObserver constructor(module: Module, scope: Scope) { this.module = module @@ -21,6 +23,7 @@ export class Context implements ErrorHandler { this.controller = new module.controllerConstructor(this) this.bindingObserver = new BindingObserver(this, this.dispatcher) this.valueObserver = new ValueObserver(this, this.controller) + this.targetObserver = new TargetObserver(this, this.controller) try { this.controller.initialize() @@ -32,9 +35,11 @@ export class Context implements ErrorHandler { connect() { this.bindingObserver.start() this.valueObserver.start() + this.targetObserver.start() try { this.controller.connect() + this.controller.isConnected = true } catch (error) { this.handleError(error, "connecting controller") } @@ -43,10 +48,12 @@ export class Context implements ErrorHandler { disconnect() { try { this.controller.disconnect() + this.controller.isConnected = false } catch (error) { this.handleError(error, "disconnecting controller") } + this.targetObserver.stop() this.valueObserver.stop() this.bindingObserver.stop() } diff --git a/packages/@stimulus/core/src/controller.ts b/packages/@stimulus/core/src/controller.ts index 29233bfb..2293cf00 100644 --- a/packages/@stimulus/core/src/controller.ts +++ b/packages/@stimulus/core/src/controller.ts @@ -12,6 +12,7 @@ export class Controller { static values: ValueDefinitionMap = {} readonly context: Context + isConnected = false constructor(context: Context) { this.context = context diff --git a/packages/@stimulus/core/src/target_observer.ts b/packages/@stimulus/core/src/target_observer.ts new file mode 100644 index 00000000..4f74b5a7 --- /dev/null +++ b/packages/@stimulus/core/src/target_observer.ts @@ -0,0 +1,50 @@ +import { Context } from "./context" +import { Controller } from "./controller" +import { TokenListObserver, TokenListObserverDelegate, Token, parseTokenString } from "@stimulus/mutation-observers" + +export class TargetObserver implements TokenListObserverDelegate { + readonly context: Context + readonly controller: Controller + readonly attributeName: string + private tokenListObserver: TokenListObserver + + constructor(context: Context, controller: Controller) { + this.context = context + this.controller = controller + this.attributeName = `data-${context.identifier}-target` + this.tokenListObserver = new TokenListObserver(this.context.element, this.attributeName, this) + } + + start() { + this.tokenListObserver.start() + } + + stop() { + this.tokenListObserver.stop() + } + + tokenMatched(token: Token): void { + if (this.controller.isConnected && this.containsDescendantWithToken(token.element, token.content)) { + this.dispatchCallback(`${token.content}TargetAdded`, token.element) + } + } + + tokenUnmatched(token: Token): void { + if (this.controller.isConnected && !this.containsDescendantWithToken(token.element, token.content)) { + this.dispatchCallback(`${token.content}TargetRemoved`, token.element) + } + } + + private containsDescendantWithToken(element: Element, content: string): boolean { + const targetTokens = parseTokenString(element.getAttribute(this.attributeName) || "", element, this.attributeName) + + return targetTokens.map(token => token.content).includes(content) && this.context.element.contains(element) + } + + private dispatchCallback(method: string, element: Element) { + const callback = (this.controller as any)[method] + if (typeof callback == "function") { + callback.call(this.controller, element) + } + } +} diff --git a/packages/@stimulus/core/src/target_properties.ts b/packages/@stimulus/core/src/target_properties.ts index 8aad1af6..f56e0705 100644 --- a/packages/@stimulus/core/src/target_properties.ts +++ b/packages/@stimulus/core/src/target_properties.ts @@ -33,6 +33,9 @@ function propertiesForTargetDefinition(name: string) { get(this: Controller) { return this.targets.has(name) } - } + }, + + [`${name}TargetAdded`]: (element: Element) => {}, + [`${name}TargetRemoved`]: (element: Element) => {}, } } diff --git a/packages/@stimulus/core/src/tests/controllers/target_controller.ts b/packages/@stimulus/core/src/tests/controllers/target_controller.ts index 4394b098..f0a3a9dd 100644 --- a/packages/@stimulus/core/src/tests/controllers/target_controller.ts +++ b/packages/@stimulus/core/src/tests/controllers/target_controller.ts @@ -9,7 +9,9 @@ class BaseTargetController extends Controller { } export class TargetController extends BaseTargetController { + static classes = [ "added", "removed" ] static targets = [ "beta", "input" ] + static values = { inputTargetAddedCallCount: Number, inputTargetRemovedCallCount: Number } betaTarget!: Element | null betaTargets!: Element[] @@ -18,4 +20,22 @@ export class TargetController extends BaseTargetController { inputTarget!: Element | null inputTargets!: Element[] hasInputTarget!: boolean + + hasAddedClass!: boolean + hasRemovedClass!: boolean + addedClass!: string + removedClass!: string + + inputTargetAddedCallCountValue = 0 + inputTargetRemovedCallCountValue = 0 + + inputTargetAdded(element: Element) { + this.inputTargetAddedCallCountValue++ + if (this.hasAddedClass) element.classList.add(this.addedClass) + } + + inputTargetRemoved(element: Element) { + this.inputTargetRemovedCallCountValue++ + if (this.hasRemovedClass) element.classList.add(this.removedClass) + } } diff --git a/packages/@stimulus/core/src/tests/modules/target_tests.ts b/packages/@stimulus/core/src/tests/modules/target_tests.ts index be2f3570..37c035d2 100644 --- a/packages/@stimulus/core/src/tests/modules/target_tests.ts +++ b/packages/@stimulus/core/src/tests/modules/target_tests.ts @@ -3,7 +3,7 @@ import { TargetController } from "../controllers/target_controller" export default class TargetTests extends ControllerTestCase(TargetController) { fixtureHTML = ` -
+
@@ -12,7 +12,7 @@ export default class TargetTests extends ControllerTestCase(TargetController) {
- +
` @@ -62,4 +62,90 @@ export default class TargetTests extends ControllerTestCase(TargetController) { this.assert.equal(this.controller.betaTargets.length, 0) this.assert.throws(() => this.controller.betaTarget) } + + "test target added callback fires after connect()"() { + const addedInputs = this.controller.inputTargets.filter(target => target.classList.contains("added")) + + this.assert.equal(addedInputs.length, 0) + this.assert.equal(this.controller.inputTargetAddedCallCountValue, 0) + } + + async "test target added callback when element is inserted"() { + const addedInput = document.createElement("input") + addedInput.setAttribute(`data-${this.controller.identifier}-target`, "input") + + this.controller.element.appendChild(addedInput) + await this.nextFrame + + this.assert.equal(this.controller.inputTargetAddedCallCountValue, 1) + this.assert.ok(addedInput.classList.contains("added"), `expected "${addedInput.className}" to contain "added"`) + this.assert.ok(addedInput.isConnected, "element is present in document") + } + + async "test target added callback when present element adds the target attribute"() { + const element = this.findElement("#child") + + element.setAttribute(`data-${this.controller.identifier}-target`, "input") + await this.nextFrame + + this.assert.equal(this.controller.inputTargetAddedCallCountValue, 1) + this.assert.ok(element.classList.contains("added"), `expected "${element.className}" to contain "added"`) + this.assert.ok(element.isConnected, "element is still present in document") + } + + async "test target added callback when present element adds a token to an existing target attribute"() { + const element = this.findElement("#alpha1") + + element.setAttribute(`data-${this.controller.identifier}-target`, "alpha input") + await this.nextFrame + + this.assert.equal(this.controller.inputTargetAddedCallCountValue, 1) + this.assert.ok(element.classList.contains("added"), `expected "${element.className}" to contain "added"`) + this.assert.ok(element.isConnected, "element is still present in document") + } + + async "test target remove callback fires before disconnect()"() { + const inputs = this.controller.inputTargets + + this.controller.disconnect() + await this.nextFrame + + const removedInputs = inputs.filter(target => target.classList.contains("removed")) + + this.assert.equal(removedInputs.length, 0) + this.assert.equal(this.controller.inputTargetRemovedCallCountValue, 0) + } + + async "test target removed callback when element is removed"() { + const removedInput = this.findElement("#input1") + + removedInput.remove() + await this.nextFrame + + this.assert.equal(this.controller.inputTargetRemovedCallCountValue, 1) + this.assert.ok(removedInput.classList.contains("removed"), `expected "${removedInput.className}" to contain "removed"`) + this.assert.notOk(removedInput.isConnected, "element is not present in document") + } + + async "test target removed callback when an element present in the document removes the target attribute"() { + const element = this.findElement("#input1") + + element.removeAttribute(`data-${this.controller.identifier}-target`) + await this.nextFrame + + this.assert.equal(this.controller.inputTargetRemovedCallCountValue, 1) + this.assert.ok(element.classList.contains("removed"), `expected "${element.className}" to contain "removed"`) + this.assert.ok(element.isConnected, "element is still present in document") + } + + async "test target removed callback does not fire when the target name is present after the attribute change"() { + const element = this.findElement("#input1") + + element.setAttribute(`data-${this.controller.identifier}-target`, "input") + await this.nextFrame + + this.assert.equal(this.controller.inputTargetRemovedCallCountValue, 0) + this.assert.notOk(element.classList.contains("removed"), `expected "${element.className}" not to contain "removed"`) + this.assert.ok(element.isConnected, "element is still present in document") + } } diff --git a/packages/@stimulus/examples/controllers/content_loader_controller.js b/packages/@stimulus/examples/controllers/content_loader_controller.js index b83d1661..f7e88204 100644 --- a/packages/@stimulus/examples/controllers/content_loader_controller.js +++ b/packages/@stimulus/examples/controllers/content_loader_controller.js @@ -1,6 +1,7 @@ import { Controller } from "stimulus" export default class extends Controller { + static targets = ["item"] static values = { url: String, refreshInterval: Number } connect() { @@ -11,6 +12,14 @@ export default class extends Controller { } } + itemTargetAdded(target) { + console.log("itemTargetAdded:", target) + } + + itemTargetRemoved(target) { + console.log("itemTargetRemoved:", target) + } + disconnect() { this.stopRefreshing() } diff --git a/packages/@stimulus/examples/server.js b/packages/@stimulus/examples/server.js index 9c4a1a6e..85084218 100644 --- a/packages/@stimulus/examples/server.js +++ b/packages/@stimulus/examples/server.js @@ -29,7 +29,7 @@ app.get("/", (req, res) => { }) app.get("/uptime", (req, res, next) => { - res.send(process.uptime().toString()) + res.send(`${process.uptime().toString()}`) }) app.get("/:page", (req, res, next) => { diff --git a/packages/@stimulus/mutation-observers/src/token_list_observer.ts b/packages/@stimulus/mutation-observers/src/token_list_observer.ts index 382f3344..0febb3c3 100644 --- a/packages/@stimulus/mutation-observers/src/token_list_observer.ts +++ b/packages/@stimulus/mutation-observers/src/token_list_observer.ts @@ -102,7 +102,7 @@ export class TokenListObserver implements AttributeObserverDelegate { } } -function parseTokenString(tokenString: string, element: Element, attributeName: string): Token[] { +export function parseTokenString(tokenString: string, element: Element, attributeName: string): Token[] { return tokenString.trim().split(/\s+/).filter(content => content.length) .map((content, index) => ({ element, attributeName, content, index })) } From 78c094b0723e81d6a0226762d88592643563b21a Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Fri, 26 Mar 2021 21:42:36 -0400 Subject: [PATCH 2/9] attempt to fix CI replace Array function with a for loop --- packages/@stimulus/core/src/target_observer.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/@stimulus/core/src/target_observer.ts b/packages/@stimulus/core/src/target_observer.ts index 4f74b5a7..777288de 100644 --- a/packages/@stimulus/core/src/target_observer.ts +++ b/packages/@stimulus/core/src/target_observer.ts @@ -36,9 +36,13 @@ export class TargetObserver implements TokenListObserverDelegate { } private containsDescendantWithToken(element: Element, content: string): boolean { - const targetTokens = parseTokenString(element.getAttribute(this.attributeName) || "", element, this.attributeName) + let containsTargetToken = false - return targetTokens.map(token => token.content).includes(content) && this.context.element.contains(element) + for (const token of parseTokenString(element.getAttribute(this.attributeName) || "", element, this.attributeName)) { + containsTargetToken = containsTargetToken || token.content == content + } + + return containsTargetToken && this.context.element.contains(element) } private dispatchCallback(method: string, element: Element) { From 86720fc7bea0c2f976745ef0583c8debddb0691f Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Wed, 7 Apr 2021 21:12:41 -0400 Subject: [PATCH 3/9] Rename Added to Connected and Removed to Disconnected --- docs/reference/targets.md | 6 +++--- packages/@stimulus/core/src/target_observer.ts | 4 ++-- packages/@stimulus/core/src/target_properties.ts | 3 --- .../src/tests/controllers/target_controller.ts | 14 +++++++------- .../core/src/tests/modules/target_tests.ts | 16 ++++++++-------- 5 files changed, 20 insertions(+), 23 deletions(-) diff --git a/docs/reference/targets.md b/docs/reference/targets.md index 2ebab372..68fcc48e 100644 --- a/docs/reference/targets.md +++ b/docs/reference/targets.md @@ -93,7 +93,7 @@ if (this.hasResultsTarget) { Target _element callbacks_ let you respond whenever a target element is added or removed within the controller's element. -Define a method `[name]TargetAdded` or `[name]TargetRemoved` in the controller, where `[name]` is the name of the target you want to observe for additions or removals. The method receives the element as the first argument. +Define a method `[name]TargetConnected` or `[name]TargetDisconnected` in the controller, where `[name]` is the name of the target you want to observe for additions or removals. The method receives the element as the first argument. Stimulus invokes each element callback any time its target elements are added or removed after `connect()` and before `disconnect()` lifecycle hooks. @@ -101,11 +101,11 @@ Stimulus invokes each element callback any time its target elements are added or export default class extends Controller { static targets = [ "input" ] - inputTargetAdded(element) { + inputTargetConnected(element) { element.classList.add("added-animation") } - inputTargetRemoved(element) { + inputTargetDisconnected(element) { element.classList.add("removed-animation") } } diff --git a/packages/@stimulus/core/src/target_observer.ts b/packages/@stimulus/core/src/target_observer.ts index 777288de..12165a28 100644 --- a/packages/@stimulus/core/src/target_observer.ts +++ b/packages/@stimulus/core/src/target_observer.ts @@ -25,13 +25,13 @@ export class TargetObserver implements TokenListObserverDelegate { tokenMatched(token: Token): void { if (this.controller.isConnected && this.containsDescendantWithToken(token.element, token.content)) { - this.dispatchCallback(`${token.content}TargetAdded`, token.element) + this.dispatchCallback(`${token.content}TargetConnected`, token.element) } } tokenUnmatched(token: Token): void { if (this.controller.isConnected && !this.containsDescendantWithToken(token.element, token.content)) { - this.dispatchCallback(`${token.content}TargetRemoved`, token.element) + this.dispatchCallback(`${token.content}TargetDisconnected`, token.element) } } diff --git a/packages/@stimulus/core/src/target_properties.ts b/packages/@stimulus/core/src/target_properties.ts index f56e0705..8936163c 100644 --- a/packages/@stimulus/core/src/target_properties.ts +++ b/packages/@stimulus/core/src/target_properties.ts @@ -34,8 +34,5 @@ function propertiesForTargetDefinition(name: string) { return this.targets.has(name) } }, - - [`${name}TargetAdded`]: (element: Element) => {}, - [`${name}TargetRemoved`]: (element: Element) => {}, } } diff --git a/packages/@stimulus/core/src/tests/controllers/target_controller.ts b/packages/@stimulus/core/src/tests/controllers/target_controller.ts index f0a3a9dd..e7e2e0b1 100644 --- a/packages/@stimulus/core/src/tests/controllers/target_controller.ts +++ b/packages/@stimulus/core/src/tests/controllers/target_controller.ts @@ -11,7 +11,7 @@ class BaseTargetController extends Controller { export class TargetController extends BaseTargetController { static classes = [ "added", "removed" ] static targets = [ "beta", "input" ] - static values = { inputTargetAddedCallCount: Number, inputTargetRemovedCallCount: Number } + static values = { inputTargetConnectedCallCount: Number, inputTargetDisconnectedCallCount: Number } betaTarget!: Element | null betaTargets!: Element[] @@ -26,16 +26,16 @@ export class TargetController extends BaseTargetController { addedClass!: string removedClass!: string - inputTargetAddedCallCountValue = 0 - inputTargetRemovedCallCountValue = 0 + inputTargetConnectedCallCountValue = 0 + inputTargetDisconnectedCallCountValue = 0 - inputTargetAdded(element: Element) { - this.inputTargetAddedCallCountValue++ + inputTargetConnected(element: Element) { + this.inputTargetConnectedCallCountValue++ if (this.hasAddedClass) element.classList.add(this.addedClass) } - inputTargetRemoved(element: Element) { - this.inputTargetRemovedCallCountValue++ + inputTargetDisconnected(element: Element) { + this.inputTargetDisconnectedCallCountValue++ if (this.hasRemovedClass) element.classList.add(this.removedClass) } } diff --git a/packages/@stimulus/core/src/tests/modules/target_tests.ts b/packages/@stimulus/core/src/tests/modules/target_tests.ts index 37c035d2..0d649cca 100644 --- a/packages/@stimulus/core/src/tests/modules/target_tests.ts +++ b/packages/@stimulus/core/src/tests/modules/target_tests.ts @@ -67,7 +67,7 @@ export default class TargetTests extends ControllerTestCase(TargetController) { const addedInputs = this.controller.inputTargets.filter(target => target.classList.contains("added")) this.assert.equal(addedInputs.length, 0) - this.assert.equal(this.controller.inputTargetAddedCallCountValue, 0) + this.assert.equal(this.controller.inputTargetConnectedCallCountValue, 0) } async "test target added callback when element is inserted"() { @@ -77,7 +77,7 @@ export default class TargetTests extends ControllerTestCase(TargetController) { this.controller.element.appendChild(addedInput) await this.nextFrame - this.assert.equal(this.controller.inputTargetAddedCallCountValue, 1) + this.assert.equal(this.controller.inputTargetConnectedCallCountValue, 1) this.assert.ok(addedInput.classList.contains("added"), `expected "${addedInput.className}" to contain "added"`) this.assert.ok(addedInput.isConnected, "element is present in document") } @@ -88,7 +88,7 @@ export default class TargetTests extends ControllerTestCase(TargetController) { element.setAttribute(`data-${this.controller.identifier}-target`, "input") await this.nextFrame - this.assert.equal(this.controller.inputTargetAddedCallCountValue, 1) + this.assert.equal(this.controller.inputTargetConnectedCallCountValue, 1) this.assert.ok(element.classList.contains("added"), `expected "${element.className}" to contain "added"`) this.assert.ok(element.isConnected, "element is still present in document") } @@ -99,7 +99,7 @@ export default class TargetTests extends ControllerTestCase(TargetController) { element.setAttribute(`data-${this.controller.identifier}-target`, "alpha input") await this.nextFrame - this.assert.equal(this.controller.inputTargetAddedCallCountValue, 1) + this.assert.equal(this.controller.inputTargetConnectedCallCountValue, 1) this.assert.ok(element.classList.contains("added"), `expected "${element.className}" to contain "added"`) this.assert.ok(element.isConnected, "element is still present in document") } @@ -113,7 +113,7 @@ export default class TargetTests extends ControllerTestCase(TargetController) { const removedInputs = inputs.filter(target => target.classList.contains("removed")) this.assert.equal(removedInputs.length, 0) - this.assert.equal(this.controller.inputTargetRemovedCallCountValue, 0) + this.assert.equal(this.controller.inputTargetDisconnectedCallCountValue, 0) } async "test target removed callback when element is removed"() { @@ -122,7 +122,7 @@ export default class TargetTests extends ControllerTestCase(TargetController) { removedInput.remove() await this.nextFrame - this.assert.equal(this.controller.inputTargetRemovedCallCountValue, 1) + this.assert.equal(this.controller.inputTargetDisconnectedCallCountValue, 1) this.assert.ok(removedInput.classList.contains("removed"), `expected "${removedInput.className}" to contain "removed"`) this.assert.notOk(removedInput.isConnected, "element is not present in document") } @@ -133,7 +133,7 @@ export default class TargetTests extends ControllerTestCase(TargetController) { element.removeAttribute(`data-${this.controller.identifier}-target`) await this.nextFrame - this.assert.equal(this.controller.inputTargetRemovedCallCountValue, 1) + this.assert.equal(this.controller.inputTargetDisconnectedCallCountValue, 1) this.assert.ok(element.classList.contains("removed"), `expected "${element.className}" to contain "removed"`) this.assert.ok(element.isConnected, "element is still present in document") } @@ -144,7 +144,7 @@ export default class TargetTests extends ControllerTestCase(TargetController) { element.setAttribute(`data-${this.controller.identifier}-target`, "input") await this.nextFrame - this.assert.equal(this.controller.inputTargetRemovedCallCountValue, 0) + this.assert.equal(this.controller.inputTargetDisconnectedCallCountValue, 0) this.assert.notOk(element.classList.contains("removed"), `expected "${element.className}" not to contain "removed"`) this.assert.ok(element.isConnected, "element is still present in document") } From c182b82cda2d7ef26a2467fe8f145cfc14428cc0 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Wed, 7 Apr 2021 21:30:01 -0400 Subject: [PATCH 4/9] ADd documentation to the Lifecycle Callbacks page --- docs/reference/lifecycle_callbacks.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/reference/lifecycle_callbacks.md b/docs/reference/lifecycle_callbacks.md index 474f2b50..478fa2a2 100644 --- a/docs/reference/lifecycle_callbacks.md +++ b/docs/reference/lifecycle_callbacks.md @@ -27,7 +27,9 @@ Method | Invoked by Stimulus… ------------ | -------------------- initialize() | Once, when the controller is first instantiated connect() | Anytime the controller is connected to the DOM +[name]TargetConnected(target: Element) | Anytime a target is connected to the DOM disconnect() | Anytime the controller is disconnected from the DOM +[name]TargetDisconnected(target: Element) | Anytime a target is disconnected from the DOM ## Connection @@ -38,6 +40,15 @@ A controller is _connected_ to the document when both of the following condition When a controller becomes connected, Stimulus calls its `connect()` method. +### Targets + +A target is _connected_ to the document when both of the following conditions are true: + +* its element is present in the document as a descendant of its corresponding controller's element +* its identifier is present in the element's `data-{identifier}-target` attribute + +When a target becomes connected, Stimulus calls its controller's `[name]TargetConnected()` method, passing the target element as a parameter. + ## Disconnection A connected controller will later become _disconnected_ when either of the preceding conditions becomes false, such as under any of the following scenarios: @@ -50,12 +61,26 @@ A connected controller will later become _disconnected_ when either of the prece When a controller becomes disconnected, Stimulus calls its `disconnect()` method. +### Targets + +A connected target will later become _disconnected_ when either of the preceding conditions becomes false, such as under any of the following scenarios: + +* the element is explicitly removed from the document with `Node#removeChild()` or `ChildNode#remove()` +* one of the element's parent elements is removed from the document +* one of the element's parent elements has its contents replaced by `Element#innerHTML=` +* the element's `data-{identifier}-target` attribute is removed or modified +* the document installs a new `` element, such as during a Turbo page change + +When a target becomes disconnected, Stimulus calls its controller's `[name]TargetDisconnected()` method, passing the target element as a parameter. + ## Reconnection A disconnected controller may become connected again at a later time. When this happens, such as after removing the controller's element from the document and then re-attaching it, Stimulus will reuse the element's previous controller instance, calling its `connect()` method multiple times. +Similarly, a disconnected target may be connected again at a later time. Stimulus will invoke its controller's `[name]TargetConnected()` method multiple times. + ## Order and Timing Stimulus watches the page for changes asynchronously using the [DOM `MutationObserver` API](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver). From 770b2fd93d6110072a1c12df92ebda785c488d30 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Wed, 7 Apr 2021 21:45:50 -0400 Subject: [PATCH 5/9] Incorporate code review feedback Introduce the concept of a `TargetObserverDelegate`, then make `Context` implement the interface. --- packages/@stimulus/core/src/context.ts | 27 ++++-- packages/@stimulus/core/src/controller.ts | 1 - .../@stimulus/core/src/target_observer.ts | 86 ++++++++++++------ .../tests/controllers/target_controller.ts | 14 +-- .../core/src/tests/modules/target_tests.ts | 88 +++++++++++-------- packages/@stimulus/multimap/src/multimap.ts | 4 + .../src/token_list_observer.ts | 2 +- 7 files changed, 145 insertions(+), 77 deletions(-) diff --git a/packages/@stimulus/core/src/context.ts b/packages/@stimulus/core/src/context.ts index 1af34422..bddb51a6 100644 --- a/packages/@stimulus/core/src/context.ts +++ b/packages/@stimulus/core/src/context.ts @@ -7,9 +7,9 @@ import { Module } from "./module" import { Schema } from "./schema" import { Scope } from "./scope" import { ValueObserver } from "./value_observer" -import { TargetObserver } from "./target_observer" +import { TargetObserver, TargetObserverDelegate } from "./target_observer" -export class Context implements ErrorHandler { +export class Context implements ErrorHandler, TargetObserverDelegate { readonly module: Module readonly scope: Scope readonly controller: Controller @@ -23,7 +23,7 @@ export class Context implements ErrorHandler { this.controller = new module.controllerConstructor(this) this.bindingObserver = new BindingObserver(this, this.dispatcher) this.valueObserver = new ValueObserver(this, this.controller) - this.targetObserver = new TargetObserver(this, this.controller) + this.targetObserver = new TargetObserver(this, this) try { this.controller.initialize() @@ -39,7 +39,6 @@ export class Context implements ErrorHandler { try { this.controller.connect() - this.controller.isConnected = true } catch (error) { this.handleError(error, "connecting controller") } @@ -48,7 +47,6 @@ export class Context implements ErrorHandler { disconnect() { try { this.controller.disconnect() - this.controller.isConnected = false } catch (error) { this.handleError(error, "disconnecting controller") } @@ -89,4 +87,23 @@ export class Context implements ErrorHandler { detail = Object.assign({ identifier, controller, element }, detail) this.application.handleError(error, `Error ${message}`, detail) } + + // Target observer delegate + + targetConnected(element: Element, name: string) { + this.invokeControllerMethod(`${name}TargetConnected`, element) + } + + targetDisconnected(element: Element, name: string) { + this.invokeControllerMethod(`${name}TargetDisconnected`, element) + } + + // Private + + invokeControllerMethod(methodName: string, ...args: any[]) { + const controller: any = this.controller + if (typeof controller[methodName] == "function") { + controller[methodName](...args) + } + } } diff --git a/packages/@stimulus/core/src/controller.ts b/packages/@stimulus/core/src/controller.ts index 2293cf00..29233bfb 100644 --- a/packages/@stimulus/core/src/controller.ts +++ b/packages/@stimulus/core/src/controller.ts @@ -12,7 +12,6 @@ export class Controller { static values: ValueDefinitionMap = {} readonly context: Context - isConnected = false constructor(context: Context) { this.context = context diff --git a/packages/@stimulus/core/src/target_observer.ts b/packages/@stimulus/core/src/target_observer.ts index 12165a28..45b99b89 100644 --- a/packages/@stimulus/core/src/target_observer.ts +++ b/packages/@stimulus/core/src/target_observer.ts @@ -1,54 +1,86 @@ +import { Multimap } from "@stimulus/multimap" +import { Token, TokenListObserver, TokenListObserverDelegate } from "@stimulus/mutation-observers" import { Context } from "./context" -import { Controller } from "./controller" -import { TokenListObserver, TokenListObserverDelegate, Token, parseTokenString } from "@stimulus/mutation-observers" + +export interface TargetObserverDelegate { + targetConnected(element: Element, name: string): void + targetDisconnected(element: Element, name: string): void +} export class TargetObserver implements TokenListObserverDelegate { readonly context: Context - readonly controller: Controller - readonly attributeName: string - private tokenListObserver: TokenListObserver + readonly delegate: TargetObserverDelegate + readonly targetsByName: Multimap + private tokenListObserver?: TokenListObserver - constructor(context: Context, controller: Controller) { + constructor(context: Context, delegate: TargetObserverDelegate) { this.context = context - this.controller = controller - this.attributeName = `data-${context.identifier}-target` - this.tokenListObserver = new TokenListObserver(this.context.element, this.attributeName, this) + this.delegate = delegate + this.targetsByName = new Multimap } start() { - this.tokenListObserver.start() + if (!this.tokenListObserver) { + this.tokenListObserver = new TokenListObserver(this.element, this.attributeName, this) + this.tokenListObserver.start() + } } stop() { - this.tokenListObserver.stop() + if (this.tokenListObserver) { + this.disconnectAllTargets() + this.tokenListObserver.stop() + delete this.tokenListObserver + } } - tokenMatched(token: Token): void { - if (this.controller.isConnected && this.containsDescendantWithToken(token.element, token.content)) { - this.dispatchCallback(`${token.content}TargetConnected`, token.element) + // Token list observer delegate + + tokenMatched({ element, content: name }: Token) { + if (this.scope.containsElement(element)) { + this.connectTarget(element, name) } } - tokenUnmatched(token: Token): void { - if (this.controller.isConnected && !this.containsDescendantWithToken(token.element, token.content)) { - this.dispatchCallback(`${token.content}TargetDisconnected`, token.element) - } + tokenUnmatched({ element, content: name }: Token) { + this.disconnectTarget(element, name) } - private containsDescendantWithToken(element: Element, content: string): boolean { - let containsTargetToken = false + // Target management - for (const token of parseTokenString(element.getAttribute(this.attributeName) || "", element, this.attributeName)) { - containsTargetToken = containsTargetToken || token.content == content + connectTarget(element: Element, name: string) { + if (!this.targetsByName.has(name, element)) { + this.targetsByName.add(name, element) + this.delegate.targetConnected(element, name) } + } - return containsTargetToken && this.context.element.contains(element) + disconnectTarget(element: Element, name: string) { + if (this.targetsByName.has(name, element)) { + this.targetsByName.delete(name, element) + this.delegate.targetDisconnected(element, name) + } } - private dispatchCallback(method: string, element: Element) { - const callback = (this.controller as any)[method] - if (typeof callback == "function") { - callback.call(this.controller, element) + disconnectAllTargets() { + for (const name of this.targetsByName.keys) { + for (const element of this.targetsByName.getValuesForKey(name)) { + this.disconnectTarget(element, name) + } } } + + // Private + + private get attributeName() { + return `data-${this.context.identifier}-target` + } + + private get element() { + return this.context.element + } + + private get scope() { + return this.context.scope + } } diff --git a/packages/@stimulus/core/src/tests/controllers/target_controller.ts b/packages/@stimulus/core/src/tests/controllers/target_controller.ts index e7e2e0b1..997bf22e 100644 --- a/packages/@stimulus/core/src/tests/controllers/target_controller.ts +++ b/packages/@stimulus/core/src/tests/controllers/target_controller.ts @@ -9,7 +9,7 @@ class BaseTargetController extends Controller { } export class TargetController extends BaseTargetController { - static classes = [ "added", "removed" ] + static classes = [ "connected", "disconnected" ] static targets = [ "beta", "input" ] static values = { inputTargetConnectedCallCount: Number, inputTargetDisconnectedCallCount: Number } @@ -21,21 +21,21 @@ export class TargetController extends BaseTargetController { inputTargets!: Element[] hasInputTarget!: boolean - hasAddedClass!: boolean - hasRemovedClass!: boolean - addedClass!: string - removedClass!: string + hasConnectedClass!: boolean + hasDisconnectedClass!: boolean + connectedClass!: string + disconnectedClass!: string inputTargetConnectedCallCountValue = 0 inputTargetDisconnectedCallCountValue = 0 inputTargetConnected(element: Element) { + if (this.hasConnectedClass) element.classList.add(this.connectedClass) this.inputTargetConnectedCallCountValue++ - if (this.hasAddedClass) element.classList.add(this.addedClass) } inputTargetDisconnected(element: Element) { + if (this.hasDisconnectedClass) element.classList.add(this.disconnectedClass) this.inputTargetDisconnectedCallCountValue++ - if (this.hasRemovedClass) element.classList.add(this.removedClass) } } diff --git a/packages/@stimulus/core/src/tests/modules/target_tests.ts b/packages/@stimulus/core/src/tests/modules/target_tests.ts index 0d649cca..c4ddaa44 100644 --- a/packages/@stimulus/core/src/tests/modules/target_tests.ts +++ b/packages/@stimulus/core/src/tests/modules/target_tests.ts @@ -3,7 +3,7 @@ import { TargetController } from "../controllers/target_controller" export default class TargetTests extends ControllerTestCase(TargetController) { fixtureHTML = ` -
+
@@ -63,89 +63,105 @@ export default class TargetTests extends ControllerTestCase(TargetController) { this.assert.throws(() => this.controller.betaTarget) } - "test target added callback fires after connect()"() { - const addedInputs = this.controller.inputTargets.filter(target => target.classList.contains("added")) + "test target connected callback fires after initialize() and when calling connect()"() { + const connectedInputs = this.controller.inputTargets.filter(target => target.classList.contains("connected")) - this.assert.equal(addedInputs.length, 0) - this.assert.equal(this.controller.inputTargetConnectedCallCountValue, 0) + this.assert.equal(connectedInputs.length, 1) + this.assert.equal(this.controller.inputTargetConnectedCallCountValue, 1) } - async "test target added callback when element is inserted"() { - const addedInput = document.createElement("input") - addedInput.setAttribute(`data-${this.controller.identifier}-target`, "input") + async "test target connected callback when element is inserted"() { + const connectedInput = document.createElement("input") + connectedInput.setAttribute(`data-${this.controller.identifier}-target`, "input") + + this.assert.equal(this.controller.inputTargetConnectedCallCountValue, 1) - this.controller.element.appendChild(addedInput) + this.controller.element.appendChild(connectedInput) await this.nextFrame - this.assert.equal(this.controller.inputTargetConnectedCallCountValue, 1) - this.assert.ok(addedInput.classList.contains("added"), `expected "${addedInput.className}" to contain "added"`) - this.assert.ok(addedInput.isConnected, "element is present in document") + this.assert.equal(this.controller.inputTargetConnectedCallCountValue, 2) + this.assert.ok(connectedInput.classList.contains("connected"), `expected "${connectedInput.className}" to contain "connected"`) + this.assert.ok(connectedInput.isConnected, "element is present in document") } - async "test target added callback when present element adds the target attribute"() { - const element = this.findElement("#child") + async "test target connected callback when present element adds the target attribute"() { + const element = this.findElement("#alpha1") + + this.assert.equal(this.controller.inputTargetConnectedCallCountValue, 1) element.setAttribute(`data-${this.controller.identifier}-target`, "input") await this.nextFrame - this.assert.equal(this.controller.inputTargetConnectedCallCountValue, 1) - this.assert.ok(element.classList.contains("added"), `expected "${element.className}" to contain "added"`) + this.assert.equal(this.controller.inputTargetConnectedCallCountValue, 2) + this.assert.ok(element.classList.contains("connected"), `expected "${element.className}" to contain "connected"`) this.assert.ok(element.isConnected, "element is still present in document") } - async "test target added callback when present element adds a token to an existing target attribute"() { + async "test target connected callback when element adds a token to an existing target attribute"() { const element = this.findElement("#alpha1") + this.assert.equal(this.controller.inputTargetConnectedCallCountValue, 1) + element.setAttribute(`data-${this.controller.identifier}-target`, "alpha input") await this.nextFrame - this.assert.equal(this.controller.inputTargetConnectedCallCountValue, 1) - this.assert.ok(element.classList.contains("added"), `expected "${element.className}" to contain "added"`) + this.assert.equal(this.controller.inputTargetConnectedCallCountValue, 2) + this.assert.ok(element.classList.contains("connected"), `expected "${element.className}" to contain "connected"`) this.assert.ok(element.isConnected, "element is still present in document") } - async "test target remove callback fires before disconnect()"() { - const inputs = this.controller.inputTargets + async "test target disconnected callback fires when calling disconnect() on the controller"() { + this.assert.equal(this.controller.inputTargets.filter(target => target.classList.contains("disconnected")).length, 0) + this.assert.equal(this.controller.inputTargetDisconnectedCallCountValue, 0) - this.controller.disconnect() + this.controller.context.disconnect() await this.nextFrame - const removedInputs = inputs.filter(target => target.classList.contains("removed")) - - this.assert.equal(removedInputs.length, 0) - this.assert.equal(this.controller.inputTargetDisconnectedCallCountValue, 0) + this.assert.equal(this.controller.inputTargets.filter(target => target.classList.contains("disconnected")).length, 1) + this.assert.equal(this.controller.inputTargetDisconnectedCallCountValue, 1) } - async "test target removed callback when element is removed"() { - const removedInput = this.findElement("#input1") + async "test target disconnected callback when element is removed"() { + const disconnectedInput = this.findElement("#input1") - removedInput.remove() + this.assert.equal(this.controller.inputTargetDisconnectedCallCountValue, 0) + this.assert.notOk(disconnectedInput.classList.contains("disconnected"), `expected "${disconnectedInput.className}" not to contain "disconnected"`) + + disconnectedInput.remove() await this.nextFrame this.assert.equal(this.controller.inputTargetDisconnectedCallCountValue, 1) - this.assert.ok(removedInput.classList.contains("removed"), `expected "${removedInput.className}" to contain "removed"`) - this.assert.notOk(removedInput.isConnected, "element is not present in document") + this.assert.ok(disconnectedInput.classList.contains("disconnected"), `expected "${disconnectedInput.className}" to contain "disconnected"`) + this.assert.notOk(disconnectedInput.isConnected, "element is not present in document") } - async "test target removed callback when an element present in the document removes the target attribute"() { + async "test target disconnected callback when an element present in the document removes the target attribute"() { const element = this.findElement("#input1") + this.assert.equal(this.controller.inputTargetDisconnectedCallCountValue, 0) + this.assert.notOk(element.classList.contains("disconnected"), `expected "${element.className}" not to contain "disconnected"`) + element.removeAttribute(`data-${this.controller.identifier}-target`) await this.nextFrame this.assert.equal(this.controller.inputTargetDisconnectedCallCountValue, 1) - this.assert.ok(element.classList.contains("removed"), `expected "${element.className}" to contain "removed"`) + this.assert.ok(element.classList.contains("disconnected"), `expected "${element.className}" to contain "disconnected"`) this.assert.ok(element.isConnected, "element is still present in document") } - async "test target removed callback does not fire when the target name is present after the attribute change"() { + async "test target disconnected(), then connected() callback fired when the target name is present after the attribute change"() { const element = this.findElement("#input1") + this.assert.equal(this.controller.inputTargetConnectedCallCountValue, 1) + this.assert.equal(this.controller.inputTargetDisconnectedCallCountValue, 0) + this.assert.notOk(element.classList.contains("disconnected"), `expected "${element.className}" not to contain "disconnected"`) + element.setAttribute(`data-${this.controller.identifier}-target`, "input") await this.nextFrame - this.assert.equal(this.controller.inputTargetDisconnectedCallCountValue, 0) - this.assert.notOk(element.classList.contains("removed"), `expected "${element.className}" not to contain "removed"`) + this.assert.equal(this.controller.inputTargetConnectedCallCountValue, 2) + this.assert.equal(this.controller.inputTargetDisconnectedCallCountValue, 1) + this.assert.ok(element.classList.contains("disconnected"), `expected "${element.className}" to contain "disconnected"`) this.assert.ok(element.isConnected, "element is still present in document") } } diff --git a/packages/@stimulus/multimap/src/multimap.ts b/packages/@stimulus/multimap/src/multimap.ts index b5b0a202..b9595d43 100644 --- a/packages/@stimulus/multimap/src/multimap.ts +++ b/packages/@stimulus/multimap/src/multimap.ts @@ -7,6 +7,10 @@ export class Multimap { this.valuesByKey = new Map>() } + get keys() { + return Array.from(this.valuesByKey.keys()) + } + get values(): V[] { const sets = Array.from(this.valuesByKey.values()) return sets.reduce((values, set) => values.concat(Array.from(set)), []) diff --git a/packages/@stimulus/mutation-observers/src/token_list_observer.ts b/packages/@stimulus/mutation-observers/src/token_list_observer.ts index 0febb3c3..382f3344 100644 --- a/packages/@stimulus/mutation-observers/src/token_list_observer.ts +++ b/packages/@stimulus/mutation-observers/src/token_list_observer.ts @@ -102,7 +102,7 @@ export class TokenListObserver implements AttributeObserverDelegate { } } -export function parseTokenString(tokenString: string, element: Element, attributeName: string): Token[] { +function parseTokenString(tokenString: string, element: Element, attributeName: string): Token[] { return tokenString.trim().split(/\s+/).filter(content => content.length) .map((content, index) => ({ element, attributeName, content, index })) } From 21f2ca2c9f6e862aec83b19d433c9660e25536cd Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Thu, 8 Apr 2021 09:54:08 -0400 Subject: [PATCH 6/9] Touch up documentation --- docs/reference/targets.md | 13 ++++++++----- packages/@stimulus/core/src/target_properties.ts | 2 +- .../controllers/content_loader_controller.js | 8 ++++---- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/docs/reference/targets.md b/docs/reference/targets.md index 68fcc48e..7f0bb092 100644 --- a/docs/reference/targets.md +++ b/docs/reference/targets.md @@ -99,15 +99,18 @@ Stimulus invokes each element callback any time its target elements are added or ```js export default class extends Controller { - static targets = [ "input" ] + static targets = [ "item" ] - inputTargetConnected(element) { - element.classList.add("added-animation") + itemTargetConnected(element) { + this.sortElements(this.itemTargets) } - inputTargetDisconnected(element) { - element.classList.add("removed-animation") + itemTargetDisconnected(element) { + this.sortElements(this.itemTargets) } + + // Private + sortElements(itemTargets) { /* ... */ } } ``` diff --git a/packages/@stimulus/core/src/target_properties.ts b/packages/@stimulus/core/src/target_properties.ts index 8936163c..8aad1af6 100644 --- a/packages/@stimulus/core/src/target_properties.ts +++ b/packages/@stimulus/core/src/target_properties.ts @@ -33,6 +33,6 @@ function propertiesForTargetDefinition(name: string) { get(this: Controller) { return this.targets.has(name) } - }, + } } } diff --git a/packages/@stimulus/examples/controllers/content_loader_controller.js b/packages/@stimulus/examples/controllers/content_loader_controller.js index f7e88204..85766e8d 100644 --- a/packages/@stimulus/examples/controllers/content_loader_controller.js +++ b/packages/@stimulus/examples/controllers/content_loader_controller.js @@ -12,12 +12,12 @@ export default class extends Controller { } } - itemTargetAdded(target) { - console.log("itemTargetAdded:", target) + itemTargetConnected(target) { + console.log("itemTargetConnected:", target) } - itemTargetRemoved(target) { - console.log("itemTargetRemoved:", target) + itemTargetDisconnected(target) { + console.log("itemTargetDisconnected:", target) } disconnect() { From e6e1d122419d74c4dd22b0a98832d0e242910954 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Thu, 8 Apr 2021 11:14:12 -0400 Subject: [PATCH 7/9] Add `Node.isConnected` to polyfills The implementation was sourced from the [Mozilla Developer Network documentation][poyfill] for [Node.isConnected][] [poyfill]: https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected#polyfill [Node.isConnected]: https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected --- packages/@stimulus/polyfills/index.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/@stimulus/polyfills/index.js b/packages/@stimulus/polyfills/index.js index 5601543d..6721fdf7 100644 --- a/packages/@stimulus/polyfills/index.js +++ b/packages/@stimulus/polyfills/index.js @@ -15,3 +15,18 @@ if (typeof SVGElement.prototype.contains != "function") { return this === node || this.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_CONTAINED_BY } } + +// From https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected#polyfill +if (!("isConnected" in Node.prototype)) { + Object.defineProperty(Node.prototype, "isConnected", { + get() { + return ( + !this.ownerDocument || + !( + this.ownerDocument.compareDocumentPosition(this) & + this.DOCUMENT_POSITION_DISCONNECTED + ) + ) + } + }) +} From 1b6e95e0427e6642e2fed0d040899c7e10ccd63a Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Thu, 8 Apr 2021 12:21:19 -0400 Subject: [PATCH 8/9] Attempt to address CI failure ``` IE 11.0 (Windows 8.1) ERROR 133 Expected identifier, string or number 134 at @stimulus/core/dist/tests/index.js:2281:1 135 136 SyntaxError: Expected identifier, string or number 137 at ./packages/@stimulus/polyfills/index.js (@stimulus/core/dist/tests/index.js:2281:1) ``` ``` IE 11.0 (Windows 8.1) target disconnected callback when element is removed FAILED 125 Promise rejected during "target disconnected callback when element is removed": Object doesn't support property or method 'remove' 126 TypeError: Object doesn't support property or method 'remove' 127 at Anonymous function (eval code:191:25) ``` --- packages/@stimulus/core/src/tests/modules/target_tests.ts | 2 +- packages/@stimulus/polyfills/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@stimulus/core/src/tests/modules/target_tests.ts b/packages/@stimulus/core/src/tests/modules/target_tests.ts index c4ddaa44..f5524899 100644 --- a/packages/@stimulus/core/src/tests/modules/target_tests.ts +++ b/packages/@stimulus/core/src/tests/modules/target_tests.ts @@ -127,7 +127,7 @@ export default class TargetTests extends ControllerTestCase(TargetController) { this.assert.equal(this.controller.inputTargetDisconnectedCallCountValue, 0) this.assert.notOk(disconnectedInput.classList.contains("disconnected"), `expected "${disconnectedInput.className}" not to contain "disconnected"`) - disconnectedInput.remove() + disconnectedInput.parentElement?.removeChild(disconnectedInput) await this.nextFrame this.assert.equal(this.controller.inputTargetDisconnectedCallCountValue, 1) diff --git a/packages/@stimulus/polyfills/index.js b/packages/@stimulus/polyfills/index.js index 6721fdf7..6131e72f 100644 --- a/packages/@stimulus/polyfills/index.js +++ b/packages/@stimulus/polyfills/index.js @@ -19,7 +19,7 @@ if (typeof SVGElement.prototype.contains != "function") { // From https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected#polyfill if (!("isConnected" in Node.prototype)) { Object.defineProperty(Node.prototype, "isConnected", { - get() { + get: function() { return ( !this.ownerDocument || !( From ef175d592e63b6a95439a6a590e6ed371242d468 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Thu, 8 Apr 2021 12:51:41 -0400 Subject: [PATCH 9/9] Be consistent in documentation --- docs/reference/targets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/targets.md b/docs/reference/targets.md index 7f0bb092..b6dd6cfc 100644 --- a/docs/reference/targets.md +++ b/docs/reference/targets.md @@ -88,7 +88,7 @@ if (this.hasResultsTarget) { } ``` -## Addition and Removal Callbacks +## Connected and Disconnected Callbacks Target _element callbacks_ let you respond whenever a target element is added or removed within the controller's element.