From 7febfa787323b27a252f593f97555717b98ce7da Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Sat, 28 Jan 2023 00:47:13 +0100 Subject: [PATCH 1/6] Add fallback selector for outlets --- src/core/outlet_set.ts | 6 +++++- src/core/scope.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/core/outlet_set.ts b/src/core/outlet_set.ts index b8ca77b4..d32de2ca 100644 --- a/src/core/outlet_set.ts +++ b/src/core/outlet_set.ts @@ -41,7 +41,11 @@ export class OutletSet { getSelectorForOutletName(outletName: string) { const attributeName = this.schema.outletAttributeForScope(this.identifier, outletName) - return this.controllerElement.getAttribute(attributeName) + const selector = this.controllerElement.getAttribute(attributeName) + + if (selector) return selector + + return this.scope.controllerSelector } private findOutlet(outletName: string) { diff --git a/src/core/scope.ts b/src/core/scope.ts index de302807..2bae3e11 100644 --- a/src/core/scope.ts +++ b/src/core/scope.ts @@ -44,7 +44,7 @@ export class Scope { return Array.from(this.element.querySelectorAll(selector)) } - private get controllerSelector(): string { + get controllerSelector(): string { return attributeValueContainsToken(this.schema.controllerAttribute, this.identifier) } From 0b08bab9d4da5fba9e0d33edf719f1bca33447b5 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Sat, 28 Jan 2023 19:18:21 +0100 Subject: [PATCH 2/6] Add tests --- src/core/outlet_set.ts | 13 +- src/tests/controllers/outlet_controller.ts | 11 ++ .../core/outlet_fallback_selector_tests.ts | 116 ++++++++++++++++++ src/tests/modules/core/outlet_tests.ts | 1 + 4 files changed, 135 insertions(+), 6 deletions(-) create mode 100644 src/tests/modules/core/outlet_fallback_selector_tests.ts diff --git a/src/core/outlet_set.ts b/src/core/outlet_set.ts index d32de2ca..5f745fe2 100644 --- a/src/core/outlet_set.ts +++ b/src/core/outlet_set.ts @@ -42,10 +42,7 @@ export class OutletSet { getSelectorForOutletName(outletName: string) { const attributeName = this.schema.outletAttributeForScope(this.identifier, outletName) const selector = this.controllerElement.getAttribute(attributeName) - - if (selector) return selector - - return this.scope.controllerSelector + return selector || this.getControllerSelectorForOutletName(outletName) } private findOutlet(outletName: string) { @@ -69,7 +66,11 @@ export class OutletSet { } private matchesElement(element: Element, selector: string, outletName: string): boolean { - const controllerAttribute = element.getAttribute(this.scope.schema.controllerAttribute) || "" - return element.matches(selector) && controllerAttribute.split(" ").includes(outletName) + const controllerSelector = this.getControllerSelectorForOutletName(outletName) + return element.matches(selector) && element.matches(controllerSelector) + } + + private getControllerSelectorForOutletName(outletName: string) { + return `[${this.schema.controllerAttribute}~="${outletName}"]` } } diff --git a/src/tests/controllers/outlet_controller.ts b/src/tests/controllers/outlet_controller.ts index 261a4734..354ce265 100644 --- a/src/tests/controllers/outlet_controller.ts +++ b/src/tests/controllers/outlet_controller.ts @@ -31,6 +31,12 @@ export class OutletController extends BaseOutletController { betaOutletElements!: Element[] hasBetaOutlet!: boolean + gammaOutlet!: Controller | null + gammaOutlets!: Controller[] + gammaOutletElement!: Element | null + gammaOutletElements!: Element[] + hasGammaOutlet!: boolean + namespacedEpsilonOutlet!: Controller | null namespacedEpsilonOutlets!: Controller[] namespacedEpsilonOutletElement!: Element | null @@ -76,6 +82,11 @@ export class OutletController extends BaseOutletController { this.gammaOutletConnectedCallCountValue++ } + gammaOutletDisconnected(_outlet: Controller, element: Element) { + if (this.hasDisconnectedClass) element.classList.add(this.disconnectedClass) + this.gammaOutletDisconnectedCallCountValue++ + } + namespacedEpsilonOutletConnected(_outlet: Controller, element: Element) { if (this.hasConnectedClass) element.classList.add(this.connectedClass) this.namespacedEpsilonOutletConnectedCallCountValue++ diff --git a/src/tests/modules/core/outlet_fallback_selector_tests.ts b/src/tests/modules/core/outlet_fallback_selector_tests.ts new file mode 100644 index 00000000..753aed14 --- /dev/null +++ b/src/tests/modules/core/outlet_fallback_selector_tests.ts @@ -0,0 +1,116 @@ +import { ControllerTestCase } from "../../cases/controller_test_case" +import { OutletController } from "../../controllers/outlet_controller" + +export default class OutletFallbackSelectorTests extends ControllerTestCase(OutletController) { + fixtureHTML = ` +
+
+ +
+
+
+
+ +
+
+
+ ` + get identifiers() { + return ["test", "alpha", "beta", "gamma"] + } + + "test OutletSet#find"() { + this.assert.equal(this.controller.outlets.find("alpha"), this.findElement("#alpha1")) + this.assert.equal(this.controller.outlets.find("beta"), this.findElement("#beta1")) + this.assert.equal(this.controller.outlets.find("gamma"), undefined) + } + + "test OutletSet#findAll"() { + this.assert.deepEqual(this.controller.outlets.findAll("alpha"), this.findElements("#alpha1", "#mixed")) + this.assert.deepEqual(this.controller.outlets.findAll("beta"), this.findElements("#beta1", "#mixed", "#beta2")) + this.assert.deepEqual(this.controller.outlets.findAll("gamma"), []) + } + + "test OutletSet#findAll with multiple arguments"() { + this.assert.deepEqual( + this.controller.outlets.findAll("alpha", "beta"), + this.findElements("#alpha1", "#mixed", "#beta1", "#mixed", "#beta2") + ) + } + + "test OutletSet#has"() { + this.assert.equal(this.controller.outlets.has("alpha"), true) + this.assert.equal(this.controller.outlets.has("beta"), true) + this.assert.equal(this.controller.outlets.has("gamma"), false) + } + + async "test OutletSet#has when no element with selector exists"() { + const element = document.createElement("div") + element.setAttribute("data-controller", "gamma") + + this.assert.equal(this.controller.outlets.has("gamma"), false) + + this.controller.element.appendChild(element) + await this.nextFrame + + this.assert.equal(this.controller.outlets.has("gamma"), true) + + const gammaOutlets = this.controller.gammaOutletElements.filter((outlet) => outlet.classList.contains("connected")) + this.assert.equal(gammaOutlets.length, 1) + this.assert.equal(this.controller.gammaOutletConnectedCallCountValue, 1) + } + + "test singular linked outlet property throws an error when no outlet is found"() { + this.findElements("#alpha1", "#mixed", "#beta1", "#mixed", "#beta2").forEach((e) => e.remove()) + + this.assert.equal(this.controller.hasAlphaOutlet, false) + this.assert.equal(this.controller.alphaOutlets.length, 0) + this.assert.equal(this.controller.alphaOutletElements.length, 0) + this.assert.throws(() => this.controller.alphaOutlet) + this.assert.throws(() => this.controller.alphaOutletElement) + + this.assert.equal(this.controller.hasBetaOutlet, false) + this.assert.equal(this.controller.betaOutlets.length, 0) + this.assert.equal(this.controller.betaOutletElements.length, 0) + this.assert.throws(() => this.controller.betaOutlet) + this.assert.throws(() => this.controller.betaOutletElement) + } + + "test outlet connected callback fires"() { + const alphaOutlets = this.controller.alphaOutletElements.filter((outlet) => outlet.classList.contains("connected")) + const betaOutlets = this.controller.betaOutletElements.filter((outlet) => outlet.classList.contains("connected")) + const gammaOutlets = this.controller.gammaOutletElements.filter((outlet) => outlet.classList.contains("connected")) + + this.assert.equal(alphaOutlets.length, 2) + this.assert.equal(this.controller.alphaOutletConnectedCallCountValue, 2) + + this.assert.equal(betaOutlets.length, 3) + this.assert.equal(this.controller.betaOutletConnectedCallCountValue, 3) + + this.assert.equal(gammaOutlets.length, 0) + this.assert.equal(this.controller.gammaOutletConnectedCallCountValue, 0) + } + + async "test outlet disconnect callback fires"() { + this.findElements("#alpha1", "#mixed", "#beta1", "#mixed", "#beta2").forEach((e) => e.remove()) + + await this.nextFrame + + const alphaOutlets = this.controller.alphaOutletElements.filter((outlet) => outlet.classList.contains("connected")) + const betaOutlets = this.controller.betaOutletElements.filter((outlet) => outlet.classList.contains("connected")) + const gammaOutlets = this.controller.gammaOutletElements.filter((outlet) => outlet.classList.contains("connected")) + + this.assert.equal(alphaOutlets.length, 0) + this.assert.equal(this.controller.alphaOutletDisconnectedCallCountValue, 2) + + this.assert.equal(betaOutlets.length, 0) + this.assert.equal(this.controller.betaOutletDisconnectedCallCountValue, 3) + + this.assert.equal(gammaOutlets.length, 0) + this.assert.equal(this.controller.gammaOutletDisconnectedCallCountValue, 0) + } +} diff --git a/src/tests/modules/core/outlet_tests.ts b/src/tests/modules/core/outlet_tests.ts index fb87ba12..b662e387 100644 --- a/src/tests/modules/core/outlet_tests.ts +++ b/src/tests/modules/core/outlet_tests.ts @@ -20,6 +20,7 @@ export default class OutletTests extends ControllerTestCase(OutletController) { data-${this.identifier}-alpha-outlet="#alpha1,#alpha2" data-${this.identifier}-beta-outlet=".beta" data-${this.identifier}-delta-outlet=".delta" + data-${this.identifier}-gamma-outlet="#gamma-doesnt-exist" data-${this.identifier}-namespaced--epsilon-outlet=".epsilon" >
From 04990c88deff2edafaffe4621f2b011fc4fa7049 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Sat, 28 Jan 2023 19:44:22 +0100 Subject: [PATCH 3/6] `Scope#controllerSelector` doesn't to need to public anymore --- src/core/scope.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/scope.ts b/src/core/scope.ts index 2bae3e11..de302807 100644 --- a/src/core/scope.ts +++ b/src/core/scope.ts @@ -44,7 +44,7 @@ export class Scope { return Array.from(this.element.querySelectorAll(selector)) } - get controllerSelector(): string { + private get controllerSelector(): string { return attributeValueContainsToken(this.schema.controllerAttribute, this.identifier) } From c64775d5beb47c5a3b1f92a326e4659e382fad34 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Mon, 30 Jan 2023 04:43:58 +0100 Subject: [PATCH 4/6] Shouldn't fallback to default selector if outlet attribute is present --- src/core/outlet_set.ts | 4 ++- .../core/outlet_fallback_selector_tests.ts | 21 +++++++++++++++ src/tests/modules/core/outlet_tests.ts | 27 ++++++++++++++++++- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/core/outlet_set.ts b/src/core/outlet_set.ts index 5f745fe2..e6035ef8 100644 --- a/src/core/outlet_set.ts +++ b/src/core/outlet_set.ts @@ -41,8 +41,10 @@ export class OutletSet { getSelectorForOutletName(outletName: string) { const attributeName = this.schema.outletAttributeForScope(this.identifier, outletName) + const hasSelector = this.controllerElement.hasAttribute(attributeName) const selector = this.controllerElement.getAttribute(attributeName) - return selector || this.getControllerSelectorForOutletName(outletName) + + return hasSelector ? selector : this.getControllerSelectorForOutletName(outletName) } private findOutlet(outletName: string) { diff --git a/src/tests/modules/core/outlet_fallback_selector_tests.ts b/src/tests/modules/core/outlet_fallback_selector_tests.ts index 753aed14..c4c333f1 100644 --- a/src/tests/modules/core/outlet_fallback_selector_tests.ts +++ b/src/tests/modules/core/outlet_fallback_selector_tests.ts @@ -113,4 +113,25 @@ export default class OutletFallbackSelectorTests extends ControllerTestCase(Outl this.assert.equal(gammaOutlets.length, 0) this.assert.equal(this.controller.gammaOutletDisconnectedCallCountValue, 0) } + + async "test outlet disconnected callback shouldn't fire when selector is removed from controller element but fallback selector still covers the outlet"() { + const alpha1 = this.findElement("#alpha1") + const mixed = this.findElement("#mixed") + + this.assert.equal(this.controller.alphaOutletDisconnectedCallCountValue, 0) + + await this.removeAttribute(this.controller.element, `data-${this.identifier}-alpha-outlet`) + + this.assert.equal(this.controller.alphaOutletDisconnectedCallCountValue, 0) + this.assert.ok(alpha1.isConnected, "#alpha1 is still present in document") + this.assert.ok(mixed.isConnected, "#mixed is still present in document") + this.assert.notOk( + alpha1.classList.contains("disconnected"), + `expected "${alpha1.className}" to contain "disconnected"` + ) + this.assert.notOk( + mixed.classList.contains("disconnected"), + `expected "${mixed.className}" to contain "disconnected"` + ) + } } diff --git a/src/tests/modules/core/outlet_tests.ts b/src/tests/modules/core/outlet_tests.ts index b662e387..f25636b6 100644 --- a/src/tests/modules/core/outlet_tests.ts +++ b/src/tests/modules/core/outlet_tests.ts @@ -349,7 +349,32 @@ export default class OutletTests extends ControllerTestCase(OutletController) { ) } - async "test outlet disconnected callback when the controlled element's outlet attribute is removed"() { + async "test outlet connect callback when the controlled element's outlet attribute is emptied"() { + const alpha1 = this.findElement("#alpha1") + const alpha2 = this.findElement("#alpha2") + + this.assert.equal(this.controller.alphaOutletConnectedCallCountValue, 2) + this.assert.equal(this.controller.alphaOutletDisconnectedCallCountValue, 0) + + await this.setAttribute(this.controller.element, `data-${this.identifier}-alpha-outlet`, "") + + this.assert.equal(this.controller.alphaOutletConnectedCallCountValue, 2) + this.assert.equal(this.controller.alphaOutletDisconnectedCallCountValue, 2) + + this.assert.ok(alpha1.isConnected, "alpha1 is still present in document") + this.assert.ok(alpha2.isConnected, "alpha2 is still present in document") + + this.assert.ok( + alpha1.classList.contains("disconnected"), + `expected "${alpha1.className}" to contain "disconnected"` + ) + this.assert.ok( + alpha2.classList.contains("disconnected"), + `expected "${alpha2.className}" to contain "disconnected"` + ) + } + + async "skip test outlet disconnected callback when the controlled element's outlet attribute is removed"() { const alpha1 = this.findElement("#alpha1") const alpha2 = this.findElement("#alpha2") From f5083ba4eff92eb5152b6b1e18f89506f806ed32 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Tue, 31 Jan 2023 02:59:40 +0100 Subject: [PATCH 5/6] "connected" -> "disconnected" --- src/tests/modules/core/outlet_tests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/modules/core/outlet_tests.ts b/src/tests/modules/core/outlet_tests.ts index f25636b6..34010f32 100644 --- a/src/tests/modules/core/outlet_tests.ts +++ b/src/tests/modules/core/outlet_tests.ts @@ -349,7 +349,7 @@ export default class OutletTests extends ControllerTestCase(OutletController) { ) } - async "test outlet connect callback when the controlled element's outlet attribute is emptied"() { + async "test outlet disconnected callback when the controlled element's outlet attribute is emptied"() { const alpha1 = this.findElement("#alpha1") const alpha2 = this.findElement("#alpha2") From 617a749bef67d1589f93cc6dc25675f5d00b3d9b Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Tue, 31 Jan 2023 03:08:30 +0100 Subject: [PATCH 6/6] unskip test case for removing outlet controller attribute --- src/tests/modules/core/outlet_tests.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/tests/modules/core/outlet_tests.ts b/src/tests/modules/core/outlet_tests.ts index 34010f32..57d335eb 100644 --- a/src/tests/modules/core/outlet_tests.ts +++ b/src/tests/modules/core/outlet_tests.ts @@ -374,15 +374,19 @@ export default class OutletTests extends ControllerTestCase(OutletController) { ) } - async "skip test outlet disconnected callback when the controlled element's outlet attribute is removed"() { + async "test outlet disconnected callback when the controlled element's outlet attribute and referenced elements are removed"() { const alpha1 = this.findElement("#alpha1") const alpha2 = this.findElement("#alpha2") - await this.removeAttribute(this.controller.element, `data-${this.identifier}-alpha-outlet`) + this.controller.element.removeAttribute(`data-${this.identifier}-alpha-outlet`) + alpha1.removeAttribute(`data-controller`) + alpha2.remove() + + await this.nextFrame this.assert.equal(this.controller.alphaOutletDisconnectedCallCountValue, 2) this.assert.ok(alpha1.isConnected, "#alpha1 is still present in document") - this.assert.ok(alpha2.isConnected, "#alpha2 is still present in document") + this.assert.notOk(alpha2.isConnected, "#alpha2 shouldn't be present in document anymore") this.assert.ok( alpha1.classList.contains("disconnected"), `expected "${alpha1.className}" to contain "disconnected"`