Skip to content

feat(components): tag_field adopts phlex-reactive 0.12.2 escape-hatch sugar - #20

Merged
mhenrixon merged 1 commit into
mainfrom
issue-6-reactive-0.12.2
Jul 11, 2026
Merged

feat(components): tag_field adopts phlex-reactive 0.12.2 escape-hatch sugar#20
mhenrixon merged 1 commit into
mainfrom
issue-6-reactive-0.12.2

Conversation

@mhenrixon

Copy link
Copy Markdown
Collaborator

Summary

Closes #6.

Bumps phlex-reactive to >= 0.12.2 and migrates Forms::TagField off its raw wire-contract data attributes onto the 0.12.2 escape-hatch sugar — reactive_tags(name:) + reactive_filter(input:) — resolving issue #6 Caveats 1 & 2 (the sugar gap the issue was drafted against, since landed upstream in phlex-reactive #225).

tag_field itself already shipped in #7; this closes the remaining upstream-dependent work: dropping the raw-attr workaround now that phlex-reactive exposes the instance-dynamic escape hatches.

Two payoffs

  1. Render-time validation of the wire name. reactive_tags(name: "user[tags]") runs the name through verbatim_name_selector!, which rejects a blank / double-quote / backslash / control-char name at render. The raw %([name="#{name}"]) interpolation had no such guard — a stray " would silently break the [name="…"] selector the client queries with, or a control char would make querySelectorAll throw and abort the controller connect, both only in the browser.

  2. Restores client-side type-ahead filtering (the real find). The old code emitted only data-reactive-filter-input. The phlex-reactive 0.12.x client #syncFilter early-returns unless both data-reactive-filter-input and data-reactive-filter-option are present (reactive_controller.js: if (!inputSelector || !optionSelector) return). So under 0.12.x the tag widget's type-ahead was dead — the existing specs only asserted emitted markup, so nothing caught it. reactive_filter(input:) emits both selectors (option defaults to [role=option], exactly what the suggestion buttons use), so filtering runs again — standalone and on the live-form root.

Shape of the change

  • Forms::TagField#root_tag_attributes is now a private instance method calling the ClientBindings helpers on self; a new public TagField.query_id(id) centralizes the "#{id}_query" derivation so the standalone and hoisted query-input ids can't drift.
  • Forms::Live#form_attributes hoists the wire attrs by calling reactive_tags/reactive_filter directly — it's itself a reactive component with those helpers — instead of routing through TagField's (removed) class method.
  • Pins bumped to >= 0.12.2 in the gem's Gemfile (a soft dev/test dep — no gemspec dependency) and the docs site (docs/Gemfile + regenerated docs/Gemfile.lock, 0.11.50.12.2) so the live docs demo filters correctly.

Test Coverage

  • spec/forms/tag_field_spec.rb
    • emits data-reactive-filter-option="[role=option]" — the filter-regression guard (both filter selectors present ⇒ client type-ahead runs).
    • a malformed wire name (contains ") raises ArgumentError at render — the new render-time validation.
  • spec/forms/live_tag_field_spec.rb
    • the hoisted <form> reactive root also carries data-reactive-filter-option (the live path gets the fix too).

Verification

  • bundle exec rubocop lib spec — clean (87 files)
  • bundle exec rspec — 162 passed, 1 pending
  • End-to-end render probe: standalone widget emits tags-field + filter-input + filter-option, query input carries the matching id and no name (only the hidden field submits).
  • 4-lens adversarial verification, all clean: wire-attribute byte-equivalence, live-hoist parity, plain-theme + soft-dependency degradation, render-time-validation edge cases.

Deviations & judgment calls

  • The task was smaller than the issue. tag_field was already merged (feat(components): tag_field composed from phlex-reactive tag primitives #7, commit 43ca088); the stale issue-6-tag-field branch was the earlier draft and was not reused — this work started fresh from main. The genuinely-remaining task was the 0.12.2 sugar adoption the issue's own comment (2026-07-11) called for.
  • The dead-filter regression was discovered, not planned. It surfaced while checking whether the "extra" data-reactive-filter-option attr was inert. It isn't — the 0.12.x client needs it — so the migration is a real fix, not cosmetic. Added a spec as the regression guard.
  • Verified the 0.12.2 API against the gem source, not the issue comment. The comment claimed reactive_filter(input:) was the caveat-2 fix; a WebFetch of the changelog suggested input: might have been removed. The installed gem source settled it: both reactive_tags(name:) and reactive_filter(input:) exist in 0.12.2 and emit exactly what this change assumes.
  • Docs lockfile: the docs bundle wants Ruby 4.0.5 (local is 3.4.2), but bundle lock --update phlex-reactive regenerated docs/Gemfile.lock cleanly — only phlex-reactive moved, valid checksum. The docs deploy bundle installs fresh on release regardless.
  • Pre-existing edge, left out of scope: Forms::Field#field_id transforms brackets in the scope while Forms::Form#field_id doesn't. For live tag fields the scope is always a bracket-free param_key, so the two agree (verified); a nested-scope live tag field would be a latent edge — untouched by this change, out of scope for tag_field: composed from phlex-reactive 0.11.4's reactive_tags primitives (draft) #6.
  • The gem stays a soft dependency. Only dev/test + docs pins moved; the gemspec has no phlex-reactive dependency and the Plain-theme / no-reactive boot path is unchanged.

… sugar

## Summary
Bumps phlex-reactive to >= 0.12.2 and migrates Forms::TagField off its raw
wire-contract data attributes onto the 0.12.2 escape hatches
(reactive_tags(name:) + reactive_filter(input:)), resolving issue #6 Caveats 1 & 2.

Two payoffs:
- Render-time validation of the instance-dynamic wire name — reactive_tags(name:)
  rejects a blank / double-quote / backslash / control-char name (which would
  silently mis-bind or throw in the browser's querySelectorAll) at render.
- Restores client-side type-ahead filtering. The old raw attr emitted ONLY
  data-reactive-filter-input; the 0.12.x client #syncFilter early-returns unless
  BOTH -input AND -option are present, so filtering was dead. reactive_filter(input:)
  emits both, so the type-ahead runs again — standalone and on the live form root.

Forms::Live hoists the same wire attrs by calling reactive_tags/reactive_filter
directly (it is itself a reactive component); TagField.query_id centralizes the
shared "#{id}_query" derivation so the standalone and hoisted ids never drift.

## Test Coverage
- tag_field_spec: emits data-reactive-filter-option="[role=option]" (the filter
  regression guard); a malformed wire name raises ArgumentError at render.
- live_tag_field_spec: the hoisted <form> root also carries -filter-option.

## Verification
- [x] bundle exec rubocop lib spec passes (clean, 87 files)
- [x] bundle exec rspec passes (162 passed, 1 pending)
- [x] 4-lens adversarial verification clean (byte-equivalence, live-hoist parity,
      plain-theme degradation, render-validation edge cases)
@mhenrixon mhenrixon self-assigned this Jul 11, 2026
@mhenrixon
mhenrixon merged commit b1b5884 into main Jul 11, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tag_field: composed from phlex-reactive 0.11.4's reactive_tags primitives (draft)

1 participant