Skip to content

Commit d3462df

Browse files
Add public API for tracking and reacting to controllers lifecycle
1 parent 422eb81 commit d3462df

File tree

3 files changed

+101
-2
lines changed

3 files changed

+101
-2
lines changed

docs/reference/lifecycle_callbacks.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,78 @@ connect() | Anytime the controller is connected to the DOM
3131
[name]TargetDisconnected(target: Element) | Anytime a target is disconnected from the DOM
3232
disconnect() | Anytime the controller is disconnected from the DOM
3333

34+
## Events
35+
36+
After each new lifecycle state is reach and the corresponding method described above has been called,
37+
a event will also be dispatched deom the controller.
38+
39+
Event name | Event detail
40+
------------ | --------------------
41+
[identifer]:initialized | `{}` empty object
42+
[identifer]:[name]TargetConnected | {target: Element}
43+
[identifer]:connected | `{}` empty object
44+
[identifer]:[name]TargetDisconnected | {target: Element}
45+
[identifer]:disconnect | `{}` empty object
46+
47+
The event are distached using [`controller.dispatch()`](https://stimulus.hotwired.dev/reference/controllers#cross-controller-coordination-with-events)
48+
so you can use [actions](https://stimulus.hotwired.dev/reference/actions) to observe the controller's
49+
lifecycle.
50+
51+
Example usage:
52+
53+
```js
54+
import { Controller } from "@hotwired/stimulus"
55+
56+
class ControllerObserver extends Controller {
57+
onObservedInputConnected(element) {
58+
}
59+
}
60+
61+
class ObservedController extends Controller {
62+
static targets = ["input"]
63+
}
64+
65+
application.register("observer", ControllerObserver)
66+
application.register("observed", ControllerObserver)
67+
```
68+
69+
```html
70+
<div data-controller="observer">
71+
<div data-controller="observed" data-action="observed:inputTargetConnected->observer#onObservedInputConnected">
72+
<input type="text" data-observed-target="input">
73+
</div>
74+
</div>
75+
```
76+
77+
## Knowing the controller's current lifecycle state
78+
79+
In addition to the events explained above, the controller also expose their current lifecycle through
80+
the read-only property `lifecycle`. Combined with the said events, this can be used to write controller extensions:
81+
82+
```js
83+
import { Lifecycle } from "@hotwired/stimulus"
84+
85+
function useExtention(controller) {
86+
function onConnected() {
87+
controller.element.addEventListener(`${controller.identifier}:connected`, removeEventListener)
88+
// extend the controller
89+
}
90+
91+
if(controller.lifecycle < Lifecycle.connected) {
92+
controller.element.addEventListener(`${controller.identifier}:connected`, onConnected)
93+
} else {
94+
onConnected()
95+
}
96+
}
97+
98+
class ControllerObserver extends Controller {
99+
initialize() {
100+
useExtention(this)
101+
}
102+
}
103+
```
104+
105+
34106
## Connection
35107

36108
A controller is _connected_ to the document when both of the following conditions are true:

src/core/context.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,31 @@ import { TargetObserver, TargetObserverDelegate } from "./target_observer"
1111
import { OutletObserver, OutletObserverDelegate } from "./outlet_observer"
1212
import { namespaceCamelize } from "./string_helpers"
1313

14+
export enum Lifecycle {
15+
Idle,
16+
Initialized,
17+
Connected,
18+
Disconnected,
19+
}
20+
1421
export class Context implements ErrorHandler, TargetObserverDelegate, OutletObserverDelegate {
1522
readonly module: Module
1623
readonly scope: Scope
1724
readonly controller: Controller
25+
private _lifecycle: Lifecycle
1826
private bindingObserver: BindingObserver
1927
private valueObserver: ValueObserver
2028
private targetObserver: TargetObserver
2129
private outletObserver: OutletObserver
2230

31+
get lifecycle(): Lifecycle {
32+
return this._lifecycle
33+
}
34+
2335
constructor(module: Module, scope: Scope) {
2436
this.module = module
2537
this.scope = scope
38+
this._lifecycle = Lifecycle.Idle
2639
this.controller = new module.controllerConstructor(this)
2740
this.bindingObserver = new BindingObserver(this, this.dispatcher)
2841
this.valueObserver = new ValueObserver(this, this.controller)
@@ -31,6 +44,8 @@ export class Context implements ErrorHandler, TargetObserverDelegate, OutletObse
3144

3245
try {
3346
this.controller.initialize()
47+
this._lifecycle = Lifecycle.Initialized
48+
this.controller.dispatch("initialized")
3449
this.logDebugActivity("initialize")
3550
} catch (error: any) {
3651
this.handleError(error, "initializing controller")
@@ -45,6 +60,8 @@ export class Context implements ErrorHandler, TargetObserverDelegate, OutletObse
4560

4661
try {
4762
this.controller.connect()
63+
this._lifecycle = Lifecycle.Connected
64+
this.controller.dispatch("connected")
4865
this.logDebugActivity("connect")
4966
} catch (error: any) {
5067
this.handleError(error, "connecting controller")
@@ -58,6 +75,8 @@ export class Context implements ErrorHandler, TargetObserverDelegate, OutletObse
5875
disconnect() {
5976
try {
6077
this.controller.disconnect()
78+
this._lifecycle = Lifecycle.Disconnected
79+
this.controller.dispatch("disconnected")
6180
this.logDebugActivity("disconnect")
6281
} catch (error: any) {
6382
this.handleError(error, "disconnecting controller")
@@ -112,11 +131,15 @@ export class Context implements ErrorHandler, TargetObserverDelegate, OutletObse
112131
// Target observer delegate
113132

114133
targetConnected(element: Element, name: string) {
115-
this.invokeControllerMethod(`${name}TargetConnected`, element)
134+
const methodName = `${name}TargetConnected`
135+
this.invokeControllerMethod(methodName, element)
136+
this.controller.dispatch(methodName, {detail: {element}})
116137
}
117138

118139
targetDisconnected(element: Element, name: string) {
119-
this.invokeControllerMethod(`${name}TargetDisconnected`, element)
140+
const methodName = `${name}TargetDisconnected`
141+
this.invokeControllerMethod(methodName, element)
142+
this.controller.dispatch(methodName, {detail: {element}})
120143
}
121144

122145
// Outlet observer delegate

src/core/controller.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ export class Controller<ElementType extends Element = Element> {
4949
return this.context.scope
5050
}
5151

52+
get lifecycle() {
53+
return this.context.lifecycle
54+
}
55+
5256
get element() {
5357
return this.scope.element as ElementType
5458
}

0 commit comments

Comments
 (0)