From 2fdac1a33ceeb2c9786af3f115e2de618339f7f0 Mon Sep 17 00:00:00 2001 From: "LB (Ben Johnston)" Date: Thu, 17 Nov 2022 20:31:47 +1000 Subject: [PATCH] add ability to set `afterLoad` static methods on Controllers (#579) - when a controller is registered, the `afterLoad` static method, if present, will be called - it gets passed the application instance and the identifier that was used to register it - resolves #574 --- docs/reference/controllers.md | 32 +++++++++++++++++++++++++ src/core/controller.ts | 5 ++++ src/core/router.ts | 4 ++++ src/tests/modules/core/loading_tests.ts | 30 +++++++++++++++++++++++ 4 files changed, 71 insertions(+) diff --git a/docs/reference/controllers.md b/docs/reference/controllers.md index ac6f8223..71b86814 100644 --- a/docs/reference/controllers.md +++ b/docs/reference/controllers.md @@ -163,6 +163,38 @@ class UnloadableController extends ApplicationController { application.register("unloadable", UnloadableController) ``` +### Trigger Behaviour When A Controller Is Registered + +If you want to trigger some behaviour once a controller has been registered you can add a static `afterLoad` method: + +```js +class SpinnerButton extends Controller { + static afterLoad(identifier, application) { + // use the application instance to read the configured 'data-controller' attribute + const { controllerAttribute } = application.schema + + // update any legacy buttons with the controller's registered identifier + const updateLegacySpinners = () => { + document.querySelector(".legacy-spinner-button").forEach((element) => { + element.setAttribute(controllerAttribute, identifier) + }) + } + + // called as soon as registered so DOM many not have loaded yet + if (document.readyState == "loading") { + document.addEventListener("DOMContentLoaded", updateLegacySpinners) + } else { + updateLegacySpinners() + } + } +} + +// This controller will update any legacy spinner buttons to use the controller +application.register("spinner-button", SpinnerButton) +``` + +The `afterLoad` method will get called as soon as the controller has been registered, even if no controlled elements exist in the DOM. It gets called with the `identifier` that was used when registering the controller and the Stimulus application instance. + ## Cross-Controller Coordination With Events If you need controllers to communicate with each other, you should use events. The `Controller` class has a convenience method called `dispatch` that makes this easier. It takes an `eventName` as the first argument, which is then automatically prefixed with the name of the controller separated by a colon. The payload is held in `detail`. It works like this: diff --git a/src/core/controller.ts b/src/core/controller.ts index d31f1456..f3db251a 100644 --- a/src/core/controller.ts +++ b/src/core/controller.ts @@ -1,3 +1,4 @@ +import { Application } from "./application" import { ClassPropertiesBlessing } from "./class_properties" import { Constructor } from "./constructor" import { Context } from "./context" @@ -15,6 +16,10 @@ export class Controller { return true } + static afterLoad(_identifier: string, _application: Application) { + return + } + readonly context: Context constructor(context: Context) { diff --git a/src/core/router.ts b/src/core/router.ts index e0ce3af1..c06e5e57 100644 --- a/src/core/router.ts +++ b/src/core/router.ts @@ -55,6 +55,10 @@ export class Router implements ScopeObserverDelegate { this.unloadIdentifier(definition.identifier) const module = new Module(this.application, definition) this.connectModule(module) + const afterLoad = (definition.controllerConstructor as any).afterLoad + if (afterLoad) { + afterLoad(definition.identifier, this.application) + } } unloadIdentifier(identifier: string) { diff --git a/src/tests/modules/core/loading_tests.ts b/src/tests/modules/core/loading_tests.ts index efe496d9..1190dcdb 100644 --- a/src/tests/modules/core/loading_tests.ts +++ b/src/tests/modules/core/loading_tests.ts @@ -12,6 +12,16 @@ class LoadableController extends LogController { } } +class AfterLoadController extends LogController { + static afterLoad(identifier: string, application: any) { + const newElement = document.createElement("div") + newElement.classList.add("after-load-test") + newElement.setAttribute(application.schema.controllerAttribute, identifier) + application.element.append(newElement) + document.dispatchEvent(new CustomEvent("test", { detail: { identifier, application } })) + } +} + export default class ApplicationTests extends ApplicationTestCase { fixtureHTML = `
` @@ -25,6 +35,26 @@ export default class ApplicationTests extends ApplicationTestCase { this.assert.equal(this.controllers.length, 1) } + "test module with afterLoad method should be triggered when registered"() { + // set up an event listener to track the params passed into the AfterLoadController + let data: { application?: any; identifier?: string } = {} + document.addEventListener("test", (({ detail }: CustomEvent) => { + data = detail + }) as EventListener) + + this.assert.equal(data.identifier, undefined) + this.assert.equal(data.application, undefined) + + this.application.register("after-load", AfterLoadController) + + // check the DOM element has been added based on params provided + this.assert.equal(this.findElements('[data-controller="after-load"]').length, 1) + + // check that static method was correctly called with the params + this.assert.equal(data.identifier, "after-load") + this.assert.equal(data.application, this.application) + } + get controllers() { return this.application.controllers as LogController[] }