Skip to main content
Sprincul’s entire HTML surface is four kinds of attribute. You mark container elements with data-model, wire state updates to elements with data-bind-*, hide content until initialization with data-cloaked, and attach event handlers with native on<event> attributes. Sprincul reads these at init() time and does not require any special template syntax or build step.

data-model="<Name>"

Marks an element as the root of a model instance. Sprincul looks for all [data-model] elements in the document when Sprincul.init() runs and creates one model instance per element. The value must exactly match a name you passed to Sprincul.register() or Sprincul.registerAll() — casing is significant. Every data-bind-* and on<event> attribute inside this container is scoped to this model instance.
<div data-model="Counter">
  <button onclick="decrement">-</button>
  <input type="number" data-bind-count="showCount" readonly />
  <button onclick="increment">+</button>
</div>
You can use any block-level or sectioning element as the root:
<section data-model="Profile">
  <p data-bind-name="showName"></p>
  <p data-bind-email="showEmail"></p>
</section>
Nesting one data-model container inside another is allowed — each creates its own independent model instance. Bindings and events on an inner container are scoped to the inner model only.

data-bind-<prop>="<callback>"

Registers a reactive binding between a state property and a DOM element. When this.state.<prop> changes on the model, Sprincul calls <callback>(element) so your method can update that element directly. You decide exactly what “update” means — set textContent, change a class, update a value, anything. <prop> is the lowercase state property name. <callback> is the name of a method on your model class that accepts an HTMLElement as its first argument.
<div data-model="Counter">
  <input type="number" data-bind-count="showCount" readonly />
</div>
import { SprinculModel } from 'sprincul';

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

  /** @param {HTMLInputElement} el */
  showCount(el) {
    el.value = this.state.count;
  }
}
You can bind multiple elements to the same state property, and you can bind multiple different properties within the same model:
<section data-model="Profile">
  <h2 data-bind-name="renderName"></h2>
  <p data-bind-email="renderEmail"></p>
  <span data-bind-name="renderNameBadge"></span>
</section>
State property names used in data-bind-* attributes must be lowercase. Browsers lower-case data-* attribute names when parsing HTML, so data-bind-myProp becomes data-bind-myprop in the DOM. If your state key is myProp and your attribute is data-bind-myProp, the binding will never fire. Use this.state.myprop and data-bind-myprop consistently.

data-cloaked

Hides an element until Sprincul removes the attribute at the appropriate point in the initialization cycle. You provide the CSS rule that makes the hiding work — Sprincul only removes the attribute.
<style>
  [data-cloaked] { display: none; }
</style>
The behavior differs depending on where you place the attribute: On a data-model element — Sprincul removes data-cloaked after that model’s afterInit hook completes, including any async work. Use this for models that fetch data before displaying anything.
<div data-model="Profile" data-cloaked>
  <p data-bind-name="showName"></p>
  <p data-bind-email="showEmail"></p>
</div>
On the <body> element (or any element without data-model) — Sprincul removes data-cloaked immediately after all models’ afterInit hooks are called, without waiting for them to complete. Use this for a simple page-level fade-in when you don’t need to wait for async hooks.
<body data-cloaked>
  <div data-model="Counter">...</div>
  <div data-model="Profile">...</div>
</body>
You can combine both strategies. Model-level cloaking protects individual components that do heavy async work, while page-level cloaking prevents unstyled flashes across the rest of the page:
<style>[data-cloaked] { display: none; }</style>

<body data-cloaked>
  <!-- Uncloaked when afterInit resolves (waits for async) -->
  <div data-model="Profile" data-cloaked>
    <p data-bind-name="showName"></p>
  </div>

  <!-- Uncloaked together with the body, no extra waiting -->
  <div data-model="Counter">
    <input data-bind-count="showCount" readonly />
  </div>
</body>
For models that make API calls in afterInit, prefer model-level cloaking. It prevents users from briefly seeing placeholder or default state values before real data arrives.

on<event>="<methodName>"

Attaches a native DOM event listener to an element. The value is the name of a method on your model class — not inline JavaScript. Sprincul replaces the attribute with a proper addEventListener call and passes the native Event object to your handler. Common examples: onclick, oninput, onchange, onsubmit, onkeydown. Any valid DOM event name works as long as you prefix it with on.
<div data-model="Counter">
  <button onclick="decrement">-</button>
  <button onclick="increment">+</button>
  <button onclick="resetCounter">Reset</button>
</div>
import { SprinculModel } from 'sprincul';

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

  increment() {
    this.state.count++;
  }

  decrement() {
    this.state.count--;
  }

  resetCounter() {
    this.state.count = 0;
  }
}
For input and form events, your handler receives the native event object:
<div data-model="SearchBox">
  <input type="text" oninput="handleInput" />
  <button onclick="handleSubmit">Search</button>
</div>
import { SprinculModel } from 'sprincul';

export default class SearchBox extends SprinculModel {
  beforeInit() {
    this.state.query = '';
  }

  /** @param {InputEvent} e */
  handleInput(e) {
    this.state.query = e.target.value;
  }

  /** @param {MouseEvent} e */
  handleSubmit(e) {
    console.log('Searching for:', this.state.query);
  }
}
Sprincul removes the on<event> attribute from the DOM after wiring the listener, so the raw attribute value is never exposed as executable HTML. Your handler name is private to the model instance.