Skip to content

feat(component): instance-dynamic wire names — keyword escape hatches on the field-compiling helpers - #225

Merged
mhenrixon merged 1 commit into
mainfrom
issue-224-verbatim-wire-name-escape-hatches
Jul 11, 2026
Merged

feat(component): instance-dynamic wire names — keyword escape hatches on the field-compiling helpers#225
mhenrixon merged 1 commit into
mainfrom
issue-224-verbatim-wire-name-escape-hatches

Conversation

@mhenrixon

@mhenrixon mhenrixon commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements #224: every helper that compiles a field name through the class-level reactive_scope now takes a verbatim keyword escape hatch for instance-dynamic wire names — the gap that forced phlex-forms' tag_field draft (zoolutions/phlex-forms#6, caveats 1 & 2) onto raw data attrs, losing render-time validation.

Helper Escape hatch Emits
reactive_tags(name: "user[tags]") verbatim wire name data-reactive-tags-field='[name="user[tags]"]'
reactive_filter(input: "#tags_query") raw CSS selector data-reactive-filter-input='#tags_query'
nested_field_name(:items, :qty, scope: "order") per-call prefix, wins over reactive_scope order[items_attributes][NEW_ROW][qty]
reactive_nested_list(:items, as: :json, name: "order[items]") verbatim hidden-JSON-field name data-reactive-nested-json-field='[name="order[items]"]'

