Release notes
Changelog
All notable changes to WildflowerJS. Per-entry deep links match the one-line summaries in the package CHANGELOG.md. The format follows Keep a Changelog; the project follows Semantic Versioning.
1.2.0 — 2026-07-07
Under the hood
Reactivity rebuilt as a single dependency graph #
State, computeds, effects, and bindings are now nodes in one unified dependency graph, resolved in a single pass. This replaces the previous split between the reactive state manager and a parallel binding-context system. There is no API change and nothing to migrate; the result is a smaller core, faster reads of values that have not changed, leaner effect scheduling and dependency re-tracking, and a single place where dependency tracking is defined, which is where several of this release's reactivity fixes originate.
List rendering rebuilt on targeted updates #
data-list no longer creates a reactive effect per row. Each list now registers its rows' bound fields on one per-list update dispatcher, and an in-place field change (items[3].qty = 5) routes to exactly the bindings that read that field, down to a single direct DOM write for a plainly bound field. There is no API change and nothing to migrate.
The results: roughly 20 to 25% less memory per row on lists whose templates use item-level computed properties, faster targeted updates and removals, a smaller rendering pipeline, and one update path where there used to be four. That last point mattered beyond the numbers: unifying the paths surfaced several latent inconsistencies between them, all found and fixed during this cycle, before they could ever reach a release.
Added
wildflower.unregister(name) unified component/store teardown #
A single teardown entry point that removes a component definition, destroying its live instances and disposing their reactive state, effects, and contexts, and/or disposes a store of that name, so the name is free to register again with a fresh definition. Registration was previously first-write-wins with no way to replace a definition; unregister fills that gap. It is safe to call for an unknown name and handles a name used by both a component and a store. The primary uses are live-preview and hot-module-reload teardown before re-registering the same name, dynamic applications that swap components at runtime, and tests that need a clean registry between cases.
Dev-mode warning (WF-215) for a re-registration with a different definition #
Re-registering a component or store under a name that already exists is silently skipped, keeping the original definition. When the incoming definition actually differs from the stored one, that is almost always an accidental collision (two components sharing a name, a hot reload without teardown), so development builds now warn (WF-215), name the entity, and point at wildflower.unregister. Identical re-registrations, such as the same definition scanned twice, stay quiet. The comparison hashes method source rather than only comparing shape, so two definitions that share method names but differ in a method body are still flagged. Stripped entirely from production builds. See WF-215.
Iteration dependencies are tracked #
A computed or binding that iterates a reactive object's keys, whether through Object.keys, for...in, or spread, now re-runs when a key is added or removed. Previously only reassigning the whole object woke iteration readers, so a computed like Object.keys(this.saved).length went silently stale when a key was added or deleted by direct mutation. Updating an existing key's value still does not re-run keys-only readers, so per-field writes on hot paths stay exactly as cheap as before.
Dev-mode warning (WF-214) for zero-arg computeds reading item properties #
Item-level computed properties receive the list item as their first argument. A computed declared without parameters evaluates at component scope, so a this read of an item field inside it silently resolves undefined. Development builds now warn (WF-214) when such a read misses on the component while the current list item has a property of that name, naming the computed and property and suggesting the (item) signature. Zero-arg computeds that read only component state remain legitimate inside rows and never trigger the warning. Fires once per component and computed; stripped entirely from production builds. See WF-214.
The data-csp-safe script attribute #
On pages served with a strict Content Security Policy, add data-csp-safe to the framework script tag and WildflowerJS starts directly in CSP-safe mode, never attempting dynamic code evaluation at all. The page produces zero CSP violations and zero report-uri reports. Without the attribute the framework still auto-detects the policy and falls back to its CSP-safe expression parser, at the cost of one benign violation report from the startup capability probe. See the Expressions documentation for CSP mode details and a live example.
Custom directives and lifecycle hooks in every build #
The declarative data-* custom-directive API and the component lifecycle hooks were previously bundled only with the plugin system, which left them out of the lite and mini builds. They now ship in all five build variants. The heavier plugin() registration and dependency-injection APIs stay gated to the full, spa, and standard builds. This change also fixes a latent crash in lite and mini, where the directive scan read .size on a registry that was never initialized in those variants.
DevTools timeline observability #
New introspection on the global DevTools hook, available in development builds only: a per-frame timeline records microtask drains, effect runs, and render-sweep duration. It is tree-shaken out of production builds, where only the hook's schemaVersion, version, and dev fields remain for capability detection. The data surfaces in the companion DevTools extension.
Dev-mode warning when a computed reads pool.length / pool.size #
pool.length and pool.size are plain, non-reactive getters. A computed property that reads one therefore caches a single value on its first evaluation and never re-runs, so any UI bound to it goes silently stale as the pool grows or shrinks. Development builds now emit a warning, at most once per pool, that names the offending computed and points to the fix: mirror the count into reactive state updated from tick(). The check lives entirely in a development-only path and adds nothing to production builds.
Breaking Changes
A non-boolean attribute bound to false now renders ="false" instead of being removed #
The component binding path previously removed a non-boolean attribute when its bound value was false; it now writes the literal string, matching the list path and the documented contract that non-boolean attributes hold their literal value. Boolean attributes are unchanged: false still removes them, since for a boolean attribute presence is the value. For most bindings this is more correct, and ARIA states such as aria-expanded now emit ="false" (the accessible form) instead of vanishing. The one place it can bite is a CSS attribute-presence selector used as a flag: an element bound { 'data-active': isActive } with isActive === false now matches [data-active], because it renders data-active="false", where before the attribute was absent. Migration: if you relied on a falsey bind removing such an attribute, bind null or undefined (both still remove it) instead of false, or select on the value ([data-active="true"]) rather than on presence.
Fixed
wildflower.config({ forceCSPMode: true }) now takes effect at runtime #
The documented call for forcing CSP-safe expression evaluation updated the configuration option without switching the live evaluator, so it was silently ineffective; and had the switch occurred, the first expression compiled afterward would have failed on an internal cache that only construction-time CSP mode initialized. Both are fixed: expressions compiled after the call now evaluate through the CSP-safe parser. For zero-violation strict-CSP pages, prefer the new data-csp-safe attribute, which takes effect before the framework's startup probe runs.
data-action on a component's own root element now binds #
<div data-component="x" data-action="click:save"> never wired up. The action scan used querySelectorAll, which by specification excludes the element it is called on, so an action declared on a component's own root was silently skipped. The scan now also checks the root element itself.
data-bind-html expressions re-apply on a targeted single-prop update #
When a single item property changed, the targeted-rebind path checked only each binding's path, not the variables its expression actually read. A data-bind-html expression that referenced the changed property resolved the new value but skipped the innerHTML write, leaving stale markup on screen. The targeted-rebind filter now accounts for an html expression's variables.
A nested item prop read only through an expression binding now reacts #
A binding such as data-bind-class="user.active ? 'on' : 'off'", and the equivalent style, attr, show, and render expressions, failed to update when user.active mutated in the case where no other binding on the element read that nested leaf. Only the root identifier (user) was being registered as a dependency, not the full dotted path. The whole path is now registered.
Expression and component-root bindings track dependencies consistently #
Dependency registration for expression bindings (data-bind-html, data-show) and for bindings on a component's root element is now driven by a single dependency descriptor that every consumer reads from. This closes a class of drift where one pass handled plain path bindings correctly while a parallel pass missed html or show expressions, or root-element bindings, leaving them under-tracked.
Removed an orphaned profiling timer in the data-render-in-list re-run path #
The re-run path for a data-render inside a list referenced a profiling timer that was never initialized, throwing a TypeError on every re-run. The surrounding effect's try/catch swallowed the error, but it still spammed the console and left two stray performance.now() calls running in production, since timing calls are not stripped by minification. The dead timer has been removed.
Reactivity gaps closed in item-level-computed and data-render list paths #
Three related gaps. A per-item computed that reads component-level state behind a short-circuit (for example openField === 'status' && openId === item.id) now wakes when that state changes; previously the unread branch never registered as a dependency and the binding stayed visually stuck, which is the popover-style toggle that surfaced this. A per-item data-render insert is no longer immediately undone by a stale cached false, because the conditional cache is now cleared before the data is resolved. And a data-render placeholder comment node is guarded against, so it no longer triggers a swallowed error while the renderer checks for a custom-element tag name.
data-pool binding errors surface in dev instead of failing silently #
A data-bind, -class, -style, or -attr expression in a data-pool template that threw was caught and skipped on every flush with nothing logged, so a typo failed invisibly, and in animation mode it failed on every single frame. Development builds now warn once per offending binding, naming the pool and the error. Production builds are unaffected and carry no added cost.
Passive data-pools filled one item at a time no longer wake the animation loop #
A passive pool, one that applies updates synchronously and skips the per-frame flush, still started the shared requestAnimationFrame loop when it was populated one item at a time through single add() calls. That was an idle wakeup with no work to do. Single-add now respects the same passive guard the bulk-add path already had, so a passive pool never spins the animation loop.
data-show toggles the .wf-show class on every path #
The documented anti-flash rule [data-show]:not(.wf-show) { display: none } relies on the .wf-show class being present once an element is visible, but data-show inside components and list rows toggled display without ever adding the class, so the CSS guard kept those elements hidden. The class is now written wherever a data-show verdict is applied, so the anti-flash contract holds in components and lists, not only context-bound elements.
data-bind-attr clears keys dropped from a bound object on the component path #
A key removed from a bound attribute object now removes its attribute from the element instead of leaving the previous value behind. The list path already did this; the component effect path did not, so a stale attribute could linger after the bound object stopped including it. (The related change to how a non-boolean false renders is listed under Breaking Changes.)
data-bind-style applies !important and CSS custom properties correctly on every path #
Style values were assigned through element.style[prop], which silently drops !important priority and no-ops for CSS custom properties (--x). Both the component update path and the list-row writers now route through setProperty, so !important survives reactive updates and custom properties apply. A property removed from the bound style object on a later update is also cleared instead of left in place.
subscribe store-wait timeout is bounded by elapsed time, not poll count #
The timeout for a component waiting on a subscribed store counted polling iterations rather than wall-clock time, so on a busy page the effective wait could stretch past the configured subscribeTimeout. It now fires at the configured millisecond bound regardless of how often the poll runs.
Memory leaks on list clear and row removal closed #
Clearing a list left scope-captured references to the old array alive, and a retired row's update-dispatch entry was not released when the row was removed. Both are now freed, so repeated build-and-clear cycles no longer accumulate memory.
Server-rendered nested lists hydrate into their parent item's state #
A nested data-list inside a server-rendered row was parsed into a flattened top-level shape instead of the parent item's nested array. It now reads from the correct nested state.
$this / $item primitive lists now render #
A data-list over an array of primitives (strings or numbers) referenced with $this or $item threw and rendered nothing. These lists now render their values.
Nested data-lists update when their parent item's identity changes #
Reassigning a parent row to a new object now reconciles its nested lists against the new parent instead of leaving them stale.
Portal bindings re-evaluate on store changes #
A portal driven by store state now re-evaluates when that store changes, and no longer over-invalidates unrelated portals.
The class-shape dev warning (WF-505) fires only for computeds #
It no longer flags inline data-bind-class expressions, where the shape guidance does not apply. Like every WF-NNN warning, it is stripped from production.
Performance
data-list / data-pool create path rebuilt (clone + setter) #
The create path has been rebuilt. Rows are now produced by cloning a cached row prototype and writing each text binding straight to textContent, with the whole batch assembled in a single DocumentFragment. This replaces the previous approach of serializing every row to an HTML string and reparsing it through innerHTML, which dominated the cost of building a list. Row creation is substantially faster, most visibly on large lists.
Second pass on the data-list / data-pool create path #
Three further refinements to row creation, most visible when building or replacing large lists. Each row reads its values straight from the underlying raw array rather than through the reactive proxy, since create-time reads do not need dependency tracking, which skips a per-row proxy and its access traps. Each row resolves its bound child elements by walking element node pointers instead of indexing a live element collection. And class setup copies only the item properties its class expressions actually reference, rather than the whole item. Builds compiled without server-side rendering also drop an unused legacy list path, making them slightly smaller with no change in behavior.
Single-text-binding update fast-path #
When a single item property changes and it is bound to exactly one plain text node, the framework now writes textContent directly and skips the generic per-item bind dispatch. Once such a field has been identified, later writes to it update the text node directly at assignment time, bypassing the update-batching step entirely. Anything more involved on the row (multiple bindings, attributes, or classes) falls back to the normal path.
Nested-path targeted rebind #
A change to a deep item property (for example rows[i].user.name) now rebinds only the bindings that actually read that path and leaves the rest of the row untouched, instead of rebinding the entire row. Shallow, flat item-property updates keep their existing behavior.
Leaner data-list update path #
Two redundancies removed from the per-update list path. Class bindings on a row are no longer re-evaluated when the changed property is not referenced by any class binding, and the per-update DOM re-scan that looked for nested [data-list] elements, and found none on a flat list, is gone. On flat lists this brings the per-update querySelectorAll count to zero.
Faster item insertion and removal on reactive lists #
Inserting items with push, unshift, or splice, and removing them, now operate on the underlying raw array, skipping a layer of reactive-proxy traversal on each operation. Single-item removal also drops a redundant proxy lookup while it re-indexes the rows that remain. Lists that add or remove rows frequently do less work per change.
Faster cross-store computed reads and writes on shallow chains #
Three stacked changes make cross-store computed reads and writes faster on shallow dependency chains. The proxy set traps now use direct property assignment instead of the receiver form of Reflect; a cross-store read resolves through a single proxy instead of hopping through two; and a lean re-evaluation of a computed whose cross-store dependencies are already static skips re-tracking them. Deep dependency chains are unaffected.
Targeted updates extended beyond text to class, style, and attribute bindings #
Eligible list rows now retire their per-item update effect and apply changes through a direct per-binding writer, so a class, style, or attribute change updates its single target without re-running the row's bindings. The single-text fast path is the special case of this.
Replacing a list's array with new objects updates each row once #
Reassigning a keyed list a fresh array whose items carry the same keys, the common pattern of swapping in a new page of results, now applies each reused row's bindings a single time instead of twice.
Targeted structural updates for swap, move, and single removal #
Swapping two rows, moving a row, or removing one now applies a precise minimal DOM update classified from the exact array operation, instead of re-diffing the whole list.
Reactive updates flush on the microtask #
Pending effects drain on the microtask after a state change rather than waiting for the next animation frame, removing up to a frame of latency before the DOM reflects an update.
Lower per-row memory and allocation on large lists #
Per-object reactive bookkeeping moved off the row objects into a side table, repeated per-row metadata was de-duplicated, and the per-row text writer is now shared, so building and holding large lists allocates less.
1.1.0 — 2026-05-12
Build & Toolchain
Vendored, npm-free build pipeline #
The framework now builds via a SHA-512-pinned rollup + terser toolchain fetched as 3 frozen tarballs (rollup, terser, acorn). npm run build runs zero npm install; postinstall scripts never execute. Framework users (pre-built bundles from npm/CDN) were already immune to npm supply-chain attacks; this closes the same exposure on the maintainer side. Output bundles byte-near-identical to the previous pipeline (~30 bytes per variant). Removes 5 build-time devDependencies (~50+ transitive packages).
Added
Pool entity model #
Pools now accept an entity: { state, computed, methods } block, bringing the declaration shape into line with components, stores, and plugins. state supplies defaults shallow-merged into every spawned entity; computed defines per-entity derived values bound to each entity's this; methods installs per-entity actions routed by data-action dispatch in preference to component methods. Arrow functions in computed or methods throw at registration with a clear fix suggestion. See entity-model and pool-api.
Pool array-like API #
PoolHandle now exposes JavaScript-native array methods (push, pop, length, at(i), find, filter, map, forEach, some, every, reduce, Symbol.iterator) alongside the existing add/remove/size aliases. Intentionally absent: splice, indexOf, slice. They assume stable indices, which swap-with-last pool storage does not provide. Use remove(key) to delete and at(i) for DOM-ordered positional reads.
mini build variant #
A new smallest tier in the build ladder. Includes everything from lite (core reactive UI, components, stores, lists) except the data-pool renderer. Intended for apps that don't need high-frequency entity rendering: forms, dashboards, tables, navigation, standard CRUD. Registering a component with a pools: {} block against mini throws at registration with a clear message pointing at lite or higher. Build ladder: mini → lite → min (core) → spa → full.
Pool-level props #
Parent components can inject shared data accessible to all pool entities via the props. prefix in expressions (data-show="props.visible", data-bind="props.caption"). Dotted paths are resolved in the binding fallback, not evaluated as expressions.
Browser DevTools integration (__WF_DEVTOOLS_GLOBAL_HOOK__) #
Every WildflowerJS instance now exposes a read-only introspection API on window that external inspectors can drive via chrome.devtools.inspectedWindow.eval() or a drop-in <script>. Methods: getComponents(), getStores(), getPools(), getBindings(), getRoutes() for snapshots; setState() / setStoreState() for live editing from a devtools UI (both guarded against prototype-chain key names). Two companion packages ship separately: @wildflowerjs/devtools (standalone inspector, drop-in script with a floating panel) and a MV3 browser extension for Chrome and Firefox. Bundle cost: approximately +750 bytes brotli across all variants.
jQuery 3.x and 4.x coexistence verified #
WildflowerJS is drop-in safe alongside jQuery on the same page (WordPress / legacy-CMS scenario). 34-test matrix (test-new/jquery-coexistence.test.js) — 17 scenarios × jQuery 4.0.0 + 3.7.1 — covers globals safety, DOM ownership boundaries, co-handled elements, mutation isolation, attribute preservation under reactive updates, AJAX-injected components, $.noConflict(), plus legacy-CMS hardening (detach/append round-trips, zombie listeners, init timing). Live walkthrough at /demos/jquery-integration/.
Item-level computed properties in binding expressions #
Item-level computeds (fn(item) with fn.length > 0) now resolve across every binding type — data-bind, data-bind-class, data-bind-style, data-bind-attr, data-show, data-render — as bare references and inside compound expressions (ternaries, object syntax, string concatenation). Nested lists resolve against the inner item, outer context via _parent. v1.0 silently evaluated such references as undefined. Tests: test-new/list-item-computed-other-binding-gaps.test.js. Docs: live example on /docs/lists.
wildflower.batch(fn) callback wrapper #
Convenience API for batched state mutations. Runs a function inside a batch, applies the batch on success, cancels on exception. Removes the manual try/catch boilerplate around startBatch / applyBatch / cancelBatch and makes batch usage exception-safe by construction. Sync-only; for async work the start/apply/cancel API remains available.
wildflower.toRaw(value) for structured-clone boundaries #
Returns a deep plain-JS copy of any reactive value. Required whenever WF state crosses a structured-clone boundary (IndexedDB, postMessage, Web Workers, BroadcastChannel, Cache API, History state), all of which reject reactive proxies with DataCloneError. Supports primitives, plain objects, arrays, Date, RegExp, Map, Set, and cyclic references; skips functions; returns DOM nodes by reference. Don't call from inside a reactive effect or computed — it registers every walked path as a dependency.
await db.put('issues', wildflower.toRaw(pm.issues));
worker.postMessage(wildflower.toRaw(state));
Breaking Changes
Action handlers no longer stop event propagation by default #
Events dispatched through data-action now bubble naturally. Restores clean coexistence with external delegation (jQuery $(document).on(...) was silently being consumed in v1.0). To opt back in on a specific element, add data-event-stop. Internal nested-component double-fire is still prevented via a per-event marker (event._wfHandled). Most apps will see no change; modal/dropdown click-outside guards may need the explicit opt-in.
Removed data-model-debounce attribute #
Debouncing user input now belongs on the action that receives it. Migrate any data-model-debounce="Xms" usage to the corresponding action with a debounce modifier (data-action="input.debounce.Xms:handleInput"). The attribute was experimental and its semantics collided with list re-render timing; routing debounce through the action layer is simpler and avoids the stale-value hazards of capturing state at keydown.
Bare-form item-level computeds removed #
Scope is now declared purely by signature: fn(item, index, info) { ... } is item-level (per row); fn() { ... } is component-level. v1.0's dual interpretation (zero-arg computeds becoming item-level inside list templates, with this.X binding to the current row) is removed because of silent failure modes (name shadowing, scope-dependent semantics). Migration: change fn() { return this.assignee } to fn(item) { return item.assignee }. The new info arg exposes list-context vars (info.first, info.last, info.length). v1.0 had no documented item-level computeds, so user impact is bounded.
Fixed
Item-level computed bindings reactively update on per-row state mutations #
Computed-name bindings (e.g. data-bind-style="assigneeStyle") sometimes stayed stale after the underlying item field mutated, because the targeted-rebind optimization compared binding.path === changedProp and the binding's path was the computed name, never matching the actual changed prop the computed body reads. Fix: per-binding bypass at every targeted-rebind filter site — when the binding name or expression vars match a registered computed, skip the path-equality filter. Tests: test-new/list-binding-targeted-rebind-with-computeds.test.js.
Item-level computeds in class binding expressions #
Class bindings like data-bind-class="isOn ? 'active' : 'inactive'" silently evaluated to undefined. Two evaluator branches in ListExpressionEval.js (_resolveListExprArgs and _applyCompiledClassBinding's var-resolver closure) gated computed lookup on itemComputeds[name].length > 0, but the wrapped accessor at itemComputeds[name] always has length === 0 so the branch never fired for any computed. Replaced both call sites with _originalComputedFunctions lookup routed through _evaluateComputedInListContext. Tests: test-new/item-level-computed-form-capabilities.test.js.
Nested data-list source resolves item-level computeds #
A nested-list path (e.g., inner <ul data-list="reactionChips">) now falls back to evaluating an item-level computed when the path isn't a raw field on the parent item, mirroring how data-bind already resolved them. Previously item-level computeds only worked as data-bind values, not as nested-list array sources, so users had to pre-decorate the parent rows. Patched in both ListNestedManager (initial setup) and ListRenderer (the per-frame arrayFn accessor). Test: test-new/nested-list-item-computed-source.test.js.
Multi-component scan init race that left nested data-list inner items unrendered #
The render effect fired synchronously but _listRelationships wasn't populated until later (_setupListContexts), so renders landing in the scan's sprint window saw an empty map and skipped nested-list integration — outer list rendered, inner data-list stayed a bare <template>. Symptom: section headers with no rows beneath them, intermittent based on idle-callback scheduling. Fix: walk the scan root once before features run and pre-populate _listRelationships from every template. Test: tests/nested-list-prepopulate-init-race.test.js.
Pool entity binding and dispatch issues #
Boolean-prop sync, data-bind on form inputs, and dotted-path bindings (props.X) in data-show/data-bind fallback paths now resolve correctly. Mini-build error messages include a copy-pasteable fix.
Bindings on data-list root elements #
data-bind-style, data-bind-class, data-bind-attr, and data-model placed on an element that also has data-list are now correctly collected and applied. Previously they were silently skipped because _isOwnedBindingElement treated the list root itself as "inside a list" and filtered it out. Fix: check ancestors only (not self). Unblocks the common carousel pattern of animating a list container's transform while the list renders its children.
data-cloak retained on dynamically-added list items #
List items added after the initial DOM scan, or moved between sibling data-lists, inherited data-cloak from the cached template and stayed hidden by [data-cloak]{display:none} forever, defeating data-show on inner elements. The strip is now applied along every row-creation path: the cached template attribute list in TemplateSystem, the rendered innerHTML parts, the cloneNode fallback used when the cached template was bypassed (root element and all descendants), and the data-render conditional template clones. Belt-and-suspenders so no row-creation path can leave data-cloak alive on a newly-added item.
Hover events on data-list row templates #
data-action declarations for mouseenter, mouseleave, mouseover, and mouseout inside list-row templates were silently dropped — the delegated event registry only attached listeners for a fixed whitelist that excluded them. Fix: add mouseover / mouseout to the whitelist (both bubble); synthesize mouseenter / mouseleave on top via the standard event.relatedTarget containment check.
Multiple actions on a single list-row element #
A row-template element with multiple actions (e.g. data-action="click:open mouseenter:hover mouseleave:unhover") only wired up the first one — the per-row context kept a single action context per element and skipped subsequent defs. Fix: accumulate every declared (eventType → handler) pair on the row's action context; the dispatcher routes by event.type through that map.
data-event-outside on data-list row children #
data-event-outside inside a row template was a silent no-op — neither the list-row context-creation path nor _setupActions wired up the document-level outside-click handler. Fix: TemplateSystem records a hasEventOutside flag on action metadata; ListItemBinding and ListRenderer's innerHTML fast path register the handler eagerly per row. Companion: PropsSystem._setupOutsideClickHandler rebuilt around a single document listener + per-element registry keyed by (element, methodName). Test: test-new/actions.test.js.
data-event-outside row-child handlers receive a details object #
Row-child outside-click handlers got only (event, el), unlike regular row actions which receive (event, el, details) with details.item. Now the row context is captured at registration and the outside-click registry builds the same { item, index, list, length, first, last, context } shape on dispatch. Non-list handlers unchanged. Test: test-new/actions.test.js.
Idempotent attribute writes in list and effect paths #
setAttribute is now skipped when the target attribute already holds the same value. Harmless for most attributes, but <video> fires emptied/loadstart on any write to src (even identical), which caused visible reload flashes and lost playback state during list reconciliation.
Debounce writeback regression #
Stale state from an in-flight debounced writeback no longer overwrites user input typed after the debounce window opened.
Binding validator false positives #
The dev-mode validator no longer flags property accesses of state variables (user.name when user is defined) as unknown paths, and now delegates expression-containing attributes to the expression validator instead of re-parsing them as binding paths.
Pool sub-array remove() O(n²) on bulk clear #
The internal _staticArray/_dynamicArray tracking used Array.prototype.indexOf for removal despite the pool's own main array using O(1) swap-with-last. Now uses a stored subIdx for constant-time removal, eliminating the quadratic cleanup cost at 800+ entities.
ListRenderer fingerprint collisions on arrays between 100 and 1000 items #
The change-detection fingerprint sampled only 3 positions for arrays over 100 items, causing interior mutations to be missed when only interior items changed. Full-item hashing is now used up to 1000; 7-position sampling beyond that.
SSR state parser for <input> / <textarea> / <select> #
Hydration now reads element.value for these tags instead of falling back to textContent, so server-rendered default values survive client activation.
Portaled event listener leaks on component destroy #
Listeners registered on portaled elements are now explicitly removed before the portaled content is detached, releasing handler closures (which captured the component instance) immediately rather than on the next GC cycle.
Reactivity correctness in expression cache and sync-effect reentrancy #
Four state-layer fixes including a snapshot-before-iterate guard in _notifyEffectDependents to prevent sync effect reentry from corrupting the outer loop, and _reusableEffectSet promoted to per-instance state.
Computeds that delegate to branching helper functions now re-track dependencies on every evaluation #
The optimizer used to seal the dep set from the first call, missing state read only on later branches — so a helper like pickName(state) { if (state.locale === 'en') return state.englishName; return state.spanishName; } would never see spanishName change after a locale flip. Function calls inside computed bodies now block that optimizer promotion. Companion fix: cached value is updated synchronously when a computed transitions out of the optimized fast path.
Action handlers fired before init() completes are queued and replayed #
Pre-init events (e.g. clicks landing while init awaits a slow subscribe) used to throw or be silently dropped. They're now queued and replayed in order after init() returns; replay errors route through onError. Caveats: lifecycle names (init, beforeInit, destroy, etc.) must not be reused for action handlers; replayed handlers see the original event but event.preventDefault() is a no-op by replay time — use data-event-prevent on forms instead.
Composed computed properties no longer drop dependencies in nested evaluations #
When one optimized computed read another optimized computed inside its evaluator, the inner evaluation could clobber the outer's dependency-tracking buffer, leaving the outer with an incomplete dep set. The buffer is now saved and restored across nested evaluations, and dep comparison reads from a local variable to prevent the bug from being reintroduced by future cleanup.
Effect cleanup on component destroy walks all three places effects can live #
The destroy sweep walked instance._effects and instance.context._effects but missed instance.stateManager._effects. Framework-internal effects scoped to the RSM's pre-instance stub landed there and survived destroyComponent, including every list's mapArray structural effect and per-item effects — which kept firing against external store mutations on already-removed DOM. Fix: walk the RSM's _effects too (_disposeEffect is idempotent). Test: test-new/effect-cleanup-on-destroy.test.js.
data-bind-style and data-bind-attr clear keys that drop out of the bound result #
When a style/attr computed shrank between renders (e.g. {background: color} → {} when unassigned), the framework applied the new keys but never cleared the dropped ones — visible as an avatar retaining its old background after the assignee was set back to null. Three apply paths now diff against per-element tracking sets and clear dropped keys before applying new ones. Test: test-new/binding-drop-out-clearing.test.js.
data-bind-class shape mismatch no longer crashes deep in the framework (WF-505) #
A data-bind-class binding whose computed returns a non-string (object, array, number) used to throw TypeError: t.split is not a function inside the rendering core, leaving the page blank. The element-level path now coerces the value (truthy keys joined to a class string for objects, String(value) for primitives) so the page keeps rendering, with a one-time __DEV__ warning per binding context pointing at the root cause. The Effect-based path already handled object form via {className: bool} syntax; this aligns the slower SET-trap path with that shape. Documented at /docs/error-codes?code=WF-505.
List click delegation no longer drops row clicks when an ancestor element carries data-action #
Click delegation tries closest('[data-action]') first then falls back to compiled metadata. But row data-action is stripped from the DOM (innerHTML fast path), so closest() walked past the row and returned an outer ancestor's data-action (e.g. a data-event-outside wrapper). The handler then saw the scope mismatch and bailed without trying the metadata fallback. Fix: when closest() returns an out-of-scope action, retry the metadata fallback and accept only rows whose parentElement is this list (nested-list safety). Tests: test-new/event-modifiers.test.js.
Item-level computeds in list rows re-evaluate on external store/plugin mutations #
Per-item effects only tracked the row's own item proxy and the component's local state, so an item-level computed that read from another store (e.g. a row's rollupBadge counting subtasks in a separate store) went silently stale on cross-store mutations. Per-item effects are now collected in a per-RSM _listItemEffects registry and woken from _handleEntityStateChange regardless of mutation shape. Test: test-new/list-item-computed-store-array-reassign.test.js covers six shapes (array reassign, push/splice, row property writes, reorder, keyed lookup, control).
Dev-mode warning for cross-subtree state proxy aliasing #
When the framework reuses an existing state proxy under a different parent path that doesn't share the original's first segment, dev builds now surface a warning. Catches a class of subtle aliasing bugs where the same nested object is reachable from two unrelated state subtrees and dependency tracking can get confused about which path the change occurred on.
Subscribe-only components now receive onStoreUpdate notifications #
The store's _hasNotifyTargets fast-exit cache was computed lazily on the first state change (the synthetic _internal.ready = true write at end of construction) and locked at false when no component had registered yet. Subsequent subscribePath calls populated the path-subscriber set but didn't invalidate the cache, so dispatch short-circuited and onStoreUpdate never fired. Components that also read the store via computed / data-bind were unaffected (tracking proxy refreshed the cache as a side effect); subscribe-only components silently received nothing. Fix: subscribePath now sets _hasNotifyTargets = true alongside _hasPathSubscribers. Test: test-new/notification-shape-matrix.test.js Shape 5.
Subscribe-block components are now registered as entity dependents of their store #
A subscribe: { store: ['path'] } contract previously wired the component only as a path subscriber, not as an entity dependent. The entity-dependent dispatch loop is what dirties dependent computeds — so data-bind / data-show backed by computeds reading the subscribed path could stay on stale values. The gap was usually masked by the tracking proxy registering the dep as a side effect on the first computed read, but unreliable: early-return computeds + cache-hit fast path could permanently skip registration. Surfaced as a Chrome-only blank-detail-pane in the PM demo after soft reload. Fix: subscribePath now also calls _registerEntityDependent; path-scoped invalidation still gates on declared paths. Tests: test-new/subscribe-block-registers-entity-dep.test.js.
LEAN re-eval path now sets _computedTrackingContext #
The LEAN re-evaluation path for cross-store computeds assumed external deps were stable after first eval, and skipped tracking-context setup. That breaks when the first eval early-returns before a cross-store read: the dep is never tracked, and subsequent lean re-evals also skip tracking, so the computed stays permanently disconnected from a store it actually reads on the non-early-return path. Fix: set _computedTrackingContext around node.fn() in the lean path too; per-call dedup keeps the cost minimal; finally restores prior context. Test: test-new/computed-lean-path-tracks-cross-store-deps.test.js.
_resolvePendingStoreDependencies resets _externalEvalCount on dependents #
When a late-arriving store resolves, the resolver cleared computedCache but left each computed node's _externalEvalCount intact, so subsequent re-evals stayed on the LEAN path and couldn't re-establish the cross-store dep graph. Fix: reset _externalEvalCount to zero across _computedNodes after the cache clear, forcing the next eval through the full tracking path. Combined with the LEAN tracking-context fix, this closes the late-store-resolution failure end to end. Test: test-new/computed-lean-path-tracks-cross-store-deps.test.js.
Component-level computeds referenced inside list templates no longer get falsely flagged item-level #
_evaluateComputedInListContext was adding every computed touched in list-row evaluation to _itemLevelComputedProperties, regardless of arity. A zero-arg component computed referenced inside a list-row binding got tagged item-level, then the component-level cascade skipped it on the (now wrong) assumption that per-row effects would drive re-eval — leaving the binding silently stuck on its first cached value. Fix: scope the marking to fn.length > 0 via stateManager._originalComputedFunctions. Test: test-new/lifecycle-invariants.test.js.
_setupStoreSubscriptions hoisted ahead of computed setup in the scanner #
The async scanner could yield (requestIdleCallback) between computed-setup and feature-setup. An async store init resolving in that window mutated state before the component was registered as an entity-dep, so the cascade missed it and the component stuck on its empty cached value. Surfaced as Firefox-only blank-detail-panes in the PM demo after soft reload. Fix: run _setupStoreSubscriptions as a synchronous pre-pass in both orchestrators, before any computed-eval enqueue. Tests: test-new/scanner-subscribe-before-computed.test.js plus parameterised coverage in test-new/race-harness.test.js.
List-row click delegation now bounds closest() to the list element #
Scope leakage in event delegation: when the canUseInnerHTML fast path stripped a row's data-action, event.target.closest('[data-action]') walked past the empty row and latched onto an unrelated outer ancestor (e.g. <form data-action="submit"> wrapping a modal). The handler then bailed on the form tagname and never reached the compiled-metadata fallback — silent dead-click. Fix: reject any actionEl outside the owning listElement, plus a defensive fallback in _handleDelegatedActionWithListItem that recovers the action name from listItem._compiledMetadata when the DOM attribute is missing. The compile-time strip stays for krausest-scale perf. Tests: test-new/list-row-action-attribute-preserved.test.js (3 scenarios including the form-ancestor case).
Per-row field precedence honoured by data-bind-style and data-bind-class #
Two compounding bugs caused visual leakage between unrelated entities sharing a binding name. (1) In list templates, style/class bindings resolved simple-name expressions against the component computed registry first, shadowing any same-name per-row field — opposite of the documented item-first contract. (2) RenderingCore registered a component-level effect for every in-list bind-style/bind-class element, creating a second writer that raced the list-row update path; the text-bind path had a listBoundElements guard but style/class did not. PM team page symptom: project chips followed the parent team's color ~7/10 reloads in Chrome. Fix: item-first lookup in ListExpressionEval; !inList gate on the effect-meta push in RenderingCore. Tests in test-new/data-bind-style.test.js include a 30-iteration stress loop to surface the race.
wildflower.createRouter() staged-init pattern no longer emits spurious warnings #
createRouter always auto-initialized inside the factory, so the documented staged pattern (createRouter({ mode }) → .onRoute(...) → manual .init()) ran against an empty routeTree and emitted three warnings per page load. Fix: auto-initialize only when options.routes is a non-empty array. Declarative form unchanged; staged form now skips auto-init so the caller controls timing.
router.navigate(path, { replace: true }) updates the address bar #
{ replace: true } was a no-op against the address bar: the route handler ran but no history.replaceState call was made, so the URL never updated. Replace navigation now performs replaceState (history mode) or hash update + replaceState (hash mode), via an explicit _replace flag so initial-load and popstate paths still leave the URL alone. Test: test-new/router-gaps.test.js "Replace navigation".
data-cloak strip for components registered after framework init #
Closes a Chrome-only "appear then hide" flash on default-hidden elements (welcome modals, routed sections, popovers) inside components whose wildflower.component(...) call lands after the initial cloak-strip rAF. The strip was unconditionally removing data-cloak on elements whose component ancestor hadn't initialized yet, exposing the element briefly until the late render effect wrote display:none. Fix: cloak-strip rAF defers when the closest [data-component] has no data-component-id; _initializeComponentElement then strips remaining cloaks after the first render effect runs. Both passes share _evaluateCloakShowVerdict so the strip commits the right inline display first. Tests: test-new/data-cloak.test.js.
Nested-list and refresh-effect cleanup on list re-render #
Two compounding leaks in _renderList: (1) the per-list refresh effect was only cleaned up on component destroy, so mid-life re-renders left stale effects firing on every state mutation; (2) row removal disposed the row's own item effect but didn't walk its subtree for nested [data-list] elements, orphaning every popover sub-list's mapArray + effects. PM issue list (26 rows × 4 nested popovers) leaked ~870 effects per priority change. Fix: element._disposeMapArray recursively disposes nested mapArrays and the owning list's refresh effect; mid-life and destroy paths share the wrapped dispose. 0 leaked effects per change post-fix.
Performance
Cross-store computed cache-hit fast path #
Reads of an already-cached computed property that depends on another store's state now skip the full re-evaluation path when the source stores have not changed. Measured speedups (1M-read microbenchmark): 8.7x on Firefox (533 ns → 61 ns per read), 4x on Chrome (450 ns → 115 ns). Read-heavy cross-store rendering patterns (1000:1 read:write ratio) speed up 2.7-6x end-to-end. Write-heavy patterns are unchanged.
Portal binding lookup #
_renderPortalBindings replaced an O(all-bindings) linear scan with a per-component context index, removing a per-teleport hotspot in apps with many active bindings.
Reactivity batch change-detection rebuilt around the proxy #
wildflower.batch(fn) (and startBatch / applyBatch) no longer serialize every component's state on entry and re-diff on exit. The proxy already records per-batch mutations via its set trap; the new path consumes that directly. startBatch is constant-time per batch instead of scaling with total state size — the dominant cost in small-batch / many-component apps. Krausest data-pool variant: swap1k ~25% faster, remove-one-1k ~20% faster. Real-world Lighthouse held within ±2 across 18 demos. ~600 lines of legacy code removed; startBatch shrank from ~25 lines to 6.
Portal visibility update skipped for portal-free components #
_updatePortalVisibility ran a descendant querySelectorAll on every entity state change for every component before early-returning on zero matches. For portal-free apps that's an O(descendants) DOM walk per mutation per component (PM demo: 38% of main-thread time during a select/deselect cycle). Fix: cache instance._hasPortals at init (and on late-list-item portal discovery); _scheduleComponentRender skips the call entirely when false. Apps without portals (most apps) pay zero descendant-walk cost on reactivity updates.
Class-binding eager item-computed eval gated on merged-context need #
_applyClassBindingsToRow eagerly evaluated every item-level computed on the component before applying any class binding, regardless of whether any evaluator on the row actually needed the merged context. For simple-property class bindings (the common case), that allocated 2 Proxies per computed per row per update and threw the result away. Fix: pre-scan the row's class evaluators; skip the eager loop entirely when none set _usesMergedContext. Reactivity and per-item resolution unchanged.
Path-scoped entity invalidation #
Store-state changes previously re-dirtied every dependent's computeds and re-ran every per-item effect, even when the changed path was nothing the dependent ever reads. The changed path is now matched (prefix-aware in both directions) against the component's declared subscribe paths and its runtime-tracked deps — non-matching dependents are skipped entirely. Conservative by construction: only narrows for explicit subscribe: {} contracts; falls back to full invalidation for computed-path notifications, store-computed readers, and missing metadata. Test: test-new/entity-path-scoped-invalidation.test.js.
Security
xlink:href sanitizer coverage #
Added xlink:href to the URL-attribute allow-list in ListExpressionEval._sanitizeAttrValue and PoolRenderer's _POOL_URL_ATTRS. Previously an attacker-controlled value bound to xlink:href on an SVG <a> or <use> could carry a javascript: URI through to the DOM, where Chrome and Firefox will execute it on activation. Now blocked with the same policy used for href/src/formaction/action/poster. Addressed the single exploitable finding from the 2026-04-15 security audit. Includes a 16-test regression suite (test-new/security-audit.test.js) exercising each audited finding through realistic ingress paths.
Narrowed data:image/ allowlist to raster formats only #
The previous regex permitted data:image/svg+xml, which could embed inline scripted SVG in URL-bearing attributes. Now restricted to png, jpe?g, gif, webp, avif, bmp, ico, tiff?, and x-icon. Other data:image/* subtypes are blocked.
1.0.0 — 2026-04-10
Added
- Core reactive framework with component system
- Reactive state management with computed properties and dependency tracking
- Store system for cross-component state sharing
- List rendering with automatic keyed reconciliation
- Conditional rendering (
data-show, data-render)
- Event handling with modifiers (throttle, debounce, self, outside, once, passive, capture)
- Two-way data binding (
data-model) with modifiers (trim, number, debounce, lazy)
- Attribute, style, and class binding (
data-bind-attr, data-bind-style, data-bind-class)
- Client-side routing with history and hash modes
- Server-side rendering with hydration
- Plugin system architecture
- Portal, modal, and transition systems
- Entity pools (
data-pool) for high-frequency DOM rendering
- Anti-FOUC
data-cloak system
wildflower.whenSettled() API for deterministic async waits
- 4 build variants (core, lite, spa, full)
- Comprehensive test suite (3,646 tests in real Chromium)
Security
- Expression evaluator blocklist for unsafe patterns (
eval, Function, globalThis, window)
- Pool renderer attribute blocklist and URL protocol sanitization
- HTML sanitizer routing for
data-bind-html and router outlet
data: URI blocking (except data:image/) in URL-bearing attributes