Skip to main content
Every Sprincul model goes through a predictable initialization sequence when Sprincul.init() runs. Understanding this sequence tells you exactly when your state, bindings, and async operations are active — and which hook is the right place for each kind of work.

The initialization flow

When you call Sprincul.init(), Sprincul queries the DOM for every element with a data-model attribute and processes each one in order:
1

Sprincul.init() scans the DOM

Sprincul finds all [data-model] elements and looks up the registered class for each one.
2

Model instance is created

Sprincul calls new YourModel(element), which sets up this.$el and the reactive this.state proxy.
3

beforeInit() is called

Sprincul calls beforeInit() synchronously. This is where you initialize state values and register computed properties — before any bindings exist.
4

Bindings and event listeners are attached

Sprincul reads every data-bind-* and on<event> attribute in the container and wires them to your model’s methods. Each binding callback is called once immediately with the current state.
5

afterInit() is called

Sprincul calls afterInit(). Bindings are already active at this point, so any state changes here will trigger DOM updates. afterInit() can be async.
6

Ready events fire

After all models’ afterInit hooks are called (not necessarily awaited to completion), Sprincul fires onReady callbacks and dispatches the sprincul:ready DOM event.

beforeInit()

beforeInit() runs synchronously before Sprincul attaches any bindings to the DOM. Use it to:
  • Set initial values on this.state
  • Register computed properties with this.addComputedProp()
  • Run any synchronous setup that must complete before bindings are active
import { SprinculModel } from 'sprincul';

export default class Counter extends SprinculModel {
  beforeInit() {
    this.state.count = 0;
    this.addComputedProp(
      'doubled',
      () => this.state.count * 2,
      ['count']
    );
  }
}
beforeInit() is called synchronously, but it can return a Promise. If it does, Sprincul will not await it — bindings are set up immediately after beforeInit() returns. Keep synchronous work in beforeInit() and defer async work to afterInit().

afterInit()

afterInit() runs after bindings are fully active. Use it to:
  • Fetch data from an API and update this.state
  • Connect to an external store or pub/sub system
  • Start timers or observers that rely on the DOM being ready
afterInit() can be async. Sprincul does not block the rest of initialization while it completes — it calls afterInit(), captures the returned promise, and moves on to the next model. When the promise settles, Sprincul removes data-cloaked from the model’s root element if present.
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/profile').then(r => r.json());
    this.state.name = name;
    this.state.email = email;
  }
}

Sprincul.onReady(callback)

Register a callback to be notified when all models have been initialized. The callback receives an array of SprinculModelInfo objects, one per model instance Sprincul processed.
import { Sprincul } from 'sprincul';
import Counter from './Counter.js';

Sprincul.register('Counter', Counter);

Sprincul.onReady((models) => {
  console.log(`Sprincul initialized ${models.length} models`);
  models.forEach(({ name, element }) => {
    console.log(`Model "${name}" on element:`, element);
  });
});

Sprincul.init();
You must register onReady callbacks before calling Sprincul.init(). Callbacks registered after init() will not be invoked for that initialization cycle. onReady callbacks are one-shot — they fire once per init() call and are then cleared.

The sprincul:ready DOM event

As an alternative to onReady, you can listen for the sprincul:ready CustomEvent on the document. It fires at the same time as onReady callbacks and carries the same model information in event.detail.models.
document.addEventListener('sprincul:ready', ({ detail }) => {
  const { models } = detail;
  console.log(`Initialized ${models.length} models`);
  models.forEach(({ name, element }) => {
    console.log(`Model "${name}" on element:`, element);
  });
});

Sprincul.init();
Both approaches give you access to the same data. Use whichever fits your codebase — they are equivalent.

Development mode

Pass { devMode: true } to Sprincul.init() to enable additional diagnostics:
  • Console warnings when bindings reference callbacks that don’t exist on the model.
  • Console warnings when event attributes reference methods that don’t exist on the model.
  • The instance property on each SprinculModelInfo object, giving you direct access to the model instance in onReady callbacks and the sprincul:ready event.
Sprincul.onReady((models) => {
  models.forEach(({ name, element, instance }) => {
    // instance is the live model object — useful for debugging in the console
    console.log(name, instance);
  });
});

Sprincul.init({ devMode: true });
Security note: Model instances are excluded from ready events in production (when devMode is false). This prevents internal state from being accessible through the browser console. Only enable devMode during local development.

Cloaking elements during initialization

Sprincul supports a data-cloaked attribute that hides elements until they are ready. You provide the CSS rule; Sprincul removes the attribute at the right time.
<style>[data-cloaked] { display: none; }</style>

<!-- Model-level: uncloaks after this model's afterInit() completes -->
<div data-model="Profile" data-cloaked></div>

<!-- Page-level: uncloaks after all models' afterInit() hooks are called -->
<body data-cloaked>
  <div data-model="Profile"></div>
  <div data-model="Settings"></div>
</body>
Model-level cloaking waits for afterInit() to fully resolve, making it safe for models that fetch data before rendering. Page-level cloaking removes as soon as all afterInit() calls are dispatched — not when they complete — so it is better suited for showing a loading state than for waiting on async work.