All four are never re-scoped, validated at render (blank / " / \ / control chars / booleans raise), and mutually exclusive with the blessed positional-field form, which stays the default. reactive_field already had this escape hatch (explicit name: wins) — it is the precedent and is unchanged.

Server-side sugar only. The client already resolves data-reactive-tags-field / data-reactive-filter-input as arbitrary root-scoped CSS selectors (#tagsField, #syncFilter); no JS changed (rake build:js_check green). Existing calls emit a byte-identical wire.

reactive_filter(input:) deliberately re-blesses the kwarg removed in #186 — narrowed to the one case the field form can't express: a deliberately name-less query input inside a real form (a named input would submit a stray param), targeted by id. The pre-0.10 input:/option: call shape is valid again with identical semantics instead of raising the removal error (CHANGELOG notes it).

Test coverage

  • Unit (spec/phlex/reactive/component_spec.rb): verbatim compile; stays verbatim under a declared reactive_scope; blank/quote/backslash/control-char/boolean rejection; mutual exclusion; name: without as: :json raises; bracketed scope: ("user[profile]") accepted; option:/group:/empty: compose with input:.
  • System (spec/system/tags_field_form_spec.rb, green under Puma AND Falcon): a new dummy demo (FormTagsFieldComponent, /form_tags_field) in the exact phlex-forms shape — verbatim user[tags] hidden field, id-targeted query input with no name. Proves: filter narrows by haystack, option-click + Enter add chips, remove works, zero action POSTs, no reload — then a real submit carries user[tags] comma-joined and the page's stray-param echo (request.query_parameters.keys - ["user"]) is empty.

Verification

  • bundle exec rspec spec/phlex spec/requests — 1381 passed
  • bundle exec rspec spec/system — full suite green (Puma); new spec also green under CAPYBARA_SERVER=falcon
  • bundle exec rubocop — clean (gem, 290 files; docs app file linted with its own config)
  • rake build:js_check — no client drift (no JS changes by design)
  • No bench: not a listed hot path — additive nil-default kwargs on render-time attribute builders; the blessed-form code path does no new work.

Closes #224
Refs zoolutions/phlex-forms#6

Deviations & judgment calls

Deviations

  • None from the issue's steps — the plan held. An adversarial review round (52-agent workflow: 4 lenses → 3 refuters per finding) added HARDENING the plan didn't specify, all runtime-verified by the refuters:
    1. verbatim_name_selector! also rejects backslashes and control characters (not just "): a raw newline in name: makes real Chromium's querySelectorAll THROW during the controller's connect() — breaking the whole reactive root, not just the binding (happy-dom is lenient, so the bun suite can't catch it) — and a trailing backslash CSS-escapes the closing quote so the selector silently matches the wrong name.
    2. All three new kwargs dispatch on nil-presence, not truthiness: the plausible input: cond && "#sel" / name: cond && "…" / scope: cond && "order" idiom with a false condition previously slipped past the guards and emitted a silent dead binding ([name=""]) or a corrupting wire name (false[items_attributes][…]). Booleans now fail loudly (filter_selector! + verbatim_name_selector! reject them; nested_scope! requires String/Symbol).

Discoveries

  • On the OLD reactive_tags(field = nil) signature, reactive_tags(name: " ") did not raise: Ruby folds unknown keywords into a positional Hash for a method that declares none, so the Hash became field and compiled a garbage-but-non-blank selector. The new signature makes name: a real kwarg, so that call shape now validates properly.
  • The existing blessed-form demo (TagsFieldComponent) gives its query input a name="tag_query", so its GET submit carries a stray tag_query= param — harmless there, but exactly the behavior input: exists to avoid. Left unchanged: it covers the blessed field form; FormTagsFieldComponent covers the escape hatch.

Judgment calls

  • nested_field_name(scope:) rejects non-String/Symbol and blank, but NOT " — the result is a name attribute value (Phlex-escaped), never interpolated into a [name="…"] selector, and a bracketed scope ("user[profile]", a nested fieldset's object name) must stay legal. The name: hatches DO reject "/\/control chars because they compile into a double-quoted selector the client queries with.
  • reactive_nested_list(name:) without as: :json raises (loud, guided) rather than silently ignoring the kwarg — the :attributes mode has no field to name, and a silent no-op would hide a wiring mistake.
  • The demo form is a GET echoing back to itself (matching the existing tags_field demo); the controller passes request.query_parameters.keys - ["user"] into the component, which renders them in a testid'd div — the machine-checkable "no stray param" proof.

Summary by CodeRabbit

  • New Features

    • Added escape-hatch options for reactive filters, tag inputs, and nested fields to support dynamic form-builder names and selectors.
    • Added stricter validation for blank, invalid, or conflicting selector and name inputs.
    • Added a tag-input form demonstration with filtering, adding, removing, and form submission support.
  • Documentation

    • Expanded API guidance and examples for dynamic scopes, verbatim field names, and name-less query inputs.
  • Tests

    • Added unit and end-to-end coverage for the new options, validation, and tag form behavior.

… on the field-compiling helpers

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 (zoolutions/phlex-forms#6) onto raw
data attrs, losing render-time validation. Every field-compiling helper
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 for the
  deliberately NAME-LESS query input (re-blesses the kwarg removed in
  #186, narrowed to this escape-hatch purpose)
- nested_field_name(:items, :qty, scope: "order") — per-call prefix,
  wins over reactive_scope
- reactive_nested_list(:items, as: :json, name: "order[items]")

Server-side sugar only: the client already resolves these attributes as
arbitrary root-scoped selectors; existing calls emit a byte-identical
wire. Hardened beyond the plan after adversarial review: verbatim names
reject backslashes/control chars (a raw newline makes Chromium's
querySelectorAll throw during connect, breaking the whole root), and the
kwargs dispatch on nil-presence so `input: cond && "#sel"` with a false
condition fails loudly instead of emitting a silent dead binding.

## Test Coverage
- unit: verbatim compile, never-re-scoped under reactive_scope, blank/
  quote/backslash/control/boolean rejection, mutual exclusion, name:
  without as: :json, bracketed scope:
- system (Puma + Falcon): the form-builder-shaped demo — id-targeted
  name-less query input filters/adds/removes client-side; a real submit
  carries user[tags] comma-joined with ZERO stray params

## Verification
- [x] bundle exec rubocop passes (gem + docs app)
- [x] bundle exec rspec spec/phlex spec/requests — 1381 passed
- [x] bundle exec rspec spec/system — full suite green (Puma); new spec
      also green under CAPYBARA_SERVER=falcon
- [x] rake build:js_check — no client drift (no JS changes by design)

Closes #224
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds validated escape hatches for dynamic reactive field names and selectors, documents their form-builder usage, and introduces a dummy tags form with unit and system coverage.

Changes

Dynamic wire names

Layer / File(s) Summary
Helper escape-hatch APIs and validation
lib/phlex/reactive/component/helpers.rb
Adds input:, name:, and scope: escape hatches with mutual-exclusion, blank-value, type, and selector-safety validation.
Documentation and public usage examples
CHANGELOG.md, README.md, docs/app/views/docs/pages/draft_rows_new_parent.rb
Documents raw selectors, verbatim form-builder names, per-call scopes, and JSON hidden-field naming.
Form-builder tags demo flow
spec/dummy/app/components/form_tags_field_component.rb, spec/dummy/app/controllers/demos_controller.rb, spec/dummy/config/routes.rb
Adds a tags form demo using a dynamic hidden wire, a name-less query input, client-side chip updates, and submitted-parameter rendering.
Helper and end-to-end verification
spec/phlex/reactive/component_spec.rb, spec/system/tags_field_form_spec.rb
Covers escape-hatch compilation, validation, client-side chip behavior, form submission, and stray-parameter exclusion.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant FormTagsFieldComponent
  participant DemosController
  Browser->>FormTagsFieldComponent: filter, add, or remove tags
  FormTagsFieldComponent-->>Browser: update hidden user[tags] field
  Browser->>DemosController: submit GET form
  DemosController->>FormTagsFieldComponent: pass submitted tags and stray parameters
  FormTagsFieldComponent-->>Browser: render submitted chips and echo
Loading

Possibly related PRs

Suggested labels: enhancement, documentation

Poem

A bunny wires names through the form,
With chips that hop and filters that swarm.
No stray params flee,
The scope stays free,
And JSON fields tuck safely warm.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding keyword escape hatches for instance-dynamic wire names in field-compiling helpers.
Linked Issues check ✅ Passed The changes match issue #224 by adding the four escape hatches, validation, coverage, and the requested docs updates.
Out of Scope Changes check ✅ Passed The diff stays focused on the documented helper, demo, test, and doc updates with no clear unrelated additions.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.44.1)
spec/phlex/reactive/component_spec.rb

ast-grep timed out on this file

spec/system/tags_field_form_spec.rb

ast-grep retry budget exhausted before isolating this batch


Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added documentation Improvements or additions to documentation enhancement New feature or request labels Jul 11, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
spec/system/tags_field_form_spec.rb (1)

18-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the duplicated system-spec helpers

install_fetch_spy and hidden_value are duplicated in spec/system/tags_field_spec.rb; move them to a shared system-spec helper to avoid drift.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/system/tags_field_form_spec.rb` around lines 18 - 32, Extract the
duplicated install_fetch_spy and hidden_value helpers from the tags field system
specs into the shared system-spec helper, then remove their local definitions
from both spec files and ensure both specs use the shared implementations
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@spec/system/tags_field_form_spec.rb`:
- Around line 18-32: Extract the duplicated install_fetch_spy and hidden_value
helpers from the tags field system specs into the shared system-spec helper,
then remove their local definitions from both spec files and ensure both specs
use the shared implementations unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7a5ea5c0-038d-4b3a-a816-d0a8574053b8

📥 Commits

Reviewing files that changed from the base of the PR and between cf02be2 and 469ba58.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • README.md
  • docs/app/views/docs/pages/draft_rows_new_parent.rb
  • lib/phlex/reactive/component/helpers.rb
  • spec/dummy/app/components/form_tags_field_component.rb
  • spec/dummy/app/controllers/demos_controller.rb
  • spec/dummy/config/routes.rb
  • spec/phlex/reactive/component_spec.rb
  • spec/system/tags_field_form_spec.rb

@mhenrixon

Copy link
Copy Markdown
Collaborator Author

Re CodeRabbit's nitpick (extract the duplicated install_fetch_spy/hidden_value spec helpers): deliberate — the per-file fetch spy is the established pattern across 13 system spec files, and the new spec matches the house convention. Consolidating it is a repo-wide refactor that belongs in its own chore PR, not here.

@mhenrixon
mhenrixon merged commit d88b35d into main Jul 11, 2026
11 checks passed
@mhenrixon
mhenrixon deleted the issue-224-verbatim-wire-name-escape-hatches branch July 11, 2026 13:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(component): instance-dynamic wire names — keyword escape hatches on the field-compiling helpers (unblocks phlex-forms tag_field)

1 participant