phlex-reactive 0.11.5 → 0.11.7
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 +51 -0
- data/README.md +79 -3
- data/app/javascript/phlex/reactive/reactive_controller.js +282 -6
- 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 +133 -17
- data/lib/phlex/reactive/version.rb +1 -1
- metadata +1 -1
|
@@ -793,8 +793,34 @@ module Phlex
|
|
|
793
793
|
# The container cloned rows land in — one per association, inside the
|
|
794
794
|
# root. Server-rendered rows (an edit form's persisted children) render
|
|
795
795
|
# inside it too.
|
|
796
|
-
|
|
797
|
-
|
|
796
|
+
#
|
|
797
|
+
# `as: :json` (issue #208) switches the SUBMIT wire from Rails'
|
|
798
|
+
# accepts_nested_attributes_for names to ONE hidden JSON field — for an
|
|
799
|
+
# app whose controller already parses a serialized JSON param
|
|
800
|
+
# (`JSON.parse(params[:order][:todos])`) instead of nested attributes.
|
|
801
|
+
# The container keeps its plain marker (nestedAdd/Remove still key on
|
|
802
|
+
# it), and gains data-reactive-nested-json plus a scope-aware selector
|
|
803
|
+
# naming the hidden field the client mirrors the rows into on every
|
|
804
|
+
# add/remove/input. The default (:attributes) is unchanged — the plain
|
|
805
|
+
# accepts_nested_attributes_for wire.
|
|
806
|
+
def reactive_nested_list(association, as: :attributes)
|
|
807
|
+
name = nested_identifier!(:reactive_nested_list, :association, association)
|
|
808
|
+
data = { reactive_nested_list: name }
|
|
809
|
+
return { data: } if as == :attributes
|
|
810
|
+
|
|
811
|
+
unless as == :json
|
|
812
|
+
raise ArgumentError,
|
|
813
|
+
"reactive_nested_list(as:) takes :attributes (the default, Rails nested-attribute names) " \
|
|
814
|
+
"or :json (serialize the rows into one hidden JSON field), got #{as.inspect}"
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
# JSON mode: mark the container and name the hidden field the client
|
|
818
|
+
# keeps in sync — a scope-aware [name="…"] selector, the same
|
|
819
|
+
# convention reactive_tags/reactive_filter use so the field resolves
|
|
820
|
+
# under reactive_scope too.
|
|
821
|
+
data[:reactive_nested_json] = name
|
|
822
|
+
data[:reactive_nested_json_field] = %([name="#{scoped_field_name(association)}"])
|
|
823
|
+
{ data: }
|
|
798
824
|
end
|
|
799
825
|
|
|
800
826
|
# The <template> holding ONE row's markup (server-owned, inert until
|
|
@@ -814,14 +840,58 @@ module Phlex
|
|
|
814
840
|
# The add-a-row trigger — CLIENT-ONLY (no dispatch descriptor, no
|
|
815
841
|
# POST). Forced type="button": a bare button inside the surrounding
|
|
816
842
|
# real <form> would submit it.
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
843
|
+
#
|
|
844
|
+
# FILL-THEN-ADD (issue #208 Scenario A). By default add clones the
|
|
845
|
+
# template and focuses the new row's first field — INLINE-EDIT (you
|
|
846
|
+
# type INTO the row). But a common form shape puts the add controls
|
|
847
|
+
# OUTSIDE the row (a preset <select>, a typeahead, plain inputs) and
|
|
848
|
+
# "Add" SNAPSHOTS those values into a new row, then clears them for the
|
|
849
|
+
# next entry. `from:` expresses that: a map of ROW FIELD name => SOURCE
|
|
850
|
+
# CONTROL selector. On click the client fills each cloned-row field
|
|
851
|
+
# from its source's current value (matching the field by the trailing
|
|
852
|
+
# bracket segment of its name — the SAME key inference JSON mode uses,
|
|
853
|
+
# so the two agree), keeps focus on the sources, and (with `clear:
|
|
854
|
+
# true`) resets the sources. It composes with BOTH wire modes: the
|
|
855
|
+
# seeded values ride the renumbered `…_attributes[i][field]` names on
|
|
856
|
+
# submit (:attributes), and the end-of-add JSON sync serializes them
|
|
857
|
+
# (as: :json) with no extra wiring.
|
|
858
|
+
#
|
|
859
|
+
# a(**reactive_nested_add(:items,
|
|
860
|
+
# from: { name: "#item-name", quantity: "#item-qty" }, clear: true))
|
|
861
|
+
#
|
|
862
|
+
# `from:` values are RAW CSS selectors (the escape-hatch posture of
|
|
863
|
+
# reactive_filter/reactive_tags) — the sources are author-owned markup,
|
|
864
|
+
# not reactive_field bindings — resolved root-scoped (#15 ownership).
|
|
865
|
+
def reactive_nested_add(association, from: nil, clear: false)
|
|
866
|
+
data = {
|
|
867
|
+
action: "click->reactive#nestedAdd",
|
|
868
|
+
reactive_association_param: nested_identifier!(:reactive_nested_add, :association, association)
|
|
824
869
|
}
|
|
870
|
+
unless from.nil?
|
|
871
|
+
data[:reactive_nested_from_param] = nested_from_param!(from)
|
|
872
|
+
# STRING "true", not boolean: a valueless boolean attr reads "" (falsy)
|
|
873
|
+
# client-side — the on()/tags precedent.
|
|
874
|
+
data[:reactive_nested_clear_param] = "true" if clear
|
|
875
|
+
end
|
|
876
|
+
{ type: "button", data: }
|
|
877
|
+
end
|
|
878
|
+
|
|
879
|
+
# Validate + compile a fill-then-add `from:` map into its JSON wire
|
|
880
|
+
# (issue #208). Each key is a ROW FIELD name (plain identifier, like the
|
|
881
|
+
# association); each value is a SOURCE CONTROL selector (non-blank). An
|
|
882
|
+
# empty map is a dead binding — fail at render (the reactive_show_targets
|
|
883
|
+
# posture), never a silent no-op.
|
|
884
|
+
def nested_from_param!(from)
|
|
885
|
+
unless from.is_a?(Hash) && from.any?
|
|
886
|
+
raise ArgumentError,
|
|
887
|
+
"reactive_nested_add from: needs at least one row-field => source-selector pair " \
|
|
888
|
+
"(e.g. from: { quantity: \"#item-qty\" }), got #{from.inspect}"
|
|
889
|
+
end
|
|
890
|
+
|
|
891
|
+
from.to_h do |field, selector|
|
|
892
|
+
[nested_identifier!(:reactive_nested_add, :field, field),
|
|
893
|
+
filter_selector!(:reactive_nested_add_from, selector)]
|
|
894
|
+
end.to_json
|
|
825
895
|
end
|
|
826
896
|
|
|
827
897
|
# A row's remove trigger — client-only. Draft rows leave the DOM;
|
|
@@ -864,6 +934,22 @@ module Phlex
|
|
|
864
934
|
#
|
|
865
935
|
# reactive_show_targets(mode: { "#advanced-tab" => "advanced" },
|
|
866
936
|
# kind: { "#premium-note" => %w[gold platinum] })
|
|
937
|
+
#
|
|
938
|
+
# MULTI-FIELD targets (issue #209): a "#id" KEY takes a full
|
|
939
|
+
# if:/if_any:/unless: conditions Hash — the SAME language reactive_show
|
|
940
|
+
# speaks, so a cross-root target can finally read a COMBINATION of
|
|
941
|
+
# owned fields (the last case forcing a bespoke JS listener):
|
|
942
|
+
#
|
|
943
|
+
# reactive_show_targets("#trade-warning" => {
|
|
944
|
+
# if: { type: "trade", price: ..0 } # type == "trade" AND price <= 0
|
|
945
|
+
# })
|
|
946
|
+
#
|
|
947
|
+
# Target-keyed and field-keyed entries mix in the ONE call (a "#" key is
|
|
948
|
+
# unambiguous — a field name may never start with "#"). The client folds
|
|
949
|
+
# the target's DNF payload with the same per-term field reads as an
|
|
950
|
+
# in-root reactive_show: every referenced field must be OWNED by this
|
|
951
|
+
# root (a missing owned field reads as blank, fail-closed); a target
|
|
952
|
+
# whose fields are ALL unowned is left alone, like the single-field skip.
|
|
867
953
|
def reactive_show_targets(field, targets = nil)
|
|
868
954
|
field_maps = targets.nil? ? field : { field => targets }
|
|
869
955
|
unless field_maps.is_a?(Hash) && field_maps.any?
|
|
@@ -873,16 +959,12 @@ module Phlex
|
|
|
873
959
|
end
|
|
874
960
|
|
|
875
961
|
normalized = field_maps.to_h do |name, map|
|
|
876
|
-
# Catch the forgotten-field-name misuse — reactive_show_targets(
|
|
877
|
-
# "#id" => {…}) — before the per-target validation turns it into a
|
|
878
|
-
# baffling "predicate" error.
|
|
879
962
|
if name.to_s.start_with?("#")
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
963
|
+
# Target-keyed conditions (issue #209): "#id" => { if:/if_any:/unless: }.
|
|
964
|
+
[name.to_s, normalize_show_target_conditions(name.to_s, map)]
|
|
965
|
+
else
|
|
966
|
+
[name.to_s, normalize_show_target_map(name, map)]
|
|
883
967
|
end
|
|
884
|
-
|
|
885
|
-
[name.to_s, normalize_show_target_map(name, map)]
|
|
886
968
|
end
|
|
887
969
|
|
|
888
970
|
{ data: { reactive_show_targets: normalized.to_json } }
|
|
@@ -1122,6 +1204,40 @@ module Phlex
|
|
|
1122
1204
|
end
|
|
1123
1205
|
end
|
|
1124
1206
|
|
|
1207
|
+
# Normalize + validate ONE target-keyed entry (issue #209): the "#id"
|
|
1208
|
+
# key is a single id selector (the same declare-time guard as
|
|
1209
|
+
# field-keyed targets), the value a full if:/if_any:/unless: conditions
|
|
1210
|
+
# Hash compiled by ShowConditions into the SAME { "any" => groups } DNF
|
|
1211
|
+
# payload reactive_show emits. Anything else — a bare value, unknown
|
|
1212
|
+
# keys, empty conditions — raises a guided error at render (a dead
|
|
1213
|
+
# binding must never reach the browser).
|
|
1214
|
+
def normalize_show_target_conditions(selector, conditions)
|
|
1215
|
+
unless selector.match?(DSL::MIRROR_ID_SELECTOR)
|
|
1216
|
+
raise ArgumentError,
|
|
1217
|
+
"reactive_show_targets target #{selector.inspect} must be a single " \
|
|
1218
|
+
"ID selector (\"#id\") — cross-root visibility is id-allowlisted, like mirror: (#159)"
|
|
1219
|
+
end
|
|
1220
|
+
unless conditions.is_a?(Hash) && conditions.any?
|
|
1221
|
+
raise ArgumentError,
|
|
1222
|
+
"reactive_show_targets: a target key takes a conditions Hash — " \
|
|
1223
|
+
"reactive_show_targets(#{selector.inspect} => { if: { field: value, ... } }); " \
|
|
1224
|
+
"to key by field instead: reactive_show_targets(:field, #{selector.inspect} => value). " \
|
|
1225
|
+
"Got #{selector.inspect} => #{conditions.inspect}"
|
|
1226
|
+
end
|
|
1227
|
+
# Unknown keys are reported BEFORE the presence check so { bogus: 1 }
|
|
1228
|
+
# names its offender instead of the generic shape message (specific
|
|
1229
|
+
# beats generic). A non-empty hash surviving this subtraction holds
|
|
1230
|
+
# only condition keys, so no separate presence check remains.
|
|
1231
|
+
if (unknown = conditions.keys - SHOW_CONDITION_KEYS).any?
|
|
1232
|
+
raise ArgumentError,
|
|
1233
|
+
"reactive_show_targets #{selector.inspect}: unknown conditions key(s) " \
|
|
1234
|
+
"#{unknown.map(&:inspect).join(", ")} — a target's conditions Hash takes only " \
|
|
1235
|
+
"if:/if_any:/unless: (the reactive_show language)"
|
|
1236
|
+
end
|
|
1237
|
+
|
|
1238
|
+
{ "any" => Phlex::Reactive::ShowConditions.normalize(**conditions) }
|
|
1239
|
+
end
|
|
1240
|
+
|
|
1125
1241
|
# Reject the removed 0.9.5 reactive_show surface (issue #180 clean break)
|
|
1126
1242
|
# with a GUIDED error printing the if:/if_any:/unless: rewrite. A
|
|
1127
1243
|
# positional field, a predicate kwarg (equals:/not:/in:/gte:/…), or a
|