phlex-reactive 0.11.5 → 0.11.6

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.
@@ -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
- def reactive_nested_list(association)
797
- { data: { reactive_nested_list: nested_identifier!(:reactive_nested_list, :association, association) } }
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
@@ -864,6 +890,22 @@ module Phlex
864
890
  #
865
891
  # reactive_show_targets(mode: { "#advanced-tab" => "advanced" },
866
892
  # kind: { "#premium-note" => %w[gold platinum] })
893
+ #
894
+ # MULTI-FIELD targets (issue #209): a "#id" KEY takes a full
895
+ # if:/if_any:/unless: conditions Hash — the SAME language reactive_show
896
+ # speaks, so a cross-root target can finally read a COMBINATION of
897
+ # owned fields (the last case forcing a bespoke JS listener):
898
+ #
899
+ # reactive_show_targets("#trade-warning" => {
900
+ # if: { type: "trade", price: ..0 } # type == "trade" AND price <= 0
901
+ # })
902
+ #
903
+ # Target-keyed and field-keyed entries mix in the ONE call (a "#" key is
904
+ # unambiguous — a field name may never start with "#"). The client folds
905
+ # the target's DNF payload with the same per-term field reads as an
906
+ # in-root reactive_show: every referenced field must be OWNED by this
907
+ # root (a missing owned field reads as blank, fail-closed); a target
908
+ # whose fields are ALL unowned is left alone, like the single-field skip.
867
909
  def reactive_show_targets(field, targets = nil)
868
910
  field_maps = targets.nil? ? field : { field => targets }
869
911
  unless field_maps.is_a?(Hash) && field_maps.any?
@@ -873,16 +915,12 @@ module Phlex
873
915
  end
874
916
 
875
917
  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
918
  if name.to_s.start_with?("#")
880
- raise ArgumentError,
881
- "reactive_show_targets: #{name.inspect} looks like a target selector, not a field " \
882
- "name — call reactive_show_targets(:field, #{name.inspect} => { ... })"
919
+ # Target-keyed conditions (issue #209): "#id" => { if:/if_any:/unless: }.
920
+ [name.to_s, normalize_show_target_conditions(name.to_s, map)]
921
+ else
922
+ [name.to_s, normalize_show_target_map(name, map)]
883
923
  end
884
-
885
- [name.to_s, normalize_show_target_map(name, map)]
886
924
  end
887
925
 
888
926
  { data: { reactive_show_targets: normalized.to_json } }
@@ -1122,6 +1160,40 @@ module Phlex
1122
1160
  end
1123
1161
  end
1124
1162
 
1163
+ # Normalize + validate ONE target-keyed entry (issue #209): the "#id"
1164
+ # key is a single id selector (the same declare-time guard as
1165
+ # field-keyed targets), the value a full if:/if_any:/unless: conditions
1166
+ # Hash compiled by ShowConditions into the SAME { "any" => groups } DNF
1167
+ # payload reactive_show emits. Anything else — a bare value, unknown
1168
+ # keys, empty conditions — raises a guided error at render (a dead
1169
+ # binding must never reach the browser).
1170
+ def normalize_show_target_conditions(selector, conditions)
1171
+ unless selector.match?(DSL::MIRROR_ID_SELECTOR)
1172
+ raise ArgumentError,
1173
+ "reactive_show_targets target #{selector.inspect} must be a single " \
1174
+ "ID selector (\"#id\") — cross-root visibility is id-allowlisted, like mirror: (#159)"
1175
+ end
1176
+ unless conditions.is_a?(Hash) && conditions.any?
1177
+ raise ArgumentError,
1178
+ "reactive_show_targets: a target key takes a conditions Hash — " \
1179
+ "reactive_show_targets(#{selector.inspect} => { if: { field: value, ... } }); " \
1180
+ "to key by field instead: reactive_show_targets(:field, #{selector.inspect} => value). " \
1181
+ "Got #{selector.inspect} => #{conditions.inspect}"
1182
+ end
1183
+ # Unknown keys are reported BEFORE the presence check so { bogus: 1 }
1184
+ # names its offender instead of the generic shape message (specific
1185
+ # beats generic). A non-empty hash surviving this subtraction holds
1186
+ # only condition keys, so no separate presence check remains.
1187
+ if (unknown = conditions.keys - SHOW_CONDITION_KEYS).any?
1188
+ raise ArgumentError,
1189
+ "reactive_show_targets #{selector.inspect}: unknown conditions key(s) " \
1190
+ "#{unknown.map(&:inspect).join(", ")} — a target's conditions Hash takes only " \
1191
+ "if:/if_any:/unless: (the reactive_show language)"
1192
+ end
1193
+
1194
+ { "any" => Phlex::Reactive::ShowConditions.normalize(**conditions) }
1195
+ end
1196
+
1125
1197
  # Reject the removed 0.9.5 reactive_show surface (issue #180 clean break)
1126
1198
  # with a GUIDED error printing the if:/if_any:/unless: rewrite. A
1127
1199
  # positional field, a predicate kwarg (equals:/not:/in:/gte:/…), or a
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Phlex
4
4
  module Reactive
5
- VERSION = "0.11.5"
5
+ VERSION = "0.11.6"
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.5
4
+ version: 0.11.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson