Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit eda0f9d

Browse files
authored
Ensure Scope is connected before accessing outlets (#648)
* Ensure `Scope` is connected before accessing outlets * Add tests for accessing outlets before they are initialized * Also test connect order
1 parent 823cfee commit eda0f9d

5 files changed

Lines changed: 110 additions & 26 deletions

File tree

src/core/outlet_properties.ts

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,42 @@ export function OutletPropertiesBlessing<T>(constructor: Constructor<T>) {
1010
}, {} as PropertyDescriptorMap)
1111
}
1212

13+
function getOutletController(controller: Controller, element: Element, identifier: string) {
14+
return controller.application.getControllerForElementAndIdentifier(element, identifier)
15+
}
16+
17+
function getControllerAndEnsureConnectedScope(controller: Controller, element: Element, outletName: string) {
18+
let outletController = getOutletController(controller, element, outletName)
19+
if (outletController) return outletController
20+
21+
controller.application.router.proposeToConnectScopeForElementAndIdentifier(element, outletName)
22+
23+
outletController = getOutletController(controller, element, outletName)
24+
if (outletController) return outletController
25+
}
26+
1327
function propertiesForOutletDefinition(name: string) {
1428
const camelizedName = namespaceCamelize(name)
1529

1630
return {
1731
[`${camelizedName}Outlet`]: {
1832
get(this: Controller) {
19-
const outlet = this.outlets.find(name)
20-
21-
if (outlet) {
22-
const outletController = this.application.getControllerForElementAndIdentifier(outlet, name)
23-
if (outletController) {
24-
return outletController
25-
} else {
26-
throw new Error(
27-
`Missing "${this.application.schema.controllerAttribute}=${name}" attribute on outlet element for "${this.identifier}" controller`
28-
)
29-
}
33+
const outletElement = this.outlets.find(name)
34+
const selector = this.outlets.getSelectorForOutletName(name)
35+
36+
if (outletElement) {
37+
const outletController = getControllerAndEnsureConnectedScope(this, outletElement, name)
38+
39+
if (outletController) return outletController
40+
41+
throw new Error(
42+
`The provided outlet element is missing an outlet controller "${name}" instance for host controller "${this.identifier}"`
43+
)
3044
}
3145

32-
throw new Error(`Missing outlet element "${name}" for "${this.identifier}" controller`)
46+
throw new Error(
47+
`Missing outlet element "${name}" for host controller "${this.identifier}". Stimulus couldn't find a matching outlet element using selector "${selector}".`
48+
)
3349
},
3450
},
3551

@@ -39,16 +55,15 @@ function propertiesForOutletDefinition(name: string) {
3955

4056
if (outlets.length > 0) {
4157
return outlets
42-
.map((outlet: Element) => {
43-
const controller = this.application.getControllerForElementAndIdentifier(outlet, name)
44-
if (controller) {
45-
return controller
46-
} else {
47-
console.warn(
48-
`The provided outlet element is missing the outlet controller "${name}" for "${this.identifier}"`,
49-
outlet
50-
)
51-
}
58+
.map((outletElement: Element) => {
59+
const outletController = getControllerAndEnsureConnectedScope(this, outletElement, name)
60+
61+
if (outletController) return outletController
62+
63+
console.warn(
64+
`The provided outlet element is missing an outlet controller "${name}" instance for host controller "${this.identifier}"`,
65+
outletElement
66+
)
5267
})
5368
.filter((controller) => controller) as Controller[]
5469
}
@@ -59,11 +74,15 @@ function propertiesForOutletDefinition(name: string) {
5974

6075
[`${camelizedName}OutletElement`]: {
6176
get(this: Controller) {
62-
const outlet = this.outlets.find(name)
63-
if (outlet) {
64-
return outlet
77+
const outletElement = this.outlets.find(name)
78+
const selector = this.outlets.getSelectorForOutletName(name)
79+
80+
if (outletElement) {
81+
return outletElement
6582
} else {
66-
throw new Error(`Missing outlet element "${name}" for "${this.identifier}" controller`)
83+
throw new Error(
84+
`Missing outlet element "${name}" for host controller "${this.identifier}". Stimulus couldn't find a matching outlet element using selector "${selector}".`
85+
)
6786
}
6887
},
6988
},

src/core/router.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,16 @@ export class Router implements ScopeObserverDelegate {
7575
}
7676
}
7777

78+
proposeToConnectScopeForElementAndIdentifier(element: Element, identifier: string) {
79+
const scope = this.scopeObserver.parseValueForElementAndIdentifier(element, identifier)
80+
81+
if (scope) {
82+
this.scopeObserver.elementMatchedValue(scope.element, scope)
83+
} else {
84+
console.error(`Couldn't find or create scope for identifier: "${identifier}" and element:`, element)
85+
}
86+
}
87+
7888
// Error handler delegate
7989

8090
handleError(error: Error, message: string, detail: any) {

src/core/scope_observer.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ export class ScopeObserver implements ValueListObserverDelegate<Scope> {
4242

4343
parseValueForToken(token: Token): Scope | undefined {
4444
const { element, content: identifier } = token
45+
return this.parseValueForElementAndIdentifier(element, identifier)
46+
}
47+
48+
parseValueForElementAndIdentifier(element: Element, identifier: string): Scope | undefined {
4549
const scopesByIdentifier = this.fetchScopesByIdentifierForElement(element)
4650

4751
let scope = scopesByIdentifier.get(identifier)

src/tests/controllers/outlet_controller.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export class OutletController extends BaseOutletController {
1919
alphaOutletDisconnectedCallCount: Number,
2020
betaOutletConnectedCallCount: Number,
2121
betaOutletDisconnectedCallCount: Number,
22+
betaOutletsInConnect: Number,
2223
gammaOutletConnectedCallCount: Number,
2324
gammaOutletDisconnectedCallCount: Number,
2425
namespacedEpsilonOutletConnectedCallCount: Number,
@@ -46,11 +47,16 @@ export class OutletController extends BaseOutletController {
4647
alphaOutletDisconnectedCallCountValue = 0
4748
betaOutletConnectedCallCountValue = 0
4849
betaOutletDisconnectedCallCountValue = 0
50+
betaOutletsInConnectValue = 0
4951
gammaOutletConnectedCallCountValue = 0
5052
gammaOutletDisconnectedCallCountValue = 0
5153
namespacedEpsilonOutletConnectedCallCountValue = 0
5254
namespacedEpsilonOutletDisconnectedCallCountValue = 0
5355

56+
connect() {
57+
this.betaOutletsInConnectValue = this.betaOutlets.length
58+
}
59+
5460
alphaOutletConnected(_outlet: Controller, element: Element) {
5561
if (this.hasConnectedClass) element.classList.add(this.connectedClass)
5662
this.alphaOutletConnectedCallCountValue++
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { ControllerTestCase } from "../../cases/controller_test_case"
2+
import { OutletController } from "../../controllers/outlet_controller"
3+
4+
const connectOrder: string[] = []
5+
6+
class OutletOrderController extends OutletController {
7+
connect() {
8+
connectOrder.push(`${this.identifier}-${this.element.id}-start`)
9+
super.connect()
10+
connectOrder.push(`${this.identifier}-${this.element.id}-end`)
11+
}
12+
}
13+
14+
export default class OutletOrderTests extends ControllerTestCase(OutletOrderController) {
15+
fixtureHTML = `
16+
<div data-controller="alpha" id="alpha1" data-alpha-beta-outlet=".beta">Search</div>
17+
<div data-controller="beta" id="beta-1" class="beta">Beta</div>
18+
<div data-controller="beta" id="beta-2" class="beta">Beta</div>
19+
<div data-controller="beta" id="beta-3" class="beta">Beta</div>
20+
`
21+
22+
get identifiers() {
23+
return ["alpha", "beta"]
24+
}
25+
26+
async "test can access outlets in connect() even if they are referenced before they are connected"() {
27+
this.assert.equal(this.controller.betaOutletsInConnectValue, 3)
28+
29+
this.controller.betaOutlets.forEach((outlet) => {
30+
this.assert.equal(outlet.identifier, "beta")
31+
this.assert.equal(Array.from(outlet.element.classList.values()), "beta")
32+
})
33+
34+
this.assert.deepEqual(connectOrder, [
35+
"alpha-alpha1-start",
36+
"beta-beta-1-start",
37+
"beta-beta-1-end",
38+
"beta-beta-2-start",
39+
"beta-beta-2-end",
40+
"beta-beta-3-start",
41+
"beta-beta-3-end",
42+
"alpha-alpha1-end",
43+
])
44+
}
45+
}

0 commit comments

Comments
 (0)