> ## Documentation Index
> Fetch the complete documentation index at: https://sprincul.sinfullycoded.com/llms.txt
> Use this file to discover all available pages before exploring further.

# SprinculModel Base Class: Properties, Hooks, and Methods

> Complete reference for SprinculModel: the $el property, the reactive state object, beforeInit, afterInit lifecycle hooks, and addComputedProp.

`SprinculModel` is the base class you extend to create reactive components. Every model you write inherits the `$el` property, the reactive `state` proxy, the two lifecycle hooks, and the `addComputedProp` helper. You never call `new SprinculModel()` yourself — Sprincul does that when it processes each `data-model` element during `Sprincul.init()`.

## Properties

### `$el`

The DOM element that this model instance is bound to — the element carrying the `data-model` attribute. All bindings and event listeners managed by this model are scoped to descendants of `$el`.

**Type:** `HTMLElement`

```ts theme={null}
import { SprinculModel } from 'sprincul';

class Banner extends SprinculModel {
  afterInit() {
    console.log(this.$el); // <div data-model="Banner">
    this.$el.classList.add('ready');
  }
}
```

***

### `state`

A reactive proxy over the model's internal nanostores `MapStore`. Read and write state as ordinary object properties. Every assignment to a state property triggers all `data-bind-<prop>` callbacks for that property and re-evaluates any computed properties that list it as a dependency.

**Type:** `Record<string, any>`

State property names must be **lowercase**. HTML lower-cases `data-*` attribute names when building the DOM, so a binding written as `data-bind-myProp` becomes `data-bind-myprop` — and Sprincul reads that lowercase attribute name to match against state keys. If you use mixed-case state keys your bindings will silently never fire.

```ts theme={null}
import { SprinculModel } from 'sprincul';

interface CounterState {
  count: number;
}

class Counter extends SprinculModel {
  state!: CounterState;

  beforeInit() {
    this.state.count = 0;  // initialize
  }

  increment() {
    this.state.count++;    // triggers showCount binding
  }
}
```

<Warning>
  Always mutate `this.state.<prop>`, not `this.<prop>`. Writing to `this.count` instead of `this.state.count` bypasses the reactive proxy entirely — bindings will not fire.
</Warning>

***

## Lifecycle hooks

Sprincul calls lifecycle hooks on each model instance during `Sprincul.init()`. You override them in your subclass; both are optional.

### `beforeInit()`

Called synchronously before bindings are attached to the DOM. This is the right place to initialize state values and register computed properties with `addComputedProp()`. Because bindings haven't been wired yet, you can freely set initial state without triggering renders.

**Return type:** `void | Promise<void>`

If you return a `Promise`, Sprincul logs any rejection but does **not** await it before wiring bindings. The async portion of `beforeInit` may finish after bindings are already active. For reliable async work, use `afterInit()` instead.

```ts theme={null}
import { SprinculModel } from 'sprincul';

class Totals extends SprinculModel {
  beforeInit() {
    this.state.price = 10;
    this.state.qty = 2;
    this.addComputedProp(
      'total',
      () => this.state.price * this.state.qty,
      ['price', 'qty']
    );
  }
}
```

```js theme={null}
// JavaScript
import { SprinculModel } from 'sprincul';

export default class Totals extends SprinculModel {
  beforeInit() {
    this.state.price = 10;
    this.state.qty = 2;
    this.addComputedProp(
      'total',
      () => this.state.price * this.state.qty,
      ['price', 'qty']
    );
  }
}
```

***

### `afterInit()`

Called after bindings and event listeners are active. This hook can be safely `async`. Use it for API calls, connecting to external stores, or any work that should happen once the component is fully wired.

**Return type:** `void | Promise<void>`

When `afterInit` completes (including any async work), Sprincul removes the `data-cloaked` attribute from the model's root element if one is present. This makes `afterInit` the right hook to drive model-level uncloaking.

```ts theme={null}
import { SprinculModel } from 'sprincul';

class Profile extends SprinculModel {
  beforeInit() {
    this.state.name = '--';
    this.state.email = '--';
  }

  showName(el: HTMLElement) {
    el.textContent = this.state.name;
  }

  showEmail(el: HTMLElement) {
    el.textContent = this.state.email;
  }

  async afterInit() {
    const { name, email } = await fetch('/api/me').then(r => r.json());
    this.state.name = name;
    this.state.email = email;
  }
}
```

