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.

1.5.2 — 2026-09-15

Fixed

A data-render turning on no longer clears a pool's bindings #

When a data-render condition started false and later became true, rows in a data-pool elsewhere in the same component lost the values set by their own data-bind-attr, so an image went blank while the pool entity still held the right string. Re-inserting the section rescans the component for bindings, and that scan claimed the pool's rows as its own, then evaluated each row's expression in component scope where the entity's fields do not exist. It now leaves pool rows to the pool, as it already did for list rows.

undefined and null query values are left out of the URL #

navigate(path, { query: { search: term || undefined } }) is the ordinary way to leave a key off, and it wrote the literal string undefined into the URL instead. The next page load then read that as a real filter value. Both serialisers now drop undefined and null, per element inside an array as well, and omit the ? when nothing survives. An empty string is still kept, since ?q= says something different from no q at all. Routing.

1.5.1 — 2026-09-12

Added

wildflower.version #

wildflower.version reports the framework version string, stamped from the package version at build time, in every tier and build. Until now the only version literal in a bundle was the one the DevTools hook reported, and nothing on the page could ask which release it was running. Bug reports and support scripts can read it directly. Application API.

ES-module bundles for every tier #

Each tier now ships as an ES module beside the script-tag file, wildflower.<tier>.esm.min.js with an .esm.dev.js twin. The default export is the framework instance, the named exports match the globals, and the wildflower global is still registered, so a page behaves the same whichever file it loads. The package's import condition resolves it, so import wildflower from 'wildflowerjs/full' works in a bundler or an import map. Installation.

Object plugins no longer need an install() method #

A plugin that is only state, computed properties, and methods has the same shape as a component or a store, and now registers without an installation step. Until now the framework threw on an object plugin without install(), which made the side-by-side example on the plugins page fail as written. When install(wf, options) is present it runs exactly as before.

Opt-in typings for the script-tag global #

The package now also ships types/wildflower.global.d.ts, a second declaration for the wildflower global that infers this inside a definition from the literal you pass, so this.count, this.doubled, this.pools.enemies and this.stores.cart complete in a plain JavaScript project with no TypeScript in the app. Reference it from a jsconfig.json or a triple-slash directive; the module typings remain the package's types entry. TypeScript.

Fixed

The published typings compile under strict and cover the 1.5 surface #

types/wildflowerJS.d.ts ended with an export block that re-exported names already exported inline, so a strict TypeScript consumer with skipLibCheck off saw thirty-two errors from the file itself. Its content had also stopped at 1.2.0: it still declared the removed pool() and knew nothing of the query layer. The file now compiles clean under strict and declares getPool, query, getQuery, invalidateQueries, clearPersisted, toRaw, batch, config, setHtmlSanitizer, registerAdapter, inspect, unregister, createRouter, version, the query configuration and handle, rules, tick, and the real component context; declarations with no runtime counterpart are gone.

Unsanitized data-bind-html warns in production too #

Rendering data-bind-html with no sanitizer installed warned once in development builds and said nothing in production, so a deployed page could inject raw HTML with no sign of it in the console. The one-time warning now fires in every build. Install a sanitizer with wildflower.setHtmlSanitizer() and it stays silent.

tick() runs for a page-load component that has no init() #

A component that defined tick() without init() never ticked when the page loaded. The batched page-load path queued only components with an init() for the sequence that registers the frame hook, so the particle example on the pools page stood still, while the same component mounted dynamically ran. Registration now happens on both paths.

Minified builds keep the license banner #

The minified builds now open with the MIT notice and the version, the same banner the unminified builds always carried. The minifier stripped it because the banner was an ordinary comment; it is now a preserved one, so a file served from the CDN carries its own license and version line.

Development builds stay quiet about SSR on pages without it #

Development builds logged two SSR activation lines on every page load, including pages with no data-ssr markup at all. Activation now returns quietly when there is nothing to activate, so a plain page's console shows only what concerns it. The initialization line remains, so SSR presence is still visible.

Performance

Faster list create, replace and clear #

Bulk create registers rows in one pass and builds a row's element array only when something reads it, a delegated list action resolves its target in a single walk, and the clear path no longer sweeps every component per change. Against the 1.5.0 bundle on the js-framework-benchmark rig, create, replace and clear run 4 to 9% faster end to end, and the standard entry's script time falls by about a fifth.

1.5.0 — 2026-09-11

Added

The mini-pool tier #

Mini with entity pools in place of the list cluster, for games, simulations, and per-frame visualization that render through data-pool and never use data-list. Everything else in mini is present. It ships as wildflower.mini-pool.min.js on the CDN and as the package export wildflowerjs/mini-pool. Installation lists the tiers.

Declarative writes: to: and write() #

A query that declares where its data comes from can now declare where it goes. to: is the write transport and write(item) is the call. The item applies to its row immediately and the store reports isStale until the server confirms. A record in the response reconciles the row, a rejection rolls back only the fields that write still owns, and write() rejects into your catch. Writes never retry on their own; calling write() again is the retry.

Declarative transport: to: as a URL, named operations, and create: #

to: accepts a URL string or a map of named operations. to: '/api/items/:id' gives an update at PATCH and a delete at DELETE on that URL, and write('favorite', item) reaches a named operation. create: is its own key, since it creates the row's identity. select:, body:, and confirmation: carry the payload shapes, each overridable per operation, and the function form of to: is unchanged. See Declaring the Destination.

URL templates: :token segments in from: and to: #

A query URL may carry :token path segments, resolved from params on a read and from the item's own fields on a write, with params as the fallback. Values are percent-encoded, and existing URLs are unaffected, since only the path is scanned. A read with an unresolved token sends nothing, reports isLoading, and loads when the value arrives. A write with one rejects (WF-972).

Request headers: headers: and origin-scoped defaults #

A query can declare the headers its requests carry, as a plain object or a function called once per attempt, so a token refreshed between a failure and its retry is the one sent. wildflower.config({ headers }) sets defaults per origin, with 'self' for the document's own, so a credential is never sent to a third-party host. A query's own headers: overrides the default key by key.

Returning to a page you have already seen no longer waits #

A query keeps the rows of recently resolved URLs in memory. A revisited page, filter, or detail view paints immediately, marked isStale, while the query revalidates. There is nothing to declare. Only confirmed server truth enters the cache, and it is not read while a write on that query is unsettled. Fifteen URLs are held per query under a five thousand row budget, both adjustable through wildflower.config().

Query cache persistence with persist: #

