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

Add ability to set afterLoad static methods on Controllers #579

Merged
merged 1 commit into from Nov 17, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
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
30 changes: 30 additions & 0 deletions src/tests/modules/core/loading_tests.ts
Expand Up @@ -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 = `<div data-controller="loadable"><div data-controller="unloadable">`

Expand All @@ -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[]
}
Expand Down