Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions docs/reference/lifecycle_callbacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,78 @@ connect() | Anytime the controller is connected to the DOM
[name]TargetDisconnected(target: Element) | Anytime a target is disconnected from the DOM
disconnect() | Anytime the controller is disconnected from the DOM

## Events

After each new lifecycle state is reached and the corresponding method described above has been called,
a event will also be dispatched deom the controller.

Event name | Event detail
------------ | --------------------
[identifer]:initialized | `{}` empty object
[identifer]:[name]TargetConnected | {target: Element}
[identifer]:connected | `{}` empty object
[identifer]:[name]TargetDisconnected | {target: Element}
[identifer]:disconnect | `{}` empty object

The event are distached using [`controller.dispatch()`](https://stimulus.hotwired.dev/reference/controllers#cross-controller-coordination-with-events)
so you can use [actions](https://stimulus.hotwired.dev/reference/actions) to observe the controller's
lifecycle.

Example usage:

```js
import { Controller } from "@hotwired/stimulus"

class ControllerObserver extends Controller {
onObservedInputConnected({target}) {
}
}

class ObservedController extends Controller {
static targets = ["input"]
}

application.register("observer", ControllerObserver)
application.register("observed", ControllerObserver)
```

```html
<div data-controller="observer">
<div data-controller="observed" data-action="observed:inputTargetConnected->observer#onObservedInputConnected">
<input type="text" data-observed-target="input">
</div>
</div>
```

## Knowing the controller's current lifecycle state

In addition to the events explained above, the controller also expose their current lifecycle through
the read-only property `lifecycle`. Combined with the said events, this can be used to write controller extensions:

```js
import { Lifecycle } from "@hotwired/stimulus"

function useExtention(controller) {
function onConnected() {
controller.element.addEventListener(`${controller.identifier}:connected`, removeEventListener)
// extend the controller
}

if(controller.lifecycle < Lifecycle.connected) {
controller.element.addEventListener(`${controller.identifier}:connected`, onConnected)
} else {
onConnected()
}
}
Comment on lines +86 to +96
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There could be a controller method for this pattern, IDK.


class ControllerObserver extends Controller {
initialize() {
useExtention(this)
}
}
```


## Connection

A controller is _connected_ to the document when both of the following conditions are true:
Expand Down
27 changes: 25 additions & 2 deletions src/core/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,31 @@ import { TargetObserver, TargetObserverDelegate } from "./target_observer"
import { OutletObserver, OutletObserverDelegate } from "./outlet_observer"
import { namespaceCamelize } from "./string_helpers"

export enum Lifecycle {
Idle,
Initialized,
Connected,
Disconnected,
}

export class Context implements ErrorHandler, TargetObserverDelegate, OutletObserverDelegate {
readonly module: Module
readonly scope: Scope
readonly controller: Controller
private _lifecycle: Lifecycle
private bindingObserver: BindingObserver
private valueObserver: ValueObserver
private targetObserver: TargetObserver
private outletObserver: OutletObserver

get lifecycle(): Lifecycle {
return this._lifecycle
}

constructor(module: Module, scope: Scope) {
this.module = module
this.scope = scope
this._lifecycle = Lifecycle.Idle
this.controller = new module.controllerConstructor(this)
this.bindingObserver = new BindingObserver(this, this.dispatcher)
this.valueObserver = new ValueObserver(this, this.controller)
Expand All @@ -31,6 +44,8 @@ export class Context implements ErrorHandler, TargetObserverDelegate, OutletObse

try {
this.controller.initialize()
this._lifecycle = Lifecycle.Initialized
this.controller.dispatch("initialized")
this.logDebugActivity("initialize")
} catch (error: any) {
this.handleError(error, "initializing controller")
Expand All @@ -45,6 +60,8 @@ export class Context implements ErrorHandler, TargetObserverDelegate, OutletObse

try {
this.controller.connect()
this._lifecycle = Lifecycle.Connected
this.controller.dispatch("connected")
this.logDebugActivity("connect")
} catch (error: any) {
this.handleError(error, "connecting controller")
Expand All @@ -58,6 +75,8 @@ export class Context implements ErrorHandler, TargetObserverDelegate, OutletObse
disconnect() {
try {
this.controller.disconnect()
this._lifecycle = Lifecycle.Disconnected
this.controller.dispatch("disconnected")
this.logDebugActivity("disconnect")
} catch (error: any) {
this.handleError(error, "disconnecting controller")
Expand Down Expand Up @@ -112,11 +131,15 @@ export class Context implements ErrorHandler, TargetObserverDelegate, OutletObse
// Target observer delegate

targetConnected(element: Element, name: string) {
this.invokeControllerMethod(`${name}TargetConnected`, element)
const methodName = `${name}TargetConnected`
this.invokeControllerMethod(methodName, element)
this.controller.dispatch(methodName, {detail: {element}})
}

targetDisconnected(element: Element, name: string) {
this.invokeControllerMethod(`${name}TargetDisconnected`, element)
const methodName = `${name}TargetDisconnected`
this.invokeControllerMethod(methodName, element)
this.controller.dispatch(methodName, {detail: {element}})
}

// Outlet observer delegate
Expand Down
4 changes: 4 additions & 0 deletions src/core/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export class Controller<ElementType extends Element = Element> {
return this.context.scope
}

get lifecycle() {
return this.context.lifecycle
}
Comment on lines +52 to +54
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder is lifecycleIsAtLeast(lifecycle: Lifecycle), lifecycleIsAtMost(lifecycle: Lifecycle) and lifecycleIsBetween(from: Lifecycle, to: Lifecycle) utility functions are necessary here or if controller.lifecycle < Lifecycle.Initialized is sufficient?


get element() {
return this.scope.element as ElementType
}
Expand Down