diff --git a/src/core/outlet_set.ts b/src/core/outlet_set.ts index b8ca77b4..e6035ef8 100644 --- a/src/core/outlet_set.ts +++ b/src/core/outlet_set.ts @@ -41,7 +41,10 @@ export class OutletSet { getSelectorForOutletName(outletName: string) { const attributeName = this.schema.outletAttributeForScope(this.identifier, outletName) - return this.controllerElement.getAttribute(attributeName) + const hasSelector = this.controllerElement.hasAttribute(attributeName) + const selector = this.controllerElement.getAttribute(attributeName) + + return hasSelector ? selector : this.getControllerSelectorForOutletName(outletName) } private findOutlet(outletName: string) { @@ -65,7 +68,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..c4c333f1 --- /dev/null +++ b/src/tests/modules/core/outlet_fallback_selector_tests.ts @@ -0,0 +1,137 @@ +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) + } + + 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 fb87ba12..57d335eb 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" >
@@ -348,15 +349,44 @@ export default class OutletTests extends ControllerTestCase(OutletController) { ) } - async "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 is emptied"() { const alpha1 = this.findElement("#alpha1") const alpha2 = this.findElement("#alpha2") - await this.removeAttribute(this.controller.element, `data-${this.identifier}-alpha-outlet`) + 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 "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") + + 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"`