persist: true keeps a query's last confirmed rows in localStorage; a string chooses the key. The next visit paints the saved rows before any request leaves the machine, marked isStale, and revalidates in the background. Only confirmed rows are written, never an optimistic value, and a snapshot expires after 24 hours. wildflower.clearPersisted(...names) removes snapshots; call it at logout. See Reload Persistence.

invalidateQueries(...names) #

await wildflower.invalidateQueries('orders', 'dashboard-counts') invalidates several named queries in one call and resolves once every triggered refetch has settled. It takes names only. A query nothing observes is skipped, and an unrecognized name warns in development while the rest run. See One Entity in Two Queries.

$q.pendingWrites #

Every query exposes pendingWrites, a reactive count of writes dispatched but not yet settled. Bind it for a saving indicator, <p data-show="$tasks.pendingWrites > 0">Saving…</p>, or check it before navigating away. It falls as each write settles, on a rejection after the rollback. See Saving Indicators and Page Unload.

A warning for a refresh rung token that matches nothing #

A refresh token that matched nothing, such as 'poll:15' where the syntax is the bare number 15, registered the query with that rung missing and no warning, and a malformed suffix like 'etag:abc' switched the rung off the same way. Development builds now warn once per bad token at registration (WF-967).

Computed properties may return a promise #

A computed that returns a promise, including any async function, resolves through the normal binding surface. Bindings keep the last settled value while a new one is in flight and update when it resolves; before the first resolution they render empty. The computed: block, data-bind, and dependency tracking are unchanged.

A freshness window for event rungs #

refresh: ['focus', 'fresh:30'] keeps the focus and reconnect rungs from refetching while the last sync is younger than thirty seconds, so switching tabs does not fire a request per switch. A store holding unconfirmed local state refetches regardless, refresh() and invalidate() always fetch, and poll rungs keep their cadence.

Development builds catch reactive values written back into state #

Writing a container that holds already-reactive elements back into state, most often state.list = [...state.list, item], nests proxies inside the reactive graph, which corrupts update routing and slows every later read. Development builds now warn (WF-966) and tell you to spread from wildflower.toRaw(state.list) instead.

Nested lists accept shared sources #

A list inside a row template can name $store.path or $query.rows, or carry data-query, and every row renders that source and follows it. Previously a nested list resolved its path only against the row item, so a per-row dropdown fed from one options store rendered nothing and warned nothing. The row context still applies inside the nested list, and removing the row disposes it.

A warning for a retry: value that is not a count #

retry: is a plain number of attempts. Any other shape, including retry: { max: 3 }, the form other libraries use, coerced to zero and disabled retry with no sign of it until a network failure. Development builds now warn at registration (WF-983) and state the value the option coerced to.

A warning when a confirmation: callback throws #

A confirmation: callback that throws after an ok response rejects the write the way a server rejection does, and nothing in the console said where the error came from, so a bug in the callback read as the server refusing the write. Development builds now warn (WF-998) and identify the callback and the operation.

A warning when a write's response body is not JSON #

With confirmation: declared, a successful write whose body is present and not JSON, usually an HTML error page returned with a 200, has nothing to hand the callback. The write stands and the query refetches. Development builds now warn (WF-999) and identify the operation and the status. An empty body never reaches the warning, since 204 No Content is the ordinary answer to a delete.

A warning when a named operation does not inherit the query-level confirmation #

A query-level confirmation: reaches update and create only, the rule body: already follows, so a named operation such as favorite discarded its response and refetched the whole collection with no sign of it. Development builds now warn once per query (WF-940). Declare confirmation on the operation itself to reconcile from its response; delete is exempt. See Named operations.

refresh({ clear: true }) for a change of list #

A refresh() with new params keeps the current rows on screen, marked isStale, until the response arrives, which is wrong when the params name a different list rather than the next page of the same one. clear: true drops the rows before the request departs, and the query reports isLoading until the new rows arrive or paints the target URL's cached rows as stale. Only an explicit refresh() takes the option. See A different list, not a different page.

Pool templates inside <svg> #

A data-pool container can be an SVG element with SVG primitives in its template, so entities share one coordinate space and data-bind-attr writes geometry such as cx or d directly. The HTML parser makes a <template> inside <svg> an inert element, which previously made the pool report WF-401; the pool now reads that element's children as the template. data-list inside <svg> is unchanged and unsupported. See Pools inside svg.

Fixed

A binding on a child root stays the parent's under the data-wf- prefix #

A binding written on a child component's root element belongs to the parent, since it sits in the parent's template, and the child's own binding scan excluded it only when it could see a parent declared with the bare attribute or already initialized. With the child declared first on a prefixed page, the child claimed the binding in its own scope, where the path did not exist, and each of its renders undid the parent's paint. The check now reads both prefixes.

A child component declared before its parent receives its props #

Registering the child's definition before the parent's, with both elements in the page, built the child while the parent element had no instance, so its props resolved to nothing and were never retried, in every build and under both attribute prefixes. The child now attaches when the parent finishes initializing, and its props, this.parent, and the parent's children resolve then. A default holds until the parent exists.

A server-rendered list in a component registered after page load hydrates its rows #

For a component registered after the framework initialized, SSR activation could run before the first render pass and mark the list as rendered with its rows never adopted, so a component inside a row never received the row through data-prop-x=".". Activation now hydrates a list the render pass has not reached, in the order page load already uses. Server-side rendering describes the handoff.

data-wf-* holds through SSR adoption, nested lists, and exclusive mode #

On a prefixed page a component treated its own list rows as component bindings and blanked server-rendered text in place, a nested data-wf-list was looked up by a bare literal and never rendered, data-ssr="true" was read only in its bare spelling, and with data-wf-prefix="true" a bare data-action from another library threw inside the dispatcher. Each check now honors both prefixes, and exclusive mode leaves bare attributes alone. Configuration covers the prefix.

A permanent 4xx no longer climbs the retry ladder #

retry: N re-ran a 404 or a 422 on the doubling curve, delaying a terminal answer it could not change. A permanent 4xx now fails at once. 401, 408, and 429 still retry, since a refreshed credential or a cleared rate limit can succeed on the next attempt, and 5xx and network errors are unchanged. Automatic retry covers the ladder.

A data-model root the component's state declares stays in the component #

When a component state key and a store shared a name, data-model wrote to the store and read from component state, so the input reset on the next unrelated repaint. One rule now decides both directions. A root the component's own state declares is component state, and an undeclared root is the store, reading and writing alike. Development builds warn on the collision (WF-512).

A stream the browser closed on an error status reopens with the next fetch #

The browser closes an EventSource for good after a 401 or 404 and reconnects on its own only after a network error. The closed object stayed installed on the query, so no later fetch reopened the stream and only a full teardown recovered it. A closed stream now reads as no connection, and the fetch that finds it reopens it. Server-Sent Events covers streams.

