Skip to content

Commit

Permalink
add ability to set afterLoad static methods on Controllers
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
LB Johnston committed Aug 30, 2022
1 parent a2a030f commit 8deda6d
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 0 deletions.
32 changes: 32 additions & 0 deletions docs/reference/controllers.md
Expand Up @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions 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"
Expand All @@ -15,6 +16,10 @@ export class Controller<ElementType extends Element = Element> {
return true
}

static afterLoad(identifier: string, application: Application) {
return
}

readonly context: Context

constructor(context: Context) {
Expand Down
4 changes: 4 additions & 0 deletions src/core/router.ts
Expand Up @@ -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) {
Expand Down
32 changes: 32 additions & 0 deletions src/tests/modules/core/loading_tests.ts
Expand Up @@ -12,6 +12,18 @@ 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 = `<div data-controller="loadable"><div data-controller="unloadable">`

Expand All @@ -25,6 +37,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[]
}
Expand Down

0 comments on commit 8deda6d

Please sign in to comment.