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"`