An appended page cannot add one key twice #

Two rows sharing a key within one appended page both entered the list, and every later write resolved against the first copy. One key now yields one row. A page that mixed keyed and unkeyed rows slipped the keyed-payload check and lost its unkeyed rows from every later page; mixed pages now warn (WF-960) and apply as a replace. Infinite results covers appending.

A computed that calls one of the component's own methods no longer gets undefined #

A computed that called one of the component's own methods got undefined on the first render and cached it, so a count read zero, a data-show hid its content, or a list rendered nothing, with nothing thrown or logged. The wrapper that queues actions fired before init() was answering the call. A computed evaluation is a synchronous read that cannot be replayed, so the wrapper now runs it immediately; actions still queue and replay as before.

Item-level computeds can reach the component's methods #

A computed that takes the row, such as rowClass(item), is evaluated against a context assembled for the row, and that context never resolved the component's own methods, so calling one failed and the row binding rendered empty. Methods now resolve there in their wrapped form.

Internal refetches keep the query's parameters #

Only refresh({ params }) carried parameters, so every engine-initiated refetch, after a write, on a poll tick, or on focus or reconnect, fell back to the declaration's static params, and a query showing page two silently refetched page one. params: may now be a function, resolved on every request, so an internal refetch derives the same values the last explicit one did.

A cached validator is sent only for the resource it describes #

A query kept one ETag for every URL it requested, so the validator from one URL was sent as If-None-Match on the next conditional request whatever it was for, and a 304 could confirm the wrong content as current. Validators are now keyed by the fully resolved request URL.

computed:-prefixed bindings inside list rows track their dependencies #

A binding spelled data-bind="computed:name" inside a data-list template evaluated once and never again, while the unprefixed spelling tracked correctly. Every prefixed form now builds the same dependency edges the unprefixed spelling always has, across text, data-show, data-bind-html, and data-render.

patch() with a partial row merges instead of replacing #

The documentation promised that a keyed partial merges into its matched row, and the implementation replaced the row wholesale, dropping every field the partial did not name. A keyed partial now merges field by field, and a record-shape partial merges into the record. A fetched or streamed row still replaces its match, since a server payload is a complete row.

Development suggestions catch case-only typos and rank by distance #

A binding name wrong only in letter case, like selectedid for selectedId, produced a warning with no suggestion, and suggestion lists took the first candidates under the threshold rather than the closest. Suggestions now rank by edit distance everywhere they appear, and pure case slips suggest their fix.

A $queryName.path markup binding now activates its query #

A query came alive only when a [data-query] container scanned into the page or getQuery() read it, so a page that referenced it only through a $-shorthand binding, such as data-list="$feed.rows" or data-bind="$latestRelease.version", never fetched and never warned. Every binding type now activates the query it references when it is evaluated. A query nothing references still never activates.

A leading ! on a compound conditional now negates only its own term #

data-show, data-render, and the same shape inside a list template treated a leading ! as a whole-expression negate and stripped it before evaluating the rest, so data-show="!a && b" was evaluated as !(a && b). The shortcut now applies only to a single simple term; a compound expression keeps its ! where it was written.

Store-backed data-model paths repaint on a programmatic store write #

data-model="storeName.field" wrote to the store when typed into, and the read direction checked component state only, so a store value set from outside the input, a route handler prefilling a form or an action clearing the field, never reached the DOM. The read path now goes through the store like the write. A component-state data-model path is unaffected.

SSR-hydrated and query-adopted list rows survive their first update as the same DOM nodes #

Server-rendered rows are adopted at first paint, and the first update after that treated the list as unrendered and rebuilt every row from the template, so focus, selection, a running transition, or a third-party enhancement on a row was lost. Adopted and hydrated rows now enter the list's own row bookkeeping at takeover, so the first update patches them in place like every later one. Query adoption, new in this release, has the same guarantee.

A data-render insertion no longer misreads list rows as component-level bindings #

Re-inserting a data-render section rescans the component's bindings, and that scan could sweep up rendered list rows as component-level bindings, so the component's render effect evaluated a row's expression at component scope and overwrote its correct value with a fallback. List interiors are now excluded from the scan; only the list container itself remains eligible for a component-level binding.

Development validation no longer flags a store-routed data-model path #

The development validator (WF-509) checked every data-model root against component state, so the documented data-model="storeName.field" form reported an undefined property although it worked at runtime. A registered store name is now a valid root. An unregistered root still warns, and production was never affected.

A write whose response has no body no longer rolls itself back #

With confirmation declared, a successful write parsed its response as JSON, so 204 No Content from a delete, or any empty body, failed to parse and looked like a refusal, and the change rolled back although the server had accepted it. An empty body now means there is nothing to apply, and the query refetches, as a write with no confirmation already did.

An error thrown by your code reaches the console in production too #

The framework catches errors thrown by action handlers, lifecycle hooks, and computed properties so one cannot take the page down, and the default errorHandling: 'log' wrote them to the console only in development builds. In production a typo in a handler produced a dead control and an empty console. The log now runs in every build and includes the component, the method, and the error. errorHandling: 'silent' still discards, 'throw' rethrows, and onError() takes precedence over both.

Immutable list updates store plain objects #

this.items = this.items.filter(...), [...this.items, item], and items.map(i => ({ ...i, done: true })) wrote reactive wrappers into state, which degraded update routing and slowed every read a little more with each write, with a WF-966 warning in development and silence in production. The engine now unwraps the first two levels of any container written to state, in every build. WF-966 remains for a wrapper three or more levels deep.

data-wf-component initializes on a plain page, and portals accept the data-wf- prefix #

A component root written as data-wf-component was collected by the page-load scan and then dropped, because the step that reads the component name looked at the bare attribute only; late registration and elements added later missed it the same way, and only a manual wildflower.scan() worked. Portals ignored the prefix entirely, data-wf-prop-* and data-wf-props were never read, and data-wf-cloak was never removed. All of these now read either prefix, and exclusive mode ignores the bare forms.

A list removed by data-render is released, and a query bound only there goes idle #

Removing a subtree with data-render destroyed its nested components and unhooked its actions but never disposed the lists inside it, so each toggle left a list reconciling off the page, and a data-query list there kept its query polling for the life of the page. Removed lists are now disposed with the subtree, and a list's own reconcile counts as observation only while its element is on the page.

A data-pool inside a data-render block renders again after a toggle #

Pools bind their container once at component setup, and a data-render re-insertion rebuilt the subtree's lists, actions, and conditionals but not its pools, so after a hide and reveal the fresh container stayed empty. Re-insertion now adopts the pool. The live entity elements move into the fresh container, later adds go there, and entities added while hidden appear on reveal.

