> ## 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.

# Sprincul Limitations, FAQ, and Troubleshooting Guide

> Known limitations of Sprincul's DOM model, plus answers to common questions about bindings, events, initialization order, and the global store.

Sprincul is designed to be explicit and predictable. It makes deliberate trade-offs around DOM observation and initialization that differ from more heavyweight frameworks. This page documents those trade-offs and answers the questions that come up most often.

## Limitations

### Sprincul does not auto-initialize elements added after `init()`

Sprincul scans the DOM once when `Sprincul.init()` is called. Elements added to the DOM after that point — for example, markup injected by a fetch response or a client-side router — are not automatically hydrated.

**Workaround:** Call `Sprincul.init()` again after injecting new markup. Elements that are already initialized are skipped, so only the new roots are processed. If you use `onReady` callbacks, re-register them before the new `init()` call.

```js theme={null}
// Inject new markup
document.querySelector('#container').insertAdjacentHTML(
  'beforeend',
  '<div data-model="NewWidget">...</div>'
);

// Re-register any onReady listeners needed for this cycle
Sprincul.onReady((models) => {
  console.log('New models initialized:', models.length);
});

// Re-run init — existing roots are skipped automatically
Sprincul.init();
```

### Sprincul does not try to rebuild itself around external DOM rewrites

If code outside of Sprincul replaces the inner HTML of an already-initialized model root, Sprincul does not attempt to re-wire the bindings and event listeners inside it. The original listeners are gone, and Sprincul has no way to know the DOM was rewritten.

**Recommendation:** Treat `data-bind-*` callbacks as the single source of DOM mutations inside a model root. Let state drive the UI rather than mixing Sprincul bindings with ad hoc `innerHTML` assignments.

### Cleanup on removal is automatic

When a model root is removed from the DOM, Sprincul detects the removal via a `MutationObserver` on `document.body` and cleans up all bindings, event listeners, and computed property subscriptions for that root and any nested model roots. You do not need to call any cleanup function manually.

***

## FAQ

<AccordionGroup>
  <Accordion title="Why isn't my data-bind-* callback firing?">
    Check the following in order:

    1. **The element is inside the `data-model` container.** Bindings are scoped to the model root. If the bound element is outside the container, Sprincul will not find it.

    2. **The callback name matches a method on the class exactly.** If your method is `showCount` and your attribute is `data-bind-count="showcount"` (wrong casing), the binding will not wire.

    3. **You are mutating `this.state.<prop>`, not `this.<prop>`.** Writing to `this.count` bypasses the reactive proxy. Sprincul never sees the change and your bindings do not fire.

    4. **The state property name is lowercase.** Browsers lowercase `data-*` attribute names when parsing HTML. `data-bind-myProp` becomes `data-bind-myprop` in the DOM. Your state key must match — use `this.state.myprop` and `data-bind-myprop`.
  </Accordion>

  <Accordion title="Do I need a bundler?">
    No. You can import Sprincul directly in a `<script type="module">` tag using a CDN, your own package registry, or any bundler output. The CDN path via [esm.sh](https://esm.sh) works without any build step:

    ```html theme={null}
    <script type="module">
      import { Sprincul } from 'https://esm.sh/sprincul';
      import Counter from './Counter.js';

      Sprincul.register('Counter', Counter);
      Sprincul.init();
    </script>
    ```

    If you prefer a bundler (Vite, esbuild, Rollup), install from npm and import normally:

    ```bash theme={null}
    npm install sprincul
    ```

    ```js theme={null}
    import { Sprincul } from 'sprincul';
    ```
  </Accordion>

  <Accordion title="When can I seed Sprincul.store values?">
    Any time. You can call `Sprincul.store.set()` before registering models, before `Sprincul.init()`, from inside a model's `beforeInit()` or `afterInit()`, or from any other part of your application code after initialization. The store is independent of the initialization lifecycle.

    ```js theme={null}
    // Seed before init — models can read this in beforeInit()
    Sprincul.store.set('theme', 'dark');
    Sprincul.store.set('locale', 'en-US');

    Sprincul.register('Header', Header);
    Sprincul.init();
    ```
  </Accordion>

  <Accordion title="Why isn't my onReady() callback being called?">
    `onReady` callbacks are **one-shot per init cycle**. After all callbacks fire, Sprincul clears the internal list. This means:

    * If you register a callback **after** `Sprincul.init()` has already run, it will never be called for that cycle.
    * If you call `Sprincul.init()` a second time (for example, after injecting new markup), you must re-register your callback before that second `init()` call.

    Always register `onReady` before `Sprincul.init()`:

    ```js theme={null}
    // Correct order
    Sprincul.register('Counter', Counter);
    Sprincul.onReady((models) => { /* ... */ });
    Sprincul.init();
    ```
  </Accordion>

  <Accordion title="Can I use async functions in beforeInit()?">
    You can mark `beforeInit` as `async`, but the async portion may not complete before Sprincul attaches bindings. Sprincul calls `beforeInit` synchronously and does not await the returned `Promise` before wiring the DOM.

    If you need the result of an async operation before your bindings reflect real data, use `afterInit()` instead. It is designed for async work and runs after bindings are active. The model-level `data-cloaked` attribute lets you hide the component until `afterInit` resolves, preventing users from seeing stale initial state.

    ```js theme={null}
    // Avoid: async work in beforeInit may not finish before bindings attach
    async beforeInit() {
      const data = await fetch('/api/data').then(r => r.json()); // may be too late
      this.state.value = data.value;
    }

    // Prefer: use afterInit for async work
    async afterInit() {
      const data = await fetch('/api/data').then(r => r.json());
      this.state.value = data.value; // bindings fire correctly here
    }
    ```
  </Accordion>

  <Accordion title="Why doesn't Sprincul auto-observe DOM changes?">
    Automatically re-scanning the DOM on every mutation would run hidden work in the background. In apps that already manage the DOM themselves — server-rendered pages, partial HTML responses, client-side routers — continuous observation makes it hard to predict when initialization runs or whether it interferes with existing DOM management.

    Keeping initialization explicit makes the framework's behavior transparent. You call `Sprincul.init()` when you're ready, on your schedule, and nothing happens behind the scenes without your knowledge. The workaround for dynamic content — inject markup, then call `init()` again — requires a few extra lines but gives you full control over when and how new components are hydrated.
  </Accordion>
</AccordionGroup>
