phlex-reactive 0.11.3 → 0.11.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -640,6 +640,90 @@ module Phlex
640
640
  }
641
641
  end
642
642
 
643
+ # Tag-chip input (issue #203) — the composed combobox/tags primitive.
644
+ # Spread onto the ROOT (mix with reactive_root); it names the hidden
645
+ # field that stores the COMMA-JOINED value, and the generic controller
646
+ # maintains that field + the chip list entirely client-side. The value
647
+ # is FORM state (like text in an input), never component state — so
648
+ # add/remove round-trips nothing; the surrounding form submit carries
649
+ # the joined value and the server splits it (`tags.split(",")`).
650
+ #
651
+ # div(**mix(reactive_root, reactive_tags(:tags), reactive_filter(:tag_query))) do
652
+ # input(type: :hidden, **reactive_field(:tags), value: @tags.join(","))
653
+ # div(data: { reactive_tags_list: true }) { } # chips render here
654
+ # template(data: { reactive_tags_template: true }) do # the chip markup (server-owned)
655
+ # span(class: "chip") do
656
+ # span(data: { reactive_tag_text: true }) # the client writes the tag here (textContent)
657
+ # button(**reactive_tags_remove) { "×" } # the client fills the tag param per chip
658
+ # end
659
+ # end
660
+ # input(name: "tag_query", **mix(reactive_listnav, reactive_tags_add))
661
+ # button(**reactive_tags_option("Ruby")) { "Ruby" } # preloaded options, filter narrows them
662
+ # end
663
+ #
664
+ # The chip list is a CLIENT PROJECTION of the hidden field: every sync
665
+ # rebuilds the chips by cloning the <template> (textContent writes only
666
+ # — never innerHTML), so the hidden value is the single source of truth
667
+ # and a server re-render/morph re-seeds cleanly. An option whose tag is
668
+ # already selected is hidden and marked data-reactive-tags-selected
669
+ # (reactive_filter keeps it hidden through re-filters). Tags dedupe
670
+ # case-insensitively, keeping the first casing.
671
+ #
672
+ # Composes with reactive_filter (type to narrow — same driving input)
673
+ # and reactive_listnav (Arrow/Enter/Escape; Enter picks the highlighted
674
+ # option via its own tagsPick trigger, and reactive_tags_add only adds
675
+ # the TYPED text when nothing is highlighted — no double add).
676
+ def reactive_tags(field = nil)
677
+ if field.nil? || field.to_s.strip.empty?
678
+ raise ArgumentError, "reactive_tags needs a field name — reactive_tags(:tags)"
679
+ end
680
+
681
+ # Compile the field to a scoped [name="…"] selector, the reactive_filter
682
+ # convention — so the hidden field written via reactive_field(:tags)
683
+ # resolves under reactive_scope too.
684
+ { data: { reactive_tags_field: %([name="#{scoped_field_name(field)}"]) } }
685
+ end
686
+
687
+ # The Enter-to-add trigger for the tags query input (issue #203) — a
688
+ # CLIENT-ONLY keyboard action (no dispatch descriptor, no POST). Mix it
689
+ # AFTER reactive_listnav so Enter prefers the highlighted option
690
+ # (listnavPick preventDefaults; tagsAdd then skips), and free text adds
691
+ # only when nothing is highlighted:
692
+ # input(name: "tag_query", **mix(reactive_listnav, reactive_tags_add))
693
+ # Enter never submits the enclosing form — the client preventDefaults.
694
+ def reactive_tags_add
695
+ { data: { action: "keydown.enter->reactive#tagsAdd" } }
696
+ end
697
+
698
+ # A preloaded option row that ADDS its tag on click (issue #203) — the
699
+ # tags sibling of the combobox's on(:select) option, but CLIENT-ONLY
700
+ # (form state, no POST). Emits the [role=option] convention (so
701
+ # reactive_filter/reactive_listnav see it), the forced type="button" (a
702
+ # bare button inside a <form> would submit it), and the tag value the
703
+ # client reads. Compose extra attrs (the filter haystack, a testid) via
704
+ # mix. The tag can't contain a comma — it would corrupt the joined value.
705
+ def reactive_tags_option(tag)
706
+ {
707
+ type: "button",
708
+ role: "option",
709
+ data: {
710
+ action: "click->reactive#tagsPick",
711
+ reactive_tag_param: tags_tag!(:reactive_tags_option, tag)
712
+ }
713
+ }
714
+ end
715
+
716
+ # A chip's remove button (issue #203) — client-only, no POST. Two forms:
717
+ # inside the <template data-reactive-tags-template> chip, call it with NO
718
+ # tag (the client fills data-reactive-tag-param per cloned chip); on a
719
+ # server-rendered initial chip, pass the tag explicitly:
720
+ # button(**reactive_tags_remove(tag)) { "×" }
721
+ def reactive_tags_remove(tag = nil)
722
+ attrs = { type: "button", data: { action: "click->reactive#tagsRemove" } }
723
+ attrs[:data][:reactive_tag_param] = tags_tag!(:reactive_tags_remove, tag) unless tag.nil?
724
+ attrs
725
+ end
726
+
643
727
  # CROSS-ROOT value-conditional visibility (issue #164) — the visibility
644
728
  # parallel to reactive_compute's `mirror:` (#159). A plain reactive_show
645
729
  # is root-scoped by design (#15), so it can't express "this control
@@ -825,6 +909,21 @@ module Phlex
825
909
  selector
826
910
  end
827
911
 
912
+ # A declared tag value for reactive_tags_option/reactive_tags_remove
913
+ # (issue #203), validated non-blank and comma-free at render — the hidden
914
+ # field is comma-joined, so a declared tag containing a comma would
915
+ # corrupt the stored value (and a blank tag is a dead trigger).
916
+ def tags_tag!(helper, tag)
917
+ value = tag.to_s.strip
918
+ raise ArgumentError, "#{helper} needs a non-blank tag, got #{tag.inspect}" if value.empty?
919
+ if value.include?(",")
920
+ raise ArgumentError,
921
+ "#{helper}(#{tag.inspect}): a tag can't contain a comma — the hidden field is comma-joined"
922
+ end
923
+
924
+ value
925
+ end
926
+
828
927
  # Issue #179: apply a confirm: gate to a trigger's data hash. A String is
829
928
  # the static #52/#55 form (data-reactive-confirm-param, unchanged). A Hash
830
929
  # is the CONDITIONAL form (data-reactive-confirm-when-param, JSON) — warn
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Phlex
4
4
  module Reactive
5
- VERSION = "0.11.3"
5
+ VERSION = "0.11.4"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phlex-reactive
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.3
4
+ version: 0.11.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson