Skip to main content
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.
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.
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 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():
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.
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.

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.
// 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:
<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.
<!-- 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.

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 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 page for details.