You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
phlex-reactive 0.11.4 ships the unstyled tag-chip behavior primitives (zoolutions/phlex-reactive#203, PR zoolutions/phlex-reactive#206): reactive_tags (root names a hidden comma-joined field the client maintains), reactive_tags_add (Enter adds typed text), reactive_tags_option (click a suggestion), reactive_tags_remove (chip remove), composing with reactive_filter + reactive_listnav. The agreed split: phlex-reactive owns the behavior contract, phlex-forms owns the polished tag_field — label/error/hint chrome, daisyUI styling, model binding. This issue is that draft, to judge the composition end-to-end.
The widget is CLIENT-ONLY (Phlex::Reactive::ClientBindings — form state, no token, zero round trips), so it follows the Forms::Live soft-dependency pattern: loaded only when Phlex::Reactive is defined, loader.ignored otherwise.
Usage target
f.field:tags,as: :tags,suggestions: %w[RubyRailsHotwirePostgres]# or with haystacks (synonyms the filter matches):f.field:tags,as: :tags,suggestions: {"Postgres"=>"postgres database db sql"}
Submits ONE comma-joined param (user[tags] = "Ruby,Rails"), the primitive's wire contract — the model splits (attribute :tags + a tags=(v) = super(v.is_a?(String) ? v.split(",") : v) writer, or the cosmos Taggable concern).
The leaf component (sketch)
# lib/forms/tag_field.rb — ignored when Phlex::Reactive is absent (the Forms::Live gate)moduleFormsclassTagField < Phlex::HTMLincludePhlex::Reactive::ClientBindingsdefinitialize(*modifiers,name:,id:,value: nil,suggestions: [],error: false,placeholder: "Add a tag…", **attributes)@modifiers=modifiers@name=name# "user[tags]" — INSTANCE-dynamic (see caveat 1)@id=id@value=value.is_a?(Array) ? value.compact.join(",") : value.to_s@suggestions=suggestions.is_a?(Hash) ? suggestions : suggestions.to_h{[it,it.downcase]}@error=error@placeholder=placeholder@attributes=attributessuper()enddefview_templatediv(**mix(reactive_root(id: "#{@id}_widget"),# RAW wire attrs, not the reactive_tags/reactive_filter sugar: the helpers# compile a SYMBOL through the component's class-level reactive_scope, but a# form builder's wire name is per-instance ("user[tags]"). The data attrs# are the public contract; any CSS selector works. (Caveat 1 below.){data: {reactive_tags_field: %([name="#{@name}"]),reactive_filter_input: "##{@id}_query"# by ID → the query never submits (caveat 2)}},class: "tag-field"))doinput(type: :hidden,name: @name,id: @id,value: @value)div(class: "flex flex-wrap gap-1",data: {reactive_tags_list: true})docurrent_tags.each{chip(it)}# server-rendered first paintendtemplate(data: {reactive_tags_template: true}){chip}input(**mix(reactive_listnav,reactive_tags_add,id: "#{@id}_query",type: "search",autocomplete: "off",placeholder: @placeholder,class: input_classes))# NO name → never submitsul(class: "menu bg-base-200 rounded-box")do@suggestions.eachdo |tag,haystack|
lidobutton(**mix(reactive_tags_option(tag),data: {reactive_filter_text: haystack})){tag}endendendendendprivatedefcurrent_tags=@value.split(",").map(&:strip).reject(&:empty?)# One method, both forms: with a tag = server-rendered chip; without = the# template prototype (the client fills text + remove param per clone).defchip(tag=nil)span(class: "badge badge-primary gap-1",data: {reactive_tag: tag})dospan(data: {reactive_tag_text: true}){tag}button(**(tag ? reactive_tags_remove(tag) : reactive_tags_remove),aria: {label: "Remove"},class: "cursor-pointer"){"×"}endenddefinput_classes=PhlexForms::ClassMerge.merge("input w-full",@attributes[:class])endend
Builder#render_field_input (builder.rb:161): when :tags then render fo.tag_field(...)
Theme roles in PhlexForms::Theme.daisy/.plain + a Forms::Plain::TagField (aria-invalid, no daisy classes)
Chrome comes free: f.field already wraps in fo.control → Label + FieldError/FieldHint
Caveats surfaced by drafting (the end-to-end judgment)
Sugar gap in phlex-reactive: reactive_tags(:tags)/reactive_filter(:q) compile a symbol via class-level reactive_scope — no way to express an instance-dynamic wire name (user[tags]). Raw data attrs work today (they ARE the contract) but skip the render-time blank/comma validation. Possible upstream follow-up: accept a raw string form, e.g. reactive_tags(name: @name) / reactive_filter(input: "#id")-style escape hatch.
The query input should have NO name inside a real POST form (or it submits a stray param). The reactive_filter sugar only compiles [name=…] selectors; targeting the input by id via the raw attr avoids the stray param entirely. Upstream sugar could allow it.
Inside a Forms::Live form: the tag widget is a NESTED reactive root, so the outer live-validation root's #collectFields skips the hidden tags field (issue fix: radio value clobbering + validation controller path resolution (#13 #12) #15 ownership) — live validate won't see the tags value. Acceptable for v1 (tags rarely need live validation); worth a README note.
Value casting: phlex-forms could optionally ship the tags= split writer (or an ActiveModel::Type) so f.field :tags, as: :tags works against a plain attribute :tags, array: true with zero model code.
Context
phlex-reactive 0.11.4 ships the unstyled tag-chip behavior primitives (zoolutions/phlex-reactive#203, PR zoolutions/phlex-reactive#206):
reactive_tags(root names a hidden comma-joined field the client maintains),reactive_tags_add(Enter adds typed text),reactive_tags_option(click a suggestion),reactive_tags_remove(chip remove), composing withreactive_filter+reactive_listnav. The agreed split: phlex-reactive owns the behavior contract, phlex-forms owns the polishedtag_field— label/error/hint chrome, daisyUI styling, model binding. This issue is that draft, to judge the composition end-to-end.The widget is CLIENT-ONLY (
Phlex::Reactive::ClientBindings— form state, no token, zero round trips), so it follows theForms::Livesoft-dependency pattern: loaded only whenPhlex::Reactiveis defined,loader.ignored otherwise.Usage target
Submits ONE comma-joined param (
user[tags] = "Ruby,Rails"), the primitive's wire contract — the model splits (attribute :tags+ atags=(v) = super(v.is_a?(String) ? v.split(",") : v)writer, or the cosmosTaggableconcern).The leaf component (sketch)
Wiring (the ChoicesSelect precedent)
Forms::Field#tag_fieldmirroring#select(field.rb:79):theme[:tag_field].new(suggestions:, value: field_value, **field_attributes_minus_value_shape)Builder#render_field_input(builder.rb:161):when :tags then render fo.tag_field(...)PhlexForms::Theme.daisy/.plain+ aForms::Plain::TagField(aria-invalid, no daisy classes)f.fieldalready wraps info.control→ Label + FieldError/FieldHintCaveats surfaced by drafting (the end-to-end judgment)
reactive_tags(:tags)/reactive_filter(:q)compile a symbol via class-levelreactive_scope— no way to express an instance-dynamic wire name (user[tags]). Raw data attrs work today (they ARE the contract) but skip the render-time blank/comma validation. Possible upstream follow-up: accept a raw string form, e.g.reactive_tags(name: @name)/reactive_filter(input: "#id")-style escape hatch.nameinside a real POST form (or it submits a stray param). Thereactive_filtersugar only compiles[name=…]selectors; targeting the input by id via the raw attr avoids the stray param entirely. Upstream sugar could allow it.Forms::Liveform: the tag widget is a NESTED reactive root, so the outer live-validation root's#collectFieldsskips the hidden tags field (issue fix: radio value clobbering + validation controller path resolution (#13 #12) #15 ownership) — livevalidatewon't see the tags value. Acceptable for v1 (tags rarely need live validation); worth a README note.tags=split writer (or anActiveModel::Type) sof.field :tags, as: :tagsworks against a plainattribute :tags, array: truewith zero model code.Refs zoolutions/phlex-reactive#203