A data-query record subtree resolves every expression against the record #

In a data-query record subtree only a bare data-bind path reached the record; a ternary, a concatenation, a data-bind-attr, a data-bind-class, or a data-show over record fields resolved to undefined with no warning. The subtree now evaluates like a list row, with the record shadowing component state, and a bare field name in data-show and the class, style, attr, and html bindings reads the record as well. data-model stays on component state.

A binding compiled before its store or query registered renders once it does #

A $name.path binding on data-bind or data-list whose store or query did not exist yet stayed empty for the life of the page, since only computeds registered themselves as waiting and registration found nothing to wake. Every miss now registers, and registration re-renders the waiting lists and bindings. A data-query element declared before its wildflower.query() call behaves the same way.

Overlapping optimistic operations no longer flash each other's rows #

An optimistic delete followed at once by an optimistic add each triggered an invalidation when its own request settled. Whichever settled first synced a snapshot that did not yet reflect the other operation in flight, so the deleted row reappeared, then the added row flashed out and back when the second sync caught up. Outstanding operations are now counted, and one invalidation runs when the count returns to zero.

A validator vouches only for a response that was applied #

The ETag was read and stored before the body parsed, so a 200 carrying an HTML error page rejected in the parse while its validator stayed behind. The next conditional request sent it, the server answered 304, and the query marked stale rows fresh. The validator is now stored after the body parses and before it is applied, so it can only ever vouch for content the query holds.

A 304 before the first successful sync clears the loading flag #

The not-modified branch never cleared isLoading, so a 304 arriving before any full sync, which happens when a validator was stored before the first load applied, left the flag true for the life of the tab and a bound spinner on over an empty list. Both 304 branches now clear isLoading and error like a full sync.

Activation no longer re-issues a read already in flight #

The catch-up fetch a query runs when its first bound element enters the document aborted whatever request was already running and issued the same one again, so an app that calls refresh() before the matching section mounts paid two identical requests on every cold load. Activation now leaves a read in flight alone; with nothing running it fetches as before, and an explicit refresh() still supersedes a request in flight.

A query inside a hidden data-render section no longer fetches at init #

Initialization ran the query transform and the binding pass before the conditional pass removed a data-render section whose condition was false, so a data-query element or $query binding inside it was observed on the way through and the query fetched for a view nobody could see. A hidden section is no longer observed, its nested conditionals are not registered, and a list inside it is not mounted. Revealing the section binds, observes, and fetches once.

A data-render section gated on a store is no longer removed and re-inserted during init #

A section whose condition names a store or query, such as $route.page === 'feed', and is true at init was removed by the initial conditional pass and put back a moment later as a fresh clone, because that pass used a second evaluator that answered false to every $ expression. A slot template inside such a section never rendered at all. One evaluator now serves every condition read, so a section that is true at init stays in place.

A re-inserted data-render section brings its nested sections and portals with it #

When a data-render section came back, a nested data-render inside it was evaluated once at insertion and then frozen, and a data-portal inside it was never teleported on reveal or withdrawn on hide, so a later reveal put a second copy inline. Insertion now runs the same steps as init for the returned subtree, and records of removed generations are dropped, so toggling a section many times no longer grows the component's bookkeeping.

Two instances of a component that name different pools no longer share a container #

The second instance of a component reuses a compiled snapshot of the first instance's binding positions when the two structures match. The match ignored data-pool, so a second instance whose markup named a different pool at the same position was registered under the first instance's pool, and its container stayed empty. The structural fingerprint now includes the pool name, and a mismatch falls back to the ordinary scan.

A portal shown and hidden repeatedly no longer accumulates binding metadata #

Teleporting portal content queued effect metadata to be picked up once after init(), and a portal teleported later, by a data-show flip or a re-inserted section, queued entries nothing ever read, one set per teleport. The queue now closes once its consumer has run.

Performance

Framework-rendered rows skip the component scanner #

The mutation observer's added-node path scanned every list row for nested components, a subtree query the list renderer had already performed itself. Rendered rows now skip the scan, the same exemption pool entities always had. A 10,000-row create saves about a millisecond, and a component inserted into a row later is its own added node and is still found.

A store write no longer costs one DOM scan per list row #

A store change reaching a component with a data-list ran two refresh passes that walked every row on screen, on every write, including writes to fields the list never reads; on an 800-row list a single field write cost about 153 microseconds before any real work began. The item-computed pass now runs only when the component declares a computed that takes an item, and the external-binding pass stops at each list it meets. The same write now costs about 3.4 microseconds.

1.4.1 — 2026-08-11

Fixed

Lists no longer double their rows after a data-render re-insertion #

data-render re-inserts its subtree from a snapshot, normally taken before any list inside has rendered. A cold-load timing race could take it after, carrying the rendered rows as inert copies, so the fresh mount rendered the array again beside them and every row appeared twice, with the duplicates ignoring clicks. A list now sweeps every non-template child once its template resolves, before its first render. Present since v1.2.0.

Components wait for stores registered later in the document #

A component whose subscribe: store was not yet registered received an immediate, permanent not-found and ran init() without it, an ordinary ordering in a streamed response where a later chunk carries the store. While the document is still parsing, the wait now holds until the store arrives, up to subscribeTimeout; once loading completes, a missing store still fails fast.

Streaming adoption works with the framework loaded from <head> #

Loading the framework from <head> deferred component observation until DOMContentLoaded, which a streamed response fires only at its final chunk, so every component in the stream waited for the response to close before initializing. Observation now starts immediately against the document root, and <head> placement behaves the same as <body>.

1.4.0 — 2026-08-08

Added

Cross-field validation rules #

A rules: block on a component declares facts about the form as a whole, such as a return date on or after departure, on the same validation pass as data-validate, with the same submit gate, invalid class, and data-error-for plumbing. check: and when: take a string evaluated by the CSP-safe interpreter or a function bound to the component. Three development diagnostics (WF-228, WF-229, WF-234) catch a declaration that fails to parse, a check that throws, and a return value that is never a verdict. See Advanced Forms.

** in the CSP-safe expression evaluator #

The exponentiation operator parses in the CSP-safe expression evaluator, right-associative at JavaScript's precedence, so 2 ** 3 ** 2 is 512 in every build including CSP-locked pages. Previously an expression using ** parsed as something else instead of being refused. See Expressions.

getItemFromEvent resolves pool entities #

getItemFromEvent returned null for pool entities, breaking any delegated handler on a pool container. It now resolves the entity and its element as it does for data-list rows, returning { item, element }. index stays undefined for pools, since pool storage is index-unstable. See getItemFromEvent.

