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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +58 -0
- data/app/javascript/phlex/reactive/reactive_controller.js +263 -1
- data/app/javascript/phlex/reactive/reactive_controller.min.js +2 -2
- data/app/javascript/phlex/reactive/reactive_controller.min.js.map +3 -3
- data/lib/phlex/reactive/component/helpers.rb +99 -0
- data/lib/phlex/reactive/version.rb +1 -1
- metadata +1 -1
|
@@ -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
|