Skip to main content
This guide walks you through building a reactive counter component from scratch. By the end, you will have a working example that demonstrates how Sprincul connects JavaScript state to HTML through data attributes — and you will understand the pattern well enough to apply it to your own components.
1

Define a model

Each component is a standard JavaScript class that extends SprinculModel. Initialize your reactive state on this.state inside beforeInit(), then add methods to update the DOM and handle events.The beforeInit() hook runs before Sprincul attaches any bindings, so it is the right place to set up initial state and computed properties. Any method you define on the class can be referenced directly from your HTML.
// 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;
  }
}
this.state.count is reactive. Whenever its value changes, Sprincul automatically calls any DOM-bound callbacks that depend on it — in this case, showCount.
2

Register and initialize

Before calling Sprincul.init(), register every model class you want Sprincul to recognize. Use register() for a single model, or registerAll() to register several at once.
// main.js — register a single model
import { Sprincul } from 'sprincul';
import Counter from './Counter.js';

Sprincul.register('Counter', Counter);
Sprincul.init();
When you have multiple models, registerAll() keeps things tidy:
// main.js — register multiple models at once
import { Sprincul } from 'sprincul';
import Counter from './Counter.js';
import UserProfile from './UserProfile.js';
import ShoppingCart from './ShoppingCart.js';

Sprincul.registerAll({
  Counter,
  UserProfile,
  ShoppingCart
});
Sprincul.init();
All model registrations must happen before you call Sprincul.init(). After initialization runs, any models registered afterward will not be picked up until you call Sprincul.init() again.
When Sprincul.init() runs, it scans the DOM for every element with a data-model attribute, creates an instance of the matching class, wires up your data-bind-* bindings and event listeners, and then runs the lifecycle hooks (beforeInit followed by afterInit).
3

Annotate your HTML

Wrap each model in a container element with a data-model attribute set to the name you registered. Place your bindings and event attributes anywhere inside that container.
<div data-model="Counter">
  <p>Counter</p>
  <button onclick="decrement">-</button>
  <input type="number" data-bind-count="showCount" readonly />
  <button onclick="increment">+</button>
  <button type="reset" onclick="resetCounter">reset</button>
</div>
Here is how each attribute type works:
  • data-model="Counter" — marks this element as the root of a Counter model instance. Sprincul creates one class instance per root element.
  • data-bind-count="showCount" — whenever this.state.count changes, Sprincul calls showCount(element) with the bound element as the argument, letting your method update it however you like.
  • onclick="decrement" — Sprincul converts this into a proper addEventListener('click', ...) call on your model instance, then removes the inline attribute. The native Event object is passed to the handler.
State property names used in data-bind-* attributes must be lowercase. Browsers normalize data-* attribute names to lowercase when building the DOM, so data-bind-count maps to this.state.count, not this.state.Count.
That is all it takes. Load your main.js as a module, open the page, and the counter is live:
<script type="module" src="./main.js"></script>