# Controllers

```ts
import { Controller } from 'cx/ui';
```



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:

```tsx
import { createModel } from "cx/data";
import { Controller } from "cx/ui";
import { Button, TextField } from "cx/widgets";

interface PageModel {
  name: string;
  greeting: string;
}

const m = createModel<PageModel>();

class PageController extends Controller {
  onInit() {
    this.store.init(m.name, "World");
  }

  greet() {
    let name = this.store.get(m.name);
    this.store.set(m.greeting, `Hello, ${name}!`);
  }

  clear() {
    this.store.delete(m.greeting);
  }
}

export default (
  <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>
);

```

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:

```tsx
import { createModel } from "cx/data";
import { NumberField } from "cx/widgets";
import { tpl } from "cx/ui";

interface PageModel {
  count: number;
  double: number;
}

const m = createModel<PageModel>();

export default (
  <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).  |

```tsx
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:

```tsx
import { createModel } from "cx/data";
import { Controller } from "cx/ui";
import { Button } from "cx/widgets";

interface PageModel {
  count: number;
}

const m = createModel<PageModel>();

class CounterController extends Controller {
  onInit() {
    this.store.init(m.count, 0);
  }

  increment(amount: number = 1) {
    this.store.update(m.count, (count) => count + amount);
  }

  decrement(amount: number = 1) {
    this.store.update(m.count, (count) => count - amount);
  }

  reset() {
    this.store.set(m.count, 0);
  }
}

export default (
  <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`:

```tsx
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:

```tsx
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:

```tsx
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`:

```tsx
this.invokeParentMethod("onSave", this.getData());
```