Skip to main content
Data bindings are the mechanism Sprincul uses to keep your DOM in sync with your model’s state. Instead of writing code that manually reaches into the DOM after every state change, you declare the relationship once in HTML — and Sprincul takes care of calling the right method whenever the state changes.

The data-bind-<prop> attribute

You add a data-bind-<prop> attribute to any element inside a data-model container. The attribute name encodes the state property to watch, and the attribute value is the name of the callback method on your model that Sprincul should call when that property changes.
<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;
  }
}
When this.state.count changes, Sprincul calls showCount(element), passing the bound element as the first argument. Your callback reads from this.state and updates the element however it needs to — setting textContent, toggling a class, updating an attribute, anything. Sprincul also calls each binding callback once immediately when bindings are first set up, so the element reflects the initial state without you having to trigger a change manually.

How the binding pattern works

1

Parse the attribute

During Sprincul.init(), Sprincul reads every data-bind-* attribute inside the model’s container. The part after data-bind- is the state property name; the attribute value is the callback method name.
2

Call the callback immediately

Sprincul calls the callback right away with the bound element, so the element reflects the current (initial) state.
3

Watch for state changes

Whenever this.state.<prop> is assigned a new value, Sprincul schedules a DOM update via requestAnimationFrame. On the next frame, it calls the callback again with the same element.
Your callback always receives the bound DOM element as its first argument and has access to the full model state through this.state.

Lowercase state property names

State property names used in data-bind-* attributes must be lowercase. Browsers lowercase all HTML attribute names when parsing the DOM, so data-bind-btnText is stored as data-bind-btntext. Sprincul reads the lowercased form, so a state property named btnText will never match a binding. Use this.state.btntext (all lowercase) to ensure bindings fire correctly.

A full example: Profile

The following Profile model and HTML show how bindings connect two state properties to two separate elements. Sprincul calls showName and showEmail whenever the corresponding state property changes.
<section data-model="Profile">
  <p data-bind-name="showName"></p>
  <p data-bind-email="showEmail"></p>
</section>
// 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 afterInit() {
    const { name, email } = await this.fakeFetch();
    this.state.name = name;
    this.state.email = email;
  }

  async fakeFetch() {
    return new Promise((resolve) => {
      setTimeout(() => resolve({ name: 'Jane Doe', email: 'jane@email.com' }), 2000);
    });
  }
}

Counter bindings

The Counter model uses the same pattern with a number input. Multiple elements can bind to the same state property — Sprincul calls each callback independently.
<div data-model="Counter">
  <button onclick="decrement">-</button>
  <input type="number" data-bind-count="showCount" readonly />
  <button onclick="increment">+</button>
  <button type="reset" onclick="resetCounter">reset</button>
</div>
// Counter.js
import { SprinculModel } from 'sprincul';

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

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

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

  /** @param {HTMLInputElement} el */
  showCount(el) {
    el.value = this.state.count;
  }

  resetCounter() {
    this.state.count = 0;
  }
}

Multiple bindings for the same property

You can put data-bind-<prop> on as many elements as you need. When the state property changes, Sprincul calls the associated callback for each bound element. The callbacks can be different methods, or the same method — whatever makes sense for each element.
<div data-model="Counter">
  <!-- Two elements both respond to count changes -->
  <input type="number" data-bind-count="showCount" readonly />
  <p data-bind-count="showCountText"></p>
</div>

Bindings must be inside the model container

Sprincul only processes data-bind-* attributes that are direct or nested descendants of the data-model container. Elements outside the container are ignored. Nested data-model elements are managed by their own model instance and are not included in the parent’s bindings.
If a binding callback is not firing, check that: the element is inside the correct data-model container, the callback name matches a method defined on the class, and you are assigning to this.state.<prop> rather than a plain property like this.<prop>.