Variadic add() / push() on pools #

pool.add() and pool.push() took a single object or an array and dropped every argument after the first. Both now accept multiple objects as separate arguments, matching Array.prototype.push, and insert them through the same single-fragment path bulk arrays use. See pool.push.

Auto-retry with backoff for data queries #

An opt-in retry: N on a query declaration, clamped to 0 through 10, re-runs a failed fetch on a doubling curve from 1 second to a 30 second cap before the failure reaches error or syncError. Rows on screen are never wiped while the ladder runs, going offline suspends it without spending an attempt, and any successful response resets it. See Freshness and Live Data.

patch() for sanctioned optimistic writes #

getQuery(name).patch(data) writes local data through the same path as a fetch. Keyed rows update in place, unseen keys append, a declared deleted: field removes by key, and an unkeyed payload replaces wholesale. The store is marked isStale until the next confirming sync, and lastSync, error, syncError, and pagination state are untouched. See Optimistic Updates with patch().

data-expect shape-drift warning for data queries #

A development-only data-expect="field:type, ..." declaration on a query element warns once per query per field when incoming rows drift from it, a declared field missing or present with the wrong primitive type. null is data, never drift, and nothing is coerced or rejected. See Declaring the Expected Shape.

Diagnostic for a data-query with no component ancestor #

A [data-query] element outside any component was a silent no-op, since queries bind during component binding and nothing ever processed it. A post-scan sweep now warns (WF-963) and tells you to wrap it in a component, even an empty one.

Fixed

A form's data-action no longer double-fires as a click handler #

A form's data-action is its submit handler, but the binding path also registered it a second time as a default click action. A form re-inserted by data-render fired its submit handler on any click inside it, including a click on a label. Present since v1.3.0.

Performance

CSP-safe expressions compile to closures #

The CSP-safe evaluator re-walked the expression's syntax tree on every evaluation. Each parsed expression now compiles once into a tree of closures, with operator dispatch, blocked-global checks, and variable resolution settled at compile time, and nothing touches new Function. Per-call overhead against the standard evaluator drops from 46x to under 3x, and typical expressions evaluate 2.4x to 4.7x faster. Cross-field rules: use this path in every build. See Expressions.

1.3.0 — 2026-07-26

Added

Live server data with data-query #

wildflower.query('products', { from: '/api/products', key: 'id', refresh: ['focus', 'etag:60'] }) declares a named source, and data-query="products" renders it, as a keyed list when the element has a <template> child and as a single record otherwise. Query state binds anywhere through $products.isLoading and its siblings and reads in JS through wildflower.getQuery(). The refresh ladder covers polling, conditional GET, focus, reconnect, and server push; a failed background sync never wipes rows on screen, and a query stands down after its last observer leaves. See Data Queries.

SSR pages hand off to data queries #

Inside data-ssr="true", server-rendered markup is adopted as a query's first result, so the document itself is the seed, with no hydration payload and no loading state over data the user can already see. The catch-up fetch runs as a background refresh and the freshness ladder keeps the page live from there. See SSR with Data Queries.

data-seed carries unrendered fields into hydrated state #

Fields the page never displays, such as row ids or machine-precision values, go in a data-seed attribute holding a small JSON object on the element the data belongs to. <tr data-seed='{"id":811}'> gives an adopted row its identity, and <span data-bind="count">250+</span> with data-seed='{"count":250}' hydrates as a number. Seed fields merge into the parsed state and win overlaps, on SSR component roots, list item roots, and query rows and records alike.

Paged and infinite results for data queries #

refresh({ params: { page } }) swaps the window while the previous rows stay visible as isStale, and append: true on the same call accumulates the page into the list, deduping by key. Once a query has accumulated, background updates merge rather than truncate, adding new rows at the head and updating existing rows in place, while an explicit refresh() without append starts over. A declared deleted: field removes rows by key wherever they arrive, since a windowed fetch cannot infer deletion from absence. The app owns the page or cursor.

State-preserving reorders via Element.moveBefore #

Keyed list and pool reorders now move existing DOM nodes atomically where the browser supports Element.moveBefore, preserving focus, text selection, and running CSS animations through the move. An input inside a row keeps its cursor when the row changes position. Browsers without the API get the previous insert-based behavior, unchanged.

Page-load initialization yields with scheduler.yield #

Large page-load scans yield to the browser with continuation priority where scheduler.yield is available, so input handling stays responsive during heavy initialization while the scan resumes ahead of other queued work. Initialization order is identical everywhere; browsers without the API use the previous scheduling.

$entity.path values in props #

data-prop-* and data-props values resolve external entity paths, so a child component can receive state from any store or query directly: <div data-component="badge" data-prop-total="$cart.total">. Previously the parent had to mirror the external value into its own state to pass it down.

Every warning now carries an error code #

All user-facing development warnings route through a WF- code with an entry in the error-codes reference, which now spans 96 entries including a new Security section. Every diagnostic the framework emits links to a single page explaining what happened, why, and how to fix it; no warning lacks a code. Warnings remain compiled out of production builds.

The nano build tier #

A build variant below mini, the smallest tier at roughly 45 KB brotli. It ships the core reactive UI without the data-list render cluster, so data-bind, data-show, data-render, data-model, computed properties, external(), props, forms, error boundaries, directives, lifecycle hooks, and WildQuery are present. It is for widgets, embeds, and single-file pages that never render a list. Separating the list machinery surfaced two bugs, fixed in this release for every build and listed under Fixed.

pool.length and pool.size are reactive #

A computed that reads pool.length or pool.size re-evaluates when entities are added, removed, or cleared, including when the computed first runs before the pool has registered. Previously it evaluated once against the empty pool and stayed stale, and the WF-212 warning that guarded that trap is retired. The reactivity is on demand, so a pool no computed reads carries none, and per-frame entity mutation stays outside reactivity.

Releases ship with signed SLSA provenance #

wildflowerjs is published only by the tag-triggered CI workflow in the public repository through npm trusted publishing. npm generates a Sigstore-signed build attestation, token-based publishing is disabled on the package, and no publish credential exists anywhere. Verify an installed copy with npm audit signatures; the CDN inherits the guarantee, since jsDelivr and unpkg serve from npm. The full trust model is in the package's PROVENANCE.md.

Dev-mode diagnostics for nine silent failure modes #

Development builds now warn, with the fix inline, where the framework previously failed silently: state written at the top level of a definition instead of inside state: {}, a methods: or actions: block carried over from another framework, a lifecycle hook name wired as a data-action, a list whose <template> could not resolve, a method shadowing a state field, destroyComponent() with the element still in the document, index-based array methods on pools, and preventDefault() inside a replayed action. All are compiled out of production builds.

