Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make selector for Outlets API optional #647

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 10 additions & 3 deletions src/core/outlet_set.ts
Expand Up @@ -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) {
Expand All @@ -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}"]`
}
}
11 changes: 11 additions & 0 deletions src/tests/controllers/outlet_controller.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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++
Expand Down
137 changes: 137 additions & 0 deletions 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 = `
<div id="container">
<div data-controller="alpha" id="alpha1"></div>

<div data-controller="beta" id="beta1">
<div data-controller="alpha beta" id="mixed"></div>
<div data-controller="beta" id="beta2"></div>
</div>

<div
data-controller="${this.identifier}"
data-${this.identifier}-connected-class="connected"
data-${this.identifier}-disconnected-class="disconnected"
>
</div>
</div>
`
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"`
)
}
}
28 changes: 27 additions & 1 deletion src/tests/modules/core/outlet_tests.ts
Expand Up @@ -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"
>
<div data-controller="gamma" class="gamma" id="gamma2"></div>
Expand Down Expand Up @@ -348,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 disconnected 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"() {
marcoroth marked this conversation as resolved.
Show resolved Hide resolved
const alpha1 = this.findElement("#alpha1")
const alpha2 = this.findElement("#alpha2")

Expand Down