From 20a398932e18a5c8a999badffcb4800deaf1e568 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Sat, 28 Jan 2023 00:44:25 +0100 Subject: [PATCH 1/5] Ensure `Scope` is connected before accessing outlets --- src/core/outlet_properties.ts | 67 +++++++++++++++++++++-------------- src/core/router.ts | 10 ++++++ src/core/scope_observer.ts | 4 +++ 3 files changed, 55 insertions(+), 26 deletions(-) diff --git a/src/core/outlet_properties.ts b/src/core/outlet_properties.ts index 704f6e33..249f74c9 100644 --- a/src/core/outlet_properties.ts +++ b/src/core/outlet_properties.ts @@ -10,26 +10,40 @@ export function OutletPropertiesBlessing(constructor: Constructor) { }, {} as PropertyDescriptorMap) } +function getOutletController(controller: Controller, element: Element, identifier: string) { + return controller.application.getControllerForElementAndIdentifier(element, identifier) +} + +function getControllerAndEnsureConnectedScope(controller: Controller, element: Element, outletName: string) { + let outletController = getOutletController(controller, element, outletName) + if (outletController) return outletController + + controller.application.router.proposeToConnectScopeForElementAndIdentifier(element, outletName) + + outletController = getOutletController(controller, element, outletName) + if (outletController) return outletController +} + function propertiesForOutletDefinition(name: string) { const camelizedName = namespaceCamelize(name) return { [`${camelizedName}Outlet`]: { get(this: Controller) { - const outlet = this.outlets.find(name) - - if (outlet) { - const outletController = this.application.getControllerForElementAndIdentifier(outlet, name) - if (outletController) { - return outletController - } else { - throw new Error( - `Missing "${this.application.schema.controllerAttribute}=${name}" attribute on outlet element for "${this.identifier}" controller` - ) - } + const outletElement = this.outlets.find(name) + const selector = this.outlets.getSelectorForOutletName(name) + + if (outletElement) { + const outletController = getControllerAndEnsureConnectedScope(this, outletElement, name) + + if (outletController) return outletController + + throw new Error( + `The provided outlet element is missing an outlet controller "${name}" instance for host controller "${this.identifier}"`, + ) } - throw new Error(`Missing outlet element "${name}" for "${this.identifier}" controller`) + throw new Error(`Missing outlet element "${name}" for host controller "${this.identifier}". Stimulus couldn't find a matching outlet element using selector "${selector}".`) }, }, @@ -39,16 +53,15 @@ function propertiesForOutletDefinition(name: string) { if (outlets.length > 0) { return outlets - .map((outlet: Element) => { - const controller = this.application.getControllerForElementAndIdentifier(outlet, name) - if (controller) { - return controller - } else { - console.warn( - `The provided outlet element is missing the outlet controller "${name}" for "${this.identifier}"`, - outlet - ) - } + .map((outletElement: Element) => { + const outletController = getControllerAndEnsureConnectedScope(this, outletElement, name) + + if (outletController) return outletController + + console.warn( + `The provided outlet element is missing an outlet controller "${name}" instance for host controller "${this.identifier}"`, + outletElement + ) }) .filter((controller) => controller) as Controller[] } @@ -59,11 +72,13 @@ function propertiesForOutletDefinition(name: string) { [`${camelizedName}OutletElement`]: { get(this: Controller) { - const outlet = this.outlets.find(name) - if (outlet) { - return outlet + const outletElement = this.outlets.find(name) + const selector = this.outlets.getSelectorForOutletName(name) + + if (outletElement) { + return outletElement } else { - throw new Error(`Missing outlet element "${name}" for "${this.identifier}" controller`) + throw new Error(`Missing outlet element "${name}" for host controller "${this.identifier}". Stimulus couldn't find a matching outlet element using selector "${selector}".`) } }, }, diff --git a/src/core/router.ts b/src/core/router.ts index c06e5e57..64f8a2ca 100644 --- a/src/core/router.ts +++ b/src/core/router.ts @@ -75,6 +75,16 @@ export class Router implements ScopeObserverDelegate { } } + proposeToConnectScopeForElementAndIdentifier(element: Element, identifier: string) { + const scope = this.scopeObserver.parseValueForElementAndIdentifier(element, identifier) + + if (scope) { + this.scopeObserver.elementMatchedValue(scope.element, scope) + } else { + console.error(`Couldn't find or create scope for identifier: "${identifier}" and element:`, element) + } + } + // Error handler delegate handleError(error: Error, message: string, detail: any) { diff --git a/src/core/scope_observer.ts b/src/core/scope_observer.ts index 6ddcd0ef..0349a24b 100644 --- a/src/core/scope_observer.ts +++ b/src/core/scope_observer.ts @@ -42,6 +42,10 @@ export class ScopeObserver implements ValueListObserverDelegate { parseValueForToken(token: Token): Scope | undefined { const { element, content: identifier } = token + return this.parseValueForElementAndIdentifier(element, identifier) + } + + parseValueForElementAndIdentifier(element: Element, identifier: string): Scope | undefined { const scopesByIdentifier = this.fetchScopesByIdentifierForElement(element) let scope = scopesByIdentifier.get(identifier) From af40cce46644d403d4dfefcf3de6ed3b7f31187b Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Sat, 28 Jan 2023 02:20:28 +0100 Subject: [PATCH 2/5] Lint --- src/core/outlet_properties.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/core/outlet_properties.ts b/src/core/outlet_properties.ts index 249f74c9..4af436a4 100644 --- a/src/core/outlet_properties.ts +++ b/src/core/outlet_properties.ts @@ -39,11 +39,13 @@ function propertiesForOutletDefinition(name: string) { if (outletController) return outletController throw new Error( - `The provided outlet element is missing an outlet controller "${name}" instance for host controller "${this.identifier}"`, + `The provided outlet element is missing an outlet controller "${name}" instance for host controller "${this.identifier}"` ) } - throw new Error(`Missing outlet element "${name}" for host controller "${this.identifier}". Stimulus couldn't find a matching outlet element using selector "${selector}".`) + throw new Error( + `Missing outlet element "${name}" for host controller "${this.identifier}". Stimulus couldn't find a matching outlet element using selector "${selector}".` + ) }, }, @@ -78,7 +80,9 @@ function propertiesForOutletDefinition(name: string) { if (outletElement) { return outletElement } else { - throw new Error(`Missing outlet element "${name}" for host controller "${this.identifier}". Stimulus couldn't find a matching outlet element using selector "${selector}".`) + throw new Error( + `Missing outlet element "${name}" for host controller "${this.identifier}". Stimulus couldn't find a matching outlet element using selector "${selector}".` + ) } }, }, From 66484f314e6d77aed35972d76f65074f86a4149f Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Sun, 18 Jun 2023 23:22:23 +0200 Subject: [PATCH 3/5] Add tests for accessing outlets before they are initialized --- src/tests/controllers/outlet_controller.ts | 6 +++++ src/tests/modules/core/outlet_order_tests.ts | 24 ++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 src/tests/modules/core/outlet_order_tests.ts diff --git a/src/tests/controllers/outlet_controller.ts b/src/tests/controllers/outlet_controller.ts index 261a4734..b382f80c 100644 --- a/src/tests/controllers/outlet_controller.ts +++ b/src/tests/controllers/outlet_controller.ts @@ -19,6 +19,7 @@ export class OutletController extends BaseOutletController { alphaOutletDisconnectedCallCount: Number, betaOutletConnectedCallCount: Number, betaOutletDisconnectedCallCount: Number, + betaOutletsInConnect: Number, gammaOutletConnectedCallCount: Number, gammaOutletDisconnectedCallCount: Number, namespacedEpsilonOutletConnectedCallCount: Number, @@ -46,11 +47,16 @@ export class OutletController extends BaseOutletController { alphaOutletDisconnectedCallCountValue = 0 betaOutletConnectedCallCountValue = 0 betaOutletDisconnectedCallCountValue = 0 + betaOutletsInConnectValue = 0 gammaOutletConnectedCallCountValue = 0 gammaOutletDisconnectedCallCountValue = 0 namespacedEpsilonOutletConnectedCallCountValue = 0 namespacedEpsilonOutletDisconnectedCallCountValue = 0 + connect() { + this.betaOutletsInConnectValue = this.betaOutlets.length + } + alphaOutletConnected(_outlet: Controller, element: Element) { if (this.hasConnectedClass) element.classList.add(this.connectedClass) this.alphaOutletConnectedCallCountValue++ diff --git a/src/tests/modules/core/outlet_order_tests.ts b/src/tests/modules/core/outlet_order_tests.ts new file mode 100644 index 00000000..629aaddc --- /dev/null +++ b/src/tests/modules/core/outlet_order_tests.ts @@ -0,0 +1,24 @@ +import { ControllerTestCase } from "../../cases/controller_test_case" +import { OutletController } from "../../controllers/outlet_controller" + +export default class OutletOrderTests extends ControllerTestCase(OutletController) { + fixtureHTML = ` +
Search
+
Beta
+
Beta
+
Beta
+ ` + + get identifiers() { + return ["alpha", "beta"] + } + + async "test can access outlets in connect() even if they are referenced before they are connected"() { + this.assert.equal(this.controller.betaOutletsInConnectValue, 3) + + this.controller.betaOutlets.forEach(outlet => { + this.assert.equal(outlet.identifier, "beta") + this.assert.equal(Array.from(outlet.element.classList.values()), "beta") + }) + } +} From 792d15400312151448bcd23ff385ac9bcca16024 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Sun, 18 Jun 2023 23:27:14 +0200 Subject: [PATCH 4/5] Lint --- src/tests/modules/core/outlet_order_tests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/modules/core/outlet_order_tests.ts b/src/tests/modules/core/outlet_order_tests.ts index 629aaddc..211c0e40 100644 --- a/src/tests/modules/core/outlet_order_tests.ts +++ b/src/tests/modules/core/outlet_order_tests.ts @@ -16,7 +16,7 @@ export default class OutletOrderTests extends ControllerTestCase(OutletControlle async "test can access outlets in connect() even if they are referenced before they are connected"() { this.assert.equal(this.controller.betaOutletsInConnectValue, 3) - this.controller.betaOutlets.forEach(outlet => { + this.controller.betaOutlets.forEach((outlet) => { this.assert.equal(outlet.identifier, "beta") this.assert.equal(Array.from(outlet.element.classList.values()), "beta") }) From 15a3675c915c33fb8158cd4ff3ea365576e9e06f Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Sun, 18 Jun 2023 23:34:12 +0200 Subject: [PATCH 5/5] Also test connect order --- src/tests/modules/core/outlet_order_tests.ts | 31 ++++++++++++++++---- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/tests/modules/core/outlet_order_tests.ts b/src/tests/modules/core/outlet_order_tests.ts index 211c0e40..5d8bfcf4 100644 --- a/src/tests/modules/core/outlet_order_tests.ts +++ b/src/tests/modules/core/outlet_order_tests.ts @@ -1,12 +1,22 @@ import { ControllerTestCase } from "../../cases/controller_test_case" import { OutletController } from "../../controllers/outlet_controller" -export default class OutletOrderTests extends ControllerTestCase(OutletController) { +const connectOrder: string[] = [] + +class OutletOrderController extends OutletController { + connect() { + connectOrder.push(`${this.identifier}-${this.element.id}-start`) + super.connect() + connectOrder.push(`${this.identifier}-${this.element.id}-end`) + } +} + +export default class OutletOrderTests extends ControllerTestCase(OutletOrderController) { fixtureHTML = ` -
Search
-
Beta
-
Beta
-
Beta
+
Search
+
Beta
+
Beta
+
Beta
` get identifiers() { @@ -20,5 +30,16 @@ export default class OutletOrderTests extends ControllerTestCase(OutletControlle this.assert.equal(outlet.identifier, "beta") this.assert.equal(Array.from(outlet.element.classList.values()), "beta") }) + + this.assert.deepEqual(connectOrder, [ + "alpha-alpha1-start", + "beta-beta-1-start", + "beta-beta-1-end", + "beta-beta-2-start", + "beta-beta-2-end", + "beta-beta-3-start", + "beta-beta-3-end", + "alpha-alpha1-end", + ]) } }