Releases publish with npm trusted publishing and attested provenance #

The npm package is published by a GitHub workflow using OIDC trusted publishing, with Sigstore-signed SLSA provenance on the published artifact. No npm token exists anywhere in the pipeline, and the CDN inherits the attested artifact since it serves from npm.

Breaking Changes

this.pool(name) is renamed to this.getPool(name) #

Breaking. The component method that fetches a pool handle by name is now this.getPool(name), matching this.getStore(name) for stores. One rule now covers both subsystems. this.pools.name and this.stores.name are the declared-collection containers; getPool(name) and getStore(name) fetch any instance by name, including a markup-only pool declared without a pools: block. The old this.pool() spelling is removed; there is no alias. Prefer the container this.pools.name in most code, and reach for getPool only for a markup-only or imperatively created pool.

Under the hood

One applier engine for list bindings #

List templates now compile to one flat applier program, executed by three entry points: initial paint, targeted rebind for a changed field, and positional re-evaluation after reorders. This replaces seven typed executors and three divergent re-evaluation sweeps, gives class, style, and attribute paint a single owner at every call site, and removes a double class write on initial render. Row-selection updates measure about 10% faster in our krausest rig, and the rendered DOM is byte-identical.

Fixed

Emptying a list no longer strands a row that a DOM library moved elsewhere #

A drag library such as SortableJS moves a row into another list before state catches up, and when the state update then emptied the source list, the clear operated on the source container alone, so the moved element survived beside the destination list's own render of the same item. The clear now removes every tracked row wherever it currently is, so cross-list drag needs no manual evt.item cleanup.

Store destroy() and beforeDestroy() lifecycle hooks now run #

Stores accepted teardown hooks in their definition and silently discarded them at registration, so no store teardown ever ran user cleanup. An interval started in init() outlived its store. Both hooks now fire on wildflower.unregister() and every other teardown path, with this bound to store state and methods, matching how components have always behaved.

Binding validation understands configurable-template scope #

The development-mode binding validator checked a data-use-template expansion's bindings against the host component's state rather than the data-with scope they actually resolve in, reporting working bindings as undefined properties. Slot-template subtrees are now excluded from host-scope validation, matching every runtime binding path. Development builds only; production behavior was always correct.

data-bind-attr values are sanitized in every build #

The attribute-value sanitizers lived inside the list cluster, so builds that exclude it (lite, mini) could write unsanitized attribute values. The sanitizers now ship in a core module present in every tier and apply on every binding path.

Props that feed computed properties track reliably #

A reactivity gap in the props-binding update path was surfaced and fixed while extracting that path from the list cluster for the nano tier.

data-bind-class on a list row resolves the item field first #

A class binding whose name was both a list-item field and a same-name component computed or state property resolved to the component value, diverging from text, style, and attribute bindings, which are all item-first per the documented rule. All class paths are now item-field-first. Computed-over-state precedence is preserved for names that have no item field.

A <template> inside <svg> no longer crashes component initialization #

The HTML parser treats a <template> inside an SVG subtree as inert foreign content with no content fragment. The component's list discovery read through it and threw, taking down the entire component's initialization. Discovery now skips the parser's leftover, and development builds explain what the parser did and suggest the working patterns for repeated SVG primitives (a fixed set of bound elements, or a precomputed path).

tick() on a build without the pool module no longer crashes init #

Components, stores, and plugins that define tick() on a tier that excludes the pool frame loop crashed initialization through a missing internal call. Registration is now guarded on every path, and development builds warn that tick will never run on that tier.

A markup portal in a component with no init() teleports on page load #

A portal declared in the initial markup of a component without an init() method was skipped by the page-load path, which teleported markup portals only for components that defined init(). The markup-portal pass now runs for every component on that path, so a purely declarative portal teleports regardless.

Array-form subscribe: registers in production builds #

A component declaring subscribe: ['cart'], the array "wait only" form, picked up its store dependency through a development-only path. It re-rendered correctly in dev builds and silently stopped in minified production builds. Both builds now register the dependency the same way, and stores created after the component are covered.

A computed with no reactive sources no longer caches forever #

A computed that ran before the data it reads existed tracked no sources, so nothing could ever wake it, and it returned that first value for the life of the component. Source-less computeds now re-evaluate on each read and correct themselves once the data exists. Computeds that track sources cache exactly as before.

Props passed to a child refresh when a store drives the parent's computed #

When a store update flowed through a parent's computed property into a child's prop, the child kept its initial value while the parent's own bindings updated. The dependents sweep now refreshes child props on that path, so a child sees the new value at the same time the parent does.

Performance

Large-list creation defers per-item reactive proxies #

Bulk list creation (create, replace, append) reads raw items instead of allocating a reactive proxy per row up front, and consumers that need the proxy resolve it on demand. Handler items stay identity-stable against your own state reads, item writes still fire the component update dispatch, and memory is at parity with the previous release. In our krausest rig this cut large-list creation script cost by roughly 10%.

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 registers its rows' bound fields on one per-list update dispatcher, and an in-place field change such as items[3].qty = 5 routes to exactly the bindings that read that field, down to a single DOM write for a plainly bound field. Lists whose templates use item-level computeds use roughly 20 to 25% less memory per row, and one update path replaces four. There is no API change.

Added

wildflower.unregister(name) unified component/store teardown #

wildflower.unregister(name) removes a component definition, destroying its live instances and disposing their state and effects, and disposes a store of that name, so the name is free to register again. Registration was first-write-wins with no way to replace a definition. It is safe to call for an unknown name, and it serves live preview, hot reload, runtime swaps, and tests that need a clean registry.

Dev-mode warning (WF-215) for a re-registration with a different definition #

Re-registering a component or store under an existing name is skipped, keeping the original definition. When the incoming definition differs, which is almost always an accidental collision or a hot reload without teardown, development builds now warn (WF-215), identify the entity, and point at wildflower.unregister. Identical re-registrations stay quiet, and the comparison hashes method source, so a differing method body is caught.

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, and a computed declared without parameters evaluates at component scope, so a this read of an item field inside it resolved undefined. Development builds now warn (WF-214) when such a read misses on the component while the current item has a property of that name, and suggest the (item) signature. Zero-argument computeds that read only component state never trigger it.

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 identifies the offending computed and gives the fix, which is to 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 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, so aria-expanded emits ="false" instead of vanishing. Boolean attributes are unchanged, and false still removes them. Migration: an element bound { 'data-active': isActive } now matches [data-active] when the value is false, so bind null or undefined to remove the attribute, or select on the value.

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 logged on every re-run 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 #

