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.
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: Treatdata-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 aMutationObserver 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
Why isn't my data-bind-* callback firing?
Why isn't my data-bind-* callback firing?
Check the following in order:
-
The element is inside the
data-modelcontainer. Bindings are scoped to the model root. If the bound element is outside the container, Sprincul will not find it. -
The callback name matches a method on the class exactly. If your method is
showCountand your attribute isdata-bind-count="showcount"(wrong casing), the binding will not wire. -
You are mutating
this.state.<prop>, notthis.<prop>. Writing tothis.countbypasses the reactive proxy. Sprincul never sees the change and your bindings do not fire. -
The state property name is lowercase. Browsers lowercase
data-*attribute names when parsing HTML.data-bind-myPropbecomesdata-bind-mypropin the DOM. Your state key must match — usethis.state.mypropanddata-bind-myprop.
Do I need a bundler?
Do I need a bundler?
No. You can import Sprincul directly in a If you prefer a bundler (Vite, esbuild, Rollup), install from npm and import normally:
<script type="module"> tag using a CDN, your own package registry, or any bundler output. The CDN path via esm.sh works without any build step:When can I seed Sprincul.store values?
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.Why isn't my onReady() callback being called?
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 secondinit()call.
onReady before Sprincul.init():Can I use async functions in beforeInit()?
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.Why doesn't Sprincul auto-observe DOM changes?
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.