phlex-reactive 0.9.4 → 0.9.5
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 +15 -0
- data/app/javascript/phlex/reactive/reactive_controller.js +125 -4
- 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 +169 -27
- data/lib/phlex/reactive/version.rb +1 -1
- metadata +1 -1
|
@@ -441,20 +441,37 @@ module Phlex
|
|
|
441
441
|
# after a morph; render the initial `hidden:` yourself (from the same
|
|
442
442
|
# server state that renders the field) to avoid a first-paint flash.
|
|
443
443
|
# Extra attrs deep-merge over the binding (mix), like reactive_field.
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
444
|
+
# Compound forms (issue #176 part A) — visibility that depends on MORE
|
|
445
|
+
# THAN ONE field, or a threshold, staying inside the eval-free literal
|
|
446
|
+
# contract. `all:`/`any:` fold a list of per-field terms (each the same
|
|
447
|
+
# equals:/not:/in:/gte:/gt:/lte:/lt: vocabulary) with one fixed
|
|
448
|
+
# connective; the compound has no single controlling field, so it
|
|
449
|
+
# serializes as ONE JSON attr (data-reactive-show) like
|
|
450
|
+
# reactive_show_targets rather than the flat data-reactive-show-* attrs:
|
|
451
|
+
#
|
|
452
|
+
# # visible while type == "individual" AND country != "domestic"
|
|
453
|
+
# div(**reactive_show(all: [
|
|
454
|
+
# { field: :type, equals: "individual" },
|
|
455
|
+
# { field: :country, not: "domestic" }
|
|
456
|
+
# ]))
|
|
457
|
+
# # visible while director OR shareholder is checked
|
|
458
|
+
# div(**reactive_show(any: [
|
|
459
|
+
# { field: :director, equals: true },
|
|
460
|
+
# { field: :shareholder, equals: true }
|
|
461
|
+
# ]))
|
|
462
|
+
# # a numeric threshold, single field or as a compound term
|
|
463
|
+
# div(**reactive_show(:quantity, gte: 10)) # visible while qty >= 10
|
|
464
|
+
# div(**reactive_show(all: [{ field: :amount, gte: 5000 }, ...]))
|
|
465
|
+
#
|
|
466
|
+
# `all:`/`any:` are additive and mutually exclusive with the single-field
|
|
467
|
+
# form and with each other — enforced loudly at render, like the
|
|
468
|
+
# one-predicate rule. A single-field `all:` with one term degrades to the
|
|
469
|
+
# flat form's behaviour client-side.
|
|
470
|
+
def reactive_show(field = nil, **options)
|
|
471
|
+
connectives = options.slice(*SHOW_CONNECTIVE_KEYS)
|
|
472
|
+
return reactive_show_compound(field, connectives, options) if connectives.any?
|
|
473
|
+
|
|
474
|
+
reactive_show_single(field, options)
|
|
458
475
|
end
|
|
459
476
|
|
|
460
477
|
# Client-side option filtering for the searchable combobox (issue #163)
|
|
@@ -658,12 +675,33 @@ module Phlex
|
|
|
658
675
|
reactive_record_for_nested.update!(**nested_attributes(association, attrs), **extra)
|
|
659
676
|
end
|
|
660
677
|
|
|
661
|
-
# The declared reactive_show predicates
|
|
662
|
-
#
|
|
663
|
-
#
|
|
664
|
-
#
|
|
665
|
-
#
|
|
666
|
-
|
|
678
|
+
# The declared reactive_show predicates: a literal match on the
|
|
679
|
+
# controlling field's current value. Exactly one per binding — enforced
|
|
680
|
+
# loudly at render (a dead binding must not no-op in the browser).
|
|
681
|
+
# `not`/`in` are Ruby keywords, so they arrive via **options rather than
|
|
682
|
+
# named kwargs. Issue #161 shipped the string-literal trio; issue #176
|
|
683
|
+
# adds the numeric-threshold quartet below.
|
|
684
|
+
SHOW_LITERAL_KEYS = %i[equals not in].freeze
|
|
685
|
+
|
|
686
|
+
# The numeric-threshold predicates (issue #176 part B): gte/gt/lte/lt
|
|
687
|
+
# compare the field value coerced to a Number against a literal number.
|
|
688
|
+
# Still a declared literal — the RHS is a number baked into the
|
|
689
|
+
# attribute, never an expression — the only new capability is ordered
|
|
690
|
+
# comparison. The RHS must be an actual Numeric in Ruby (stricter, so a
|
|
691
|
+
# typo like gte: "10" fails at render, not silently in the browser).
|
|
692
|
+
SHOW_NUMERIC_KEYS = %i[gte gt lte lt].freeze
|
|
693
|
+
|
|
694
|
+
# Every predicate key a reactive_show / reactive_show_targets binding may
|
|
695
|
+
# declare — the literal trio plus the numeric quartet. Exactly one of
|
|
696
|
+
# these decides a single binding; a compound term declares one too.
|
|
697
|
+
SHOW_PREDICATE_KEYS = (SHOW_LITERAL_KEYS + SHOW_NUMERIC_KEYS).freeze
|
|
698
|
+
|
|
699
|
+
# The compound connectives (issue #176 part A): fold a list of per-field
|
|
700
|
+
# literal/numeric terms with ONE fixed keyword. Not an expression surface
|
|
701
|
+
# — the connective is one of two fixed keywords, each term is the same
|
|
702
|
+
# declared predicate vocabulary. Mutually exclusive with a single field
|
|
703
|
+
# and with each other (enforced loudly at render).
|
|
704
|
+
SHOW_CONNECTIVE_KEYS = %i[all any].freeze
|
|
667
705
|
|
|
668
706
|
# The declared optimistic-hint class ops (issue #98): the cosmetic class
|
|
669
707
|
# vocabulary the client applies instantly and reverts on failure. Enforced
|
|
@@ -712,20 +750,23 @@ module Phlex
|
|
|
712
750
|
end
|
|
713
751
|
end
|
|
714
752
|
|
|
715
|
-
# Validate ONE declared
|
|
753
|
+
# Validate ONE declared show predicate (issues #161/#164/#176) and
|
|
716
754
|
# return its wire form — { "equals" => "v" } / { "not" => "v" } /
|
|
717
|
-
# { "in" => ["a", …] } (a
|
|
718
|
-
#
|
|
719
|
-
#
|
|
720
|
-
#
|
|
755
|
+
# { "in" => ["a", …] } / { "gte" => 10 } (a numeric RHS stays a real
|
|
756
|
+
# Number so the wire carries a JSON number, not a string). reactive_show
|
|
757
|
+
# JSON-encodes an in: array into its own attr; the reactive_show_targets
|
|
758
|
+
# map and a compound term embed the predicate directly. Shared by every
|
|
759
|
+
# helper so the vocabulary and the loud validation can never drift.
|
|
760
|
+
# `context` names the call site in the error.
|
|
721
761
|
def normalize_show_predicate(predicates, context)
|
|
722
762
|
unless predicates.size == 1 && SHOW_PREDICATE_KEYS.include?(predicates.keys.first)
|
|
723
763
|
raise ArgumentError,
|
|
724
|
-
"#{context} needs exactly one predicate — equals:, not:,
|
|
725
|
-
"got #{predicates.keys.inspect}"
|
|
764
|
+
"#{context} needs exactly one predicate — equals:, not:, in:, or " \
|
|
765
|
+
"gte:/gt:/lte:/lt: — got #{predicates.keys.inspect}"
|
|
726
766
|
end
|
|
727
767
|
|
|
728
768
|
key, value = predicates.first
|
|
769
|
+
return normalize_show_numeric(key, value, context) if SHOW_NUMERIC_KEYS.include?(key)
|
|
729
770
|
return { key.to_s => value.to_s } unless key == :in
|
|
730
771
|
|
|
731
772
|
list = Array(value).map(&:to_s)
|
|
@@ -734,6 +775,107 @@ module Phlex
|
|
|
734
775
|
{ "in" => list }
|
|
735
776
|
end
|
|
736
777
|
|
|
778
|
+
# Validate a numeric threshold predicate (issue #176 part B): the RHS
|
|
779
|
+
# must be an actual Numeric (a String "10" is a typo caught here, not a
|
|
780
|
+
# silent NaN in the browser). It rides the wire as a JSON number so the
|
|
781
|
+
# client compares Number(value) against it directly.
|
|
782
|
+
def normalize_show_numeric(key, value, context)
|
|
783
|
+
unless value.is_a?(Numeric)
|
|
784
|
+
raise ArgumentError,
|
|
785
|
+
"#{context} #{key}: needs a number (an ordered comparison against a literal), " \
|
|
786
|
+
"got #{value.inspect}"
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
{ key.to_s => value }
|
|
790
|
+
end
|
|
791
|
+
|
|
792
|
+
# The single-field reactive_show form (issue #161, extended for numeric
|
|
793
|
+
# thresholds in #176): one owned field + one predicate → flat
|
|
794
|
+
# data-reactive-show-* attrs. An in: list JSON-encodes into its own attr;
|
|
795
|
+
# a numeric threshold or a literal stringifies into the flat attr and the
|
|
796
|
+
# client Number()-coerces the numeric case back on read.
|
|
797
|
+
def reactive_show_single(field, options)
|
|
798
|
+
if field.nil?
|
|
799
|
+
raise ArgumentError,
|
|
800
|
+
"reactive_show needs a field (reactive_show(:mode, equals: …)) or a compound " \
|
|
801
|
+
"connective (reactive_show(all: [...]) / any:) — got neither"
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
predicates = options.slice(*SHOW_PREDICATE_KEYS)
|
|
805
|
+
attrs = options.except(*SHOW_PREDICATE_KEYS)
|
|
806
|
+
unless predicates.size == 1
|
|
807
|
+
raise ArgumentError,
|
|
808
|
+
"reactive_show(#{field.inspect}) needs exactly one predicate — equals:, not:, in:, " \
|
|
809
|
+
"or gte:/gt:/lte:/lt: — got #{predicates.keys.inspect}"
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
key, value = normalize_show_predicate(predicates, "reactive_show(#{field.inspect})").first
|
|
813
|
+
data = { reactive_show_field: field.to_s }
|
|
814
|
+
# in: → JSON array; a numeric threshold or a literal → the value as-is.
|
|
815
|
+
# Phlex stringifies it into the flat attr; the client re-reads via
|
|
816
|
+
# getAttribute (always a string) and Number()-coerces the numeric case.
|
|
817
|
+
data[:"reactive_show_#{key}"] = key == "in" ? value.to_json : value.to_s
|
|
818
|
+
|
|
819
|
+
mix({ data: }, attrs)
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
# The compound reactive_show form (issue #176 part A): `all:`/`any:` fold
|
|
823
|
+
# a list of per-field terms with one fixed connective, serialized as one
|
|
824
|
+
# JSON attr (data-reactive-show). A field alongside a connective, or two
|
|
825
|
+
# connectives, is a contradiction — raise. Each term is validated through
|
|
826
|
+
# the SAME predicate normalizer, so a term's vocabulary never drifts from
|
|
827
|
+
# the single-field form.
|
|
828
|
+
def reactive_show_compound(field, connectives, options)
|
|
829
|
+
unless field.nil?
|
|
830
|
+
raise ArgumentError,
|
|
831
|
+
"reactive_show got a field AND all:/any: — the compound and single-field forms are " \
|
|
832
|
+
"mutually exclusive; use one flat binding OR a compound list, not both"
|
|
833
|
+
end
|
|
834
|
+
unless connectives.size == 1
|
|
835
|
+
raise ArgumentError,
|
|
836
|
+
"reactive_show needs exactly one of all:/any: (one fixed connective), " \
|
|
837
|
+
"got #{connectives.keys.inspect}"
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
connective, terms = connectives.first
|
|
841
|
+
# A top-level predicate alongside a connective (reactive_show(all: [...],
|
|
842
|
+
# equals: "x")) is a misuse — predicates belong INSIDE terms. Catch it
|
|
843
|
+
# loudly at render rather than leaking `equals="x"` as a bogus HTML attr
|
|
844
|
+
# (the mix at the tail treats every leftover kwarg as a literal
|
|
845
|
+
# attribute). Same default-deny posture as the single-field path.
|
|
846
|
+
if (stray = options.slice(*SHOW_PREDICATE_KEYS)).any?
|
|
847
|
+
raise ArgumentError,
|
|
848
|
+
"reactive_show #{connective}: got a top-level predicate #{stray.keys.inspect} — " \
|
|
849
|
+
"predicates belong INSIDE each term ({ field: …, #{stray.keys.first}: … }), not beside the connective"
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
attrs = options.except(*SHOW_CONNECTIVE_KEYS)
|
|
853
|
+
unless terms.is_a?(Array) && terms.any?
|
|
854
|
+
raise ArgumentError,
|
|
855
|
+
"reactive_show #{connective}: needs at least one term " \
|
|
856
|
+
"({ field: …, equals:/not:/in:/gte:/… }), got #{terms.inspect}"
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
normalized = terms.map { normalize_show_term(connective, it) }
|
|
860
|
+
mix({ data: { reactive_show: { connective.to_s => normalized }.to_json } }, attrs)
|
|
861
|
+
end
|
|
862
|
+
|
|
863
|
+
# Normalize + validate ONE compound term (issue #176 part A): a Hash with
|
|
864
|
+
# a :field and exactly one predicate. The predicate goes through the
|
|
865
|
+
# shared normalizer; the field is stringified into the term. Returns
|
|
866
|
+
# { "field" => name, <predicate> } so the wire is uniform with the
|
|
867
|
+
# reactive_show_targets embedded-predicate shape.
|
|
868
|
+
def normalize_show_term(connective, term)
|
|
869
|
+
context = "reactive_show #{connective}: term"
|
|
870
|
+
unless term.is_a?(Hash) && term.key?(:field)
|
|
871
|
+
raise ArgumentError, "#{context} — each term needs a field (got #{term.inspect})"
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
field = term[:field]
|
|
875
|
+
predicate = normalize_show_predicate(term.except(:field), "#{context} (field #{field.inspect})")
|
|
876
|
+
{ "field" => field.to_s }.merge(predicate)
|
|
877
|
+
end
|
|
878
|
+
|
|
737
879
|
# True when the hint declares checked: :keep — the click-bound
|
|
738
880
|
# checkbox/radio case that must SKIP the forced type="button" so the native
|
|
739
881
|
# control (and its native flip) survives (issue #98). Accepts symbol or
|