A per-item computed that reads component state behind a short-circuit, such as openField === 'status' && openId === item.id, now wakes when that state changes; previously the unread branch never registered as a dependency and the binding stayed stuck. A per-item data-render insert is no longer undone by a stale cached false, and a data-render placeholder comment no longer triggers a swallowed error.

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 and identify 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 starts 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 #

Row creation reads values straight from the raw array rather than through the reactive proxy, since create-time reads need no dependency tracking, resolves bound child elements by walking node pointers instead of indexing a live collection, and copies only the item properties its class expressions reference. Builds compiled without server-side rendering also drop an unused legacy list path.

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. splice, indexOf, and slice are absent because 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: minilitemin (core) → spafull.

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 runs alongside jQuery 3.x and 4.x on the same page, the WordPress and legacy-CMS case, with no changes on either side. The coverage spans globals safety, DOM ownership boundaries, elements handled by both libraries, mutation isolation, attribute preservation under reactive updates, AJAX-injected components, $.noConflict(), and legacy-CMS hardening (detach and append round-trips, stale 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 in every binding type, data-bind, data-bind-class, data-bind-style, data-bind-attr, data-show, and data-render, both as bare references and inside compound expressions (ternaries, object syntax, string concatenation). Nested lists resolve against the inner item, with the outer context available as _parent. v1.0 silently evaluated such references as undefined. There is a 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 so batch usage is exception-safe. 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. Do not call it from inside a reactive effect or computed, since 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. The targeted-rebind optimization compared each binding's path with the changed property, and a binding whose path is a computed name never matched the property the computed body reads. A binding whose name or expression variables match a registered computed now skips that filter and re-applies.

Item-level computeds in class binding expressions #

Class bindings like data-bind-class="isOn ? 'active' : 'inactive'" silently evaluated to undefined. Two evaluator branches decided whether a name was an item-level computed by inspecting the arity of a wrapped accessor, which always reported zero, so neither branch ever fired for any computed. Both now look up the original computed function and evaluate it in the row's context.

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 is not a raw field on the parent item, the way data-bind already resolved them. Previously item-level computeds only worked as data-bind values, not as nested-list array sources, so the parent rows had to be pre-decorated. Both the initial mount and the live per-frame read take the fallback.

Multi-component scan init race that left nested data-list inner items unrendered #

The render effect fired synchronously, but the parent-to-child list relationships were registered later in initialization, so a render that ran in the scan's first window saw none and skipped nested-list integration. The outer list rendered and the inner data-list stayed a bare <template>. The visible symptom was section headers with no rows beneath them, intermittent with idle-callback timing. The scan root is now walked once before features run, and every template's relationships are registered up front.

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 collected and applied. Previously they were silently skipped, because the ownership check treated the list root itself as inside a list and filtered it out; it now checks ancestors only. This 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 attribute is now stripped on every row-creation path: the cached template, the rendered innerHTML parts, the clone fallback used when the cached template is bypassed (root element and all descendants), and the data-render conditional template clones.

Hover events on data-list row templates #

data-action declarations for mouseenter, mouseleave, mouseover, and mouseout inside list-row templates were silently dropped, because the delegated event registry only attached listeners for a fixed set of event types that excluded them. mouseover and mouseout are now in the set (both bubble), and mouseenter and mouseleave are synthesized from them with 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, because the per-row context kept a single action per element and skipped the rest. Every declared event-to-handler pair is now kept on the row, and the dispatcher routes by event.type.

data-event-outside on data-list row children #

data-event-outside inside a row template was a silent no-op: neither row-creation path registered the document-level outside-click handler. Row templates now record that an outside handler is declared, every row-creation path registers it per row, and outside-click handling was rebuilt around a single document listener with a per-element registry keyed by element and method name.

data-event-outside row-child handlers receive a details object #

Row-child outside-click handlers received only (event, el), unlike regular row actions, which receive (event, el, details) with details.item. The row context is now captured at registration, and the outside-click dispatch builds the same { item, index, list, length, first, last, context } shape. Non-list handlers are unchanged.

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.

A bulk pool clear no longer costs quadratic time #

The pool's static and dynamic sub-array tracking removed entries with a linear search, while the pool's own main array already used constant-time swap-with-last removal. Removal now uses a stored sub-index, so a bulk clear at 800 or more entities no longer pays a quadratic cleanup cost.

List change detection no longer misses interior edits 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: the effect-notification loop now iterates a snapshot, so a synchronous effect that re-enters it cannot corrupt the outer loop, and a reusable effect set that was shared across instances is now per instance.

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. The cached value is also updated synchronously when a computed leaves the optimized fast path.

Action handlers fired before init() completes are queued and replayed #

Events that arrive before init() completes (for example clicks while init awaits a slow subscribe) used to throw or be silently dropped. They are now queued and replayed in order after init() returns, and replay errors route through onError. Two limits: lifecycle names (init, beforeInit, destroy, and the rest) must not be reused for action handlers, and a replayed handler sees the original event, whose event.preventDefault() is a no-op by replay time, so forms should use data-event-prevent.

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 overwrite the outer's dependency-tracking buffer, leaving the outer with an incomplete dependency set. The buffer is now saved and restored across nested evaluations, and dependency comparison reads from a local variable.

Effect cleanup on component destroy walks all three places effects can live #

The destroy sweep disposed the effects held on the instance and on its context but missed the effects held on the state manager, where framework-internal effects created before the instance existed were stored. Those survived destroyComponent, including every list's structural effect and its per-item effects, and kept firing against external store mutations on already-removed DOM. The sweep now covers all three places.

data-bind-style and data-bind-attr clear keys that drop out of the bound result #

When a style/attr computed shrank between renders (for example {background: color} becoming {} when unassigned), the framework applied the new keys but never cleared the dropped ones, so an avatar kept its old background after the assignee was set back to null. All three apply paths now track the keys they wrote per element and clear the dropped ones before applying new ones.

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 development warning per binding pointing at the root cause. The effect-driven path already handled the object form ({className: bool}); the element-level path now matches it. 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 looks for the nearest data-action ancestor first and falls back to the row's compiled metadata. A row's data-action is stripped from the DOM on the fast render path, so the search walked past the row and found an outer ancestor's action (for example a data-event-outside wrapper), saw the scope mismatch, and gave up without trying the fallback. An out-of-scope match now retries the metadata fallback and accepts only rows that belong to this list, which keeps nested lists safe.

Item-level computeds in list rows re-evaluate on external store/plugin mutations #

Per-item effects only tracked the row's own item and the component's local state, so an item-level computed that read from another store (for example a row badge counting subtasks kept in a separate store) went silently stale on cross-store mutations. Per-item effects are now registered per component and woken on any entity state change, whatever the mutation's shape: array reassignment, push or splice, row property writes, reorders, and keyed lookups.

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, development builds now warn. This catches a class of aliasing bugs where the same nested object is reachable from two unrelated state subtrees and dependency tracking cannot tell which path a change occurred on.

Subscribe-only components now receive onStoreUpdate notifications #

A store decided whether it had anyone to notify on its first state change, which happens at the end of construction before any component has registered, and cached the answer as no. Later subscriptions filled the subscriber set but never refreshed that cache, so dispatch short-circuited and onStoreUpdate never fired. Components that also read the store through a computed or data-bind were unaffected, since a tracked read refreshed the cache as a side effect; subscribe-only components silently received nothing. Subscribing now refreshes the cache.

Subscribe-block components are now registered as entity dependents of their store #

A subscribe: { store: ['path'] } contract wired the component as a path subscriber only, not as a dependent of the store entity, and the entity-dependent dispatch is what marks dependent computeds dirty, so a data-bind or data-show backed by a computed reading the subscribed path could stay stale; the visible symptom was a detail pane blank after a soft reload. Subscribing now registers the entity dependency as well.

Cross-store computeds that return early still track the store they read #

The lean re-evaluation path for cross-store computeds assumed external dependencies were stable after the first evaluation and skipped dependency tracking. A computed whose first evaluation returned early, before its cross-store read, never tracked that store, and every later lean re-evaluation skipped tracking too, so the computed stayed disconnected from a store it reads on the other branch. The lean path now tracks as well, with per-call deduplication keeping the cost small.

A store that arrives late re-tracks the computeds waiting on it #

When a late-arriving store resolved, the computed cache was cleared but each computed stayed on the lean re-evaluation path, which could not re-establish the cross-store dependency. Resolution now forces the next evaluation of every computed through the full tracking path. Together with the lean-path tracking fix above, this closes the late-store failure end to end.

Component-level computeds referenced inside list templates no longer get falsely flagged item-level #

Every computed touched during a list-row evaluation was marked item-level, whatever its arity. A zero-argument component computed referenced inside a row binding was tagged item-level, and the component-level cascade then skipped it on the assumption that per-row effects would re-evaluate it, leaving the binding silently stuck on its first cached value. Only computeds that take an item argument are marked now.

Store subscriptions register before computed setup in the scanner #

The asynchronous scanner could yield between computed setup and feature setup. An asynchronous store initialization resolving in that window mutated state before the component was registered as a dependent, so the cascade missed it and the component stayed on its empty cached value; the visible symptom was a detail pane that stayed blank after a soft reload in Firefox. Store subscriptions now register in a synchronous pass before any computed evaluation is queued, in both scan orchestrators.

List-row click delegation now bounds closest() to the list element #

When the fast render path stripped a row's data-action, the click's nearest-action search walked past the empty row and matched an unrelated outer ancestor (for example a <form data-action="submit"> wrapping a modal), then gave up on the form's tag name without reaching the compiled-metadata fallback: a silent dead click. Any match outside the owning list element is now rejected, and a fallback recovers the action name from the row's compiled metadata when the attribute is absent. The compile-time strip stays, since it matters at benchmark scale.

Per-row field precedence honoured by data-bind-style and data-bind-class #

In list templates, style and class bindings resolved simple names against the component's computeds first, shadowing a same-name row field, and a component-level effect was registered for every in-list style and class binding, creating a second writer that raced the row update path. The visible symptom was a row's color following the parent's on most reloads. Lookup is now item-first, and in-list style and class bindings get no component-level effect.

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 route table and emitted three warnings per page load. The router now auto-initializes only when options.routes is a non-empty array; the declarative form is unchanged, and the staged form leaves timing to the caller.

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), while the initial-load and popstate paths still leave the URL alone.

