> ## Documentation Index
> Fetch the complete documentation index at: https://sprincul.sinfullycoded.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Hydrate Sprincul Models in Dynamically Injected HTML

> Sprincul does not auto-initialize elements added after the first init call. Learn how to hydrate new model roots using a manual reinit pattern.

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.

<Steps>
  <Step title="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.

    ```js theme={null}
    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 });
    ```
  </Step>

  <Step title="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.

    ```js theme={null}
    function reinitialize() {
      // Register any per-cycle callbacks before calling init
      Sprincul.onReady((models) => {
        console.log(`Reinitialized ${models.length} new model(s)`);
      });

      Sprincul.init();
    }
    ```
  </Step>

  <Step title="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.

    ```js theme={null}
    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>
      `);
    }
    ```
  </Step>
</Steps>

## Complete example

```js theme={null}
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.

<Note>
  For more context on what Sprincul does and does not track across DOM changes, see the [Limitations & FAQ](/reference/limitations-faq) page.
</Note>
