diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dcc774..1de2f1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,26 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Added +- **Instance-dynamic wire names — keyword escape hatches on the + field-compiling helpers (#224).** A form builder's wire name is computed per + instance (`user[tags]`), which the class-level `reactive_scope` compile can't + express — the gap that forced phlex-forms' `tag_field` draft + (mhenrixon/phlex-forms#6) onto raw data attrs. Every helper that compiles a + field name now takes a verbatim keyword escape hatch (never re-scoped, + validated at render, mutually exclusive with the blessed field form): + `reactive_tags(name: "user[tags]")`, `reactive_filter(input: "#tags_query")` + (a raw CSS selector — the deliberately **name-less** query input inside a + real form, targeted by id, so it can never submit a stray param), + `nested_field_name(:items, :qty, scope: "order")` (a per-call parent prefix + that wins over `reactive_scope`), and + `reactive_nested_list(:items, as: :json, name: "order[items]")` (the hidden + JSON sync field, JSON mode only). Server-side sugar only — the client already + resolves these attributes as arbitrary root-scoped selectors; the shipped + wire for existing calls is byte-identical. Note: `reactive_filter(input:)` + deliberately re-blesses the kwarg removed in #186 — the field form stays the + blessed default, and a pre-0.10 `input:`/`option:` call shape is valid again + with identical semantics instead of raising the removal error. + - **Project board — the kanban flagship demo (#216).** A live three-lane board on the docs site (`/docs/example-project-board`, `/demos/project-board`) composing the toolkit end to end: each card is its own nested reactive root diff --git a/README.md b/README.md index 3111116..a0df455 100644 --- a/README.md +++ b/README.md @@ -390,17 +390,17 @@ Use in controllers: `render turbo_stream: Counter.replace(counter)`. | `reactive_text(:name, initial)` | Mirror a compute output (or a declared input) into a **text node** — a live preview heading, a character counter, `"Hello, {name}"` — via `textContent` (XSS-safe). The text sibling of `reactive_field`; carries no `name`, so it's never POSTed. See [Client-side computes](#client-side-computes-reactive_compute--reactive_text). | | `reactive_show(if:/if_any:/unless:)` | **Value-conditional visibility** (the `x-show`/`data-show` case): spread onto the element to show/hide — it toggles `hidden` from the fields' **current values**, client-only, zero round trip. One conditions language: a **Hash is an AND**, an **Array is membership**, a **Range is a threshold**, `if_any:` is OR-of-AND, `unless:` negates. `reactive_values` computes first paint; `disable:` disables a hidden section's controls. See [Value-conditional visibility](#value-conditional-visibility-reactive_show). | | `reactive_show_targets(:field, "#id" => value)` | **Cross-root visibility**: the component that owns the field declares which **outside**, id-allowlisted elements it governs (a nav tab, a panel in another pane) — the visibility parallel of `mirror:`. Spread on the **root** via `mix(reactive_root, …)`, **once per root** — several fields go in one call via the hash form. The value uses the same `where`-style vocabulary (`"advanced"`, `%w[a b]`, `10..`); a `"#id"` **key** takes a full conditions Hash for a **multi-field** predicate (`"#warn" => { if: { type: "trade", price: ..0 } }`). Id selectors only (raise at render + client warn-skip); toggles `hidden` only. See [Value-conditional visibility](#value-conditional-visibility-reactive_show). | -| `reactive_filter(:field, option: nil, group: nil, empty: nil)` | **Client-side option filtering** for a preloaded combobox: spread onto the root and name the **field** that drives it — `reactive_filter(:q)` compiles `:q` to `[name="q"]` (scope-aware) and typing shows/hides the options by their `data-reactive-filter-text` haystack, **zero round trips**. `option:` defaults to `[role=option]`; optional `group:` collapses an all-hidden group header; `empty:` reveals a no-matches node. See [Client-side option filtering](#client-side-option-filtering-reactive_filter). | +| `reactive_filter(:field, option: nil, group: nil, empty: nil)` | **Client-side option filtering** for a preloaded combobox: spread onto the root and name the **field** that drives it — `reactive_filter(:q)` compiles `:q` to `[name="q"]` (scope-aware) and typing shows/hides the options by their `data-reactive-filter-text` haystack, **zero round trips**. `option:` defaults to `[role=option]`; optional `group:` collapses an all-hidden group header; `empty:` reveals a no-matches node. `input:` is the escape hatch — a raw CSS selector for a **name-less** driving input (`input: "#tags_query"`), the form-builder case. See [Client-side option filtering](#client-side-option-filtering-reactive_filter). | | `reactive_listnav("[role=option]")` | The **standalone** combobox keyboard wiring (Arrow/Enter/Escape) for an input that fires **no action** — the preload-and-filter case. Same behavior as `on(…, listnav:)`, minus the POST. | -| `reactive_tags(:tags)` | **Tag-chip input** (the combobox/tags widget): spread onto the root and name the hidden field that stores the **comma-joined** value — the client maintains that field + the chip list entirely client-side (form state, zero round trips), rebuilding chips from your server-owned `