```js theme={null}
// JavaScript
import { SprinculModel } from 'sprincul';

export default class Profile extends SprinculModel {
  beforeInit() {
    this.state.name = '--';
    this.state.email = '--';
  }

  /** @param {HTMLElement} el */
  showName(el) {
    el.textContent = this.state.name;
  }

  /** @param {HTMLElement} el */
  showEmail(el) {
    el.textContent = this.state.email;
  }

  async afterInit() {
    const { name, email } = await fetch('/api/me').then(r => r.json());
    this.state.name = name;
    this.state.email = email;
  }
}
```

<Tip>
  For models with heavy initialization — API calls, data processing — use `data-cloaked` on the model root. Sprincul removes it after `afterInit` resolves, so users never see an incomplete UI.
</Tip>

***

## Methods

### `addComputedProp(name, fn, dependencies)`

Registers a derived state value that is recomputed whenever any of its listed dependencies change. The computed value is accessible on `this.state.<name>` just like any other state property, and any `data-bind-<name>` elements are re-rendered when the value changes.

Call `addComputedProp` from inside `beforeInit()`. Calling it before the internal core is ready will throw.

<ParamField path="name" type="string" required>
  The state key under which the computed value is stored. Must be lowercase.
</ParamField>

<ParamField path="fn" type="() => any" required>
  A function that returns the computed value. Called once immediately to seed the initial value, then again whenever a dependency changes.
</ParamField>

<ParamField path="dependencies" type="string[]" required>
  An array of state property names that this computed property depends on. Sprincul re-runs `fn` and schedules a DOM update whenever any listed key changes. If you pass an empty array, the value is set once but bound elements will not re-render when it changes — Sprincul logs a warning in this case.
</ParamField>

**Returns:** `() => void` — An unsubscribe function. You can call it to manually remove the computed property listener. Sprincul also cleans it up automatically when the model root is removed from the DOM.

```ts theme={null}
import { SprinculModel } from 'sprincul';

class Totals extends SprinculModel {
  beforeInit() {
    this.state.price = 10;
    this.state.qty = 2;

    // Recomputes whenever price or qty changes
    this.addComputedProp(
      'total',
      () => this.state.price * this.state.qty,
      ['price', 'qty']
    );
  }

  /** @param {HTMLInputElement} e */
  setPrice(e: InputEvent) {
    this.state.price = Number((e.target as HTMLInputElement).value || 0);
  }

  /** @param {HTMLInputElement} e */
  setQty(e: InputEvent) {
    this.state.qty = Number((e.target as HTMLInputElement).value || 0);
  }

  showTotal(el: HTMLElement) {
    el.textContent = String(this.state.total);
  }
}
```

```js theme={null}
// JavaScript
import { SprinculModel } from 'sprincul';

export default class Totals extends SprinculModel {
  beforeInit() {
    this.state.price = 10;
    this.state.qty = 2;

    this.addComputedProp(
      'total',
      () => this.state.price * this.state.qty,
      ['price', 'qty']
    );
  }

  /** @param {InputEvent} e */
  setPrice(e) {
    this.state.price = Number(e.target.value || 0);
  }

  /** @param {InputEvent} e */
  setQty(e) {
    this.state.qty = Number(e.target.value || 0);
  }

  /** @param {HTMLElement} el */
  showTotal(el) {
    el.textContent = String(this.state.total);
  }
}
```

The matching HTML for the example above:

```html theme={null}
<div data-model="Totals">
  <label for="price">Price</label>
  <input id="price" type="number" value="10" oninput="setPrice" />
  <label for="qty">Quantity</label>
  <input id="qty" type="number" value="2" oninput="setQty" />
  <p>Total: <span data-bind-total="showTotal"></span></p>
</div>
```

<Note>
  Computed property values and their dependency updates are batched with `requestAnimationFrame`, the same as regular state changes. Multiple updates in a single frame coalesce into one render pass.
</Note>
