Skip to main content
Sprincul is designed around a single explicit initialization call. When you call Sprincul.init(), it scans the DOM for every [data-model] element present at that moment, wires up bindings and event handlers, and runs the lifecycle hooks. Any data-model elements you add to the DOM afterward are not processed automatically.

Why Sprincul does not auto-observe

Scanning the DOM continuously for new elements means running hidden work on every mutation — work that may conflict with how your own code manages the DOM. Keeping initialization explicit makes it easier to predict exactly when models start up and avoids surprise re-initialization in apps that already control their own DOM lifecycle. If you do need to initialize models in markup injected after the first init() call, you can add that behavior yourself with a short amount of code.

The manual reinit pattern

The pattern has three steps: observe the DOM for your injected containers, register any onReady callbacks you need for that cycle, then call Sprincul.init() again. Sprincul skips elements it has already initialized, so only the newly added roots are processed.
1

Add a MutationObserver in your own code

Set up the observer before or after calling Sprincul.init(). Watch the parent element where you plan to inject new model markup.
const observer = new MutationObserver((mutations) => {
  let hasNewModelRoots = false;

  for (const mutation of mutations) {
    for (const node of mutation.addedNodes) {
      if (node.nodeType !== Node.ELEMENT_NODE) continue;
      const el = /** @type {HTMLElement} */ (node);

      // Check whether the added node itself, or any child, is a model root
      if (el.dataset.model || el.querySelector('[data-model]')) {
        hasNewModelRoots = true;
      }
    }
  }

  if (hasNewModelRoots) {
    reinitialize();
  }
});

observer.observe(document.body, { childList: true, subtree: true });
2

Register onReady callbacks for the new cycle

onReady callbacks are consumed once per init() call. If you want a callback to run after the next init, register it fresh before calling Sprincul.init() again.
function reinitialize() {
  // Register any per-cycle callbacks before calling init
  Sprincul.onReady((models) => {
    console.log(`Reinitialized ${models.length} new model(s)`);
  });

  Sprincul.init();
}
3

Inject your markup and let the observer trigger reinit

When you insert new HTML containing data-model elements, the observer fires, which calls reinitialize(), which processes only the new roots.
function loadCard(userData) {
  const container = document.getElementById('cards');

  // Inject new model markup — the MutationObserver picks this up
  container.insertAdjacentHTML('beforeend', `
    <div data-model="UserCard" data-cloaked>
      <p data-bind-username="showUsername"></p>
      <button onclick="follow">Follow</button>
    </div>
  `);
}

Complete example

import { Sprincul } from 'sprincul';
import UserCard from './UserCard.js';

// Register the model class
Sprincul.register('UserCard', UserCard);

// Run the first initialization pass
Sprincul.init();

// Set up an observer to handle later injections
const observer = new MutationObserver((mutations) => {
  let hasNewModelRoots = false;

  for (const mutation of mutations) {
    for (const node of mutation.addedNodes) {
      if (node.nodeType !== Node.ELEMENT_NODE) continue;
      const el = /** @type {HTMLElement} */ (node);

      if (el.dataset.model || el.querySelector('[data-model]')) {
        hasNewModelRoots = true;
      }
    }
  }

  if (hasNewModelRoots) {
    Sprincul.onReady((models) => {
      console.log('New models initialized:', models.map(m => m.name));
    });
    Sprincul.init();
  }
});

observer.observe(document.body, { childList: true, subtree: true });

// Simulate dynamic injection (e.g., after a fetch)
document.getElementById('load-more').addEventListener('click', async () => {
  const users = await fetchUsers();

  users.forEach((user) => {
    document.getElementById('cards').insertAdjacentHTML('beforeend', `
      <div data-model="UserCard" data-cloaked>
        <p data-bind-username="showUsername"></p>
        <p data-bind-bio="showBio"></p>
        <button onclick="follow">Follow</button>
      </div>
    `);
  });
});

Cloaking still works in subsequent init calls

If you add data-cloaked to your injected model markup, cloaking behaves exactly the same as on the first pass. Model-level cloaking waits for afterInit() to complete before revealing the element, which is especially useful when each card needs to fetch its own data after mounting.

Already-initialized roots are skipped

Sprincul tracks which elements it has processed using a WeakSet. When you call Sprincul.init() again, it queries the full document for [data-model] elements but skips any it has already seen. You do not need to scope the second call or pass a container reference — the framework handles deduplication internally.
For more context on what Sprincul does and does not track across DOM changes, see the Limitations & FAQ page.