Controllers
import { Controller } from 'cx/ui'; Copied Controllers contain the business logic for your views. They handle data initialization, event callbacks, computed values, and reactions to data changes.
Creating a Controller
Extend the Controller class and attach it to a widget using the controller property. The controller has access to the store and can define methods that widgets call:
<div controller={PageController} class="flex flex-col gap-4">
<div class="flex gap-2">
<TextField value={m.name} />
<Button
onClick={(e, instance) => {
instance.getControllerByType(PageController).greet()
}}
>
Greet
</Button>
<Button
onClick={(e, instance) => {
instance.getControllerByType(PageController).clear()
}}
>
Clear
</Button>
</div>
<div text={m.greeting} class="text-primary" />
<div class="p-3 bg-muted rounded text-sm">
<strong>Store content</strong>
<pre class="mt-2" text={(data) => JSON.stringify(data, null, 2)} />
</div>
</div> {
"name": "World"
}The controller’s methods are available to all widgets within its scope. In event handlers, access the controller through the second parameter.
Inline Controllers
For simple cases, define a controller inline using an object:
<div
controller={{
onInit() {
this.store.init(m.count, 0)
this.addComputable(m.double, [m.count], (c) => c * 2)
},
}}
class="flex items-center gap-4"
>
<NumberField value={m.count} style="width: 100px" />
<span text={tpl(m.double, "Double: {0}")} />
</div> The inline form supports lifecycle methods and controller features like addTrigger and addComputable.
Lifecycle Methods
Controllers have lifecycle methods that run at specific times:
| Method | Description |
|---|---|
onInit() | Runs once when the controller is created. Use for data initialization and setup. |
onExplore() | Runs on every render cycle during the explore phase. |
onDestroy() | Runs when the controller is destroyed. Use for cleanup (timers, subscriptions). |
class PageController extends Controller {
timer: number;
onInit() {
// Initialize data
this.store.init(m.count, 0);
// Start a timer
this.timer = window.setInterval(() => {
this.store.update(m.count, (c) => c + 1);
}, 1000);
}
onDestroy() {
// Clean up
window.clearInterval(this.timer);
}
}
Typed Controller Access
Use getControllerByType to get a typed reference to a controller. This provides full autocomplete and compile-time type checking:
<div controller={CounterController} class="flex flex-col gap-4">
<div class="flex gap-2 items-center">
<Button
onClick={(e, ins) => {
ins.getControllerByType(CounterController).decrement()
}}
>
-1
</Button>
<span class="w-12 text-center text-xl" text={m.count} />
<Button
onClick={(e, ins) => {
ins.getControllerByType(CounterController).increment()
}}
>
+1
</Button>
<Button
onClick={(e, ins) => {
ins.getControllerByType(CounterController).increment(10)
}}
>
+10
</Button>
<Button
onClick={(e, ins) => {
ins.getControllerByType(CounterController).reset()
}}
>
Reset
</Button>
</div>
</div> The getControllerByType method searches up the widget tree and returns a typed controller instance.
Triggers
Triggers watch store paths and run callbacks when values change. Use addTrigger in onInit:
class PageController extends Controller {
onInit() {
this.addTrigger(
"selection-changed",
[m.selectedId],
(selectedId) => {
if (selectedId) {
this.loadDetails(selectedId);
}
},
true,
); // true = run immediately
}
async loadDetails(id: string) {
let data = await fetch(`/api/items/${id}`).then((r) => r.json());
this.store.set(m.details, data);
}
}
The trigger name allows you to remove it later with removeTrigger("selection-changed").
Computables
Add computed values that automatically update when dependencies change:
class PageController extends Controller {
onInit() {
this.addComputable(m.fullName, [m.firstName, m.lastName], (first, last) => {
return `${first || ""} ${last || ""}`.trim();
});
this.addComputable(m.total, [m.items], (items) => {
return items?.reduce((sum, item) => sum + item.price, 0) || 0;
});
}
}
The first argument is the store path where the result is written. The computed value updates whenever any dependency changes.
Accessing Parent Controllers
Use getParentControllerByType to get a typed reference to a parent controller:
class ChildController extends Controller {
onSave() {
let parent = this.getParentControllerByType(PageController);
parent.saveChild(this.getData());
}
}
This provides full type safety and autocomplete. For dynamic method invocation by name, use invokeParentMethod:
this.invokeParentMethod("onSave", this.getData());