data-cloak strip for components registered after framework init #

Closes a Chrome-only flash on default-hidden elements (welcome modals, routed sections, popovers) inside components whose wildflower.component(...) call runs after the initial cloak-strip frame, so the element appeared, then hid. The strip removed data-cloak from elements whose component had not initialized yet, exposing them until the late render effect wrote display:none. The strip now waits for an uninitialized component, and the component strips the remaining cloaks after its first render effect runs, with both passes committing the right inline display first.

Nested-list and refresh-effect cleanup on list re-render #

The per-list refresh effect was cleaned up only on component destroy, so a mid-life re-render left a stale effect firing on every mutation, and row removal disposed the row's own effect but not the nested lists inside it. A 26-row list with four nested lists per row leaked about 870 effects per change. A list's dispose now recursively disposes its nested lists and its own refresh effect, on both the mid-life and destroy paths.

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 #

Portal binding rendering replaced a linear scan over every binding with a per-component index, removing a per-teleport hotspot in apps with many active bindings.

Reactivity batch change-detection rebuilt around the proxy #

wildflower.batch(fn), startBatch, and applyBatch no longer serialize every component's state on entry and re-diff on exit; the proxy's set trap already records per-batch mutations, and the new path consumes that directly. startBatch is now constant-time per batch instead of scaling with total state size. In the krausest data-pool benchmark, swapping rows is about 25% faster and single removal about 20% faster, and about 600 lines of legacy code were removed.

Portal visibility update skipped for portal-free components #

Portal visibility was re-checked with a descendant query on every entity state change for every component, before finding no portals and returning. For apps without portals that was a DOM walk per mutation per component, 38% of main-thread time in one select and deselect cycle. Whether a component has portals is now recorded at initialization (and on late portal discovery in list items), and the check is skipped entirely when it has none.

Class bindings skip eager item-computed evaluation when nothing needs it #

Before applying any class binding on a row, every item-level computed on the component was evaluated eagerly, whether or not any evaluator on the row needed the merged context. For simple-property class bindings, the common case, that allocated two proxies per computed per row per update and discarded the result. The row's class evaluators are now scanned first, and the eager loop is skipped when none of them needs the merged context. Reactivity and per-item resolution are 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 dependencies, and non-matching dependents are skipped entirely. The narrowing applies only to explicit subscribe: {} contracts; computed-path notifications, store-computed readers, and dependents with missing metadata still invalidate in full.

Security

xlink:href sanitizer coverage #

xlink:href is now on the URL-attribute allow-list for both list bindings and pool bindings. 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 execute it on activation. It is now blocked with the same policy used for href, src, formaction, action, and poster. This was the one exploitable finding of the security audit, and each audited finding now has a regression test through a realistic ingress path.

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