> ## 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.

# Sprincul Models: JavaScript Classes as Reactive Components

> In Sprincul, a model is a plain JavaScript class that extends SprinculModel. It holds reactive state and the methods that update your DOM.

A model is the core building block of a Sprincul application. Each model maps to a DOM element — the container marked with `data-model` — and manages the reactive state and behavior for everything inside it. Because models are plain JavaScript classes, you get full access to the language: private fields, inheritance, async methods, and any third-party library you want to use.

## Extending SprinculModel

Every model class extends `SprinculModel`, which is the base class Sprincul provides. Extending it gives your class access to `this.$el`, `this.state`, and `this.addComputedProp()`, along with the lifecycle hooks Sprincul calls during initialization.

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

export default class Profile extends SprinculModel {
  // Your state, methods, and lifecycle hooks go here
}
```

You register your model class by name, then reference that same name in HTML via the `data-model` attribute. Sprincul creates one instance of your class for each matching element it finds in the DOM.

## The `$el` property

When Sprincul initializes a model, it passes the root DOM element to the constructor and stores it as `this.$el`. This gives you a direct reference to the container element from anywhere in your class — useful when you need to query elements within the model's scope or read attributes on the root.

```js theme={null}
export default class MyModel extends SprinculModel {
  afterInit() {
    // Access the root element directly
    const heading = this.$el.querySelector('h1');
    console.log(heading?.textContent);
  }
}
```

## The `state` object

`this.state` is your model's reactive store. It is a proxy backed by a [nanostores](https://github.com/nanostores/nanostores) `MapStore`, so any assignment triggers reactivity — Sprincul detects the change and schedules DOM updates for elements bound to that property.

You initialize state properties by assigning them in `beforeInit()`:

```js theme={null}
export default class Counter extends SprinculModel {
  beforeInit() {
    this.state.count = 0;
  }

  increment() {
    this.state.count++; // triggers bound element updates
  }
}
```

Reading from `this.state` works the same way — it always returns the current value. Computed properties registered with `addComputedProp()` are also accessible through `this.state` alongside regular state values.

<Warning>
  State property names referenced in `data-bind-*` attributes must be **lowercase**. HTML lowercases attribute names when building the DOM, so `data-bind-btnText` becomes `data-bind-btntext`. Sprincul uses the lowercased form to look up bindings, meaning a state property named `btnText` will never match. Use `this.state.btntext` instead.
</Warning>

## A complete model example

The `Profile` model below shows the full pattern: state initialization in `beforeInit()`, binding callbacks that read from `this.state`, and an async `afterInit()` that hydrates state from a data source.

```js theme={null}
// Profile.js
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 fakeFetch() {
    return new Promise((resolve) => {
      const result = () =>
        resolve({
          name: 'Jane Doe',
          email: 'jane@email.com',
        });
      setTimeout(result, 2000);
    });
  }

  async updateUI() {
    const { name, email } = await this.fakeFetch();
    this.state.name = name;
    this.state.email = email;
  }

  async afterInit() {
    await this.updateUI();
  }
}
```

The corresponding HTML:

```html theme={null}
<section data-model="Profile">
  <p data-bind-name="showName"></p>
  <p data-bind-email="showEmail"></p>
</section>
```

## Multiple instances on the same page

You can have multiple elements with the same `data-model` value. Sprincul creates a separate instance of your class for each one, and each instance manages only the state and bindings within its own container. The instances do not share state by default.

```html theme={null}
<!-- Two independent Profile instances -->
<section data-model="Profile">
  <p data-bind-name="showName"></p>
  <p data-bind-email="showEmail"></p>
</section>

<section data-model="Profile">
  <p data-bind-name="showName"></p>
  <p data-bind-email="showEmail"></p>
</section>
```

If you need models to share state, use the [global store](/reference/sprincul-api#store).

## Lifecycle hooks

`SprinculModel` defines two optional lifecycle hooks you can override in your class:

* **`beforeInit()`** — runs synchronously before bindings are attached. Use it to initialize state and register computed properties. See the [Lifecycle](/concepts/lifecycle) page for details.
* **`afterInit()`** — runs after bindings are active and can be `async`. Use it for API calls, external store connections, and any work that depends on bindings already being set up. See the [Lifecycle](/concepts/lifecycle) page for details.
