phlex-reactive 0.2.7 → 0.2.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 479ff63041e0a402939303aec9ea5fc8064dc0b01f5e63c8c58cfe989e5173b3
4
- data.tar.gz: b81323095162d6b0662ff18b91b0c8cb34a8e2000028353c695e255ffa13093f
3
+ metadata.gz: 9b321b7411a892ec63333f9d85d988f7cf63609601386a03593efd736e88fb42
4
+ data.tar.gz: 00d40c394774eb54af634d328251f646a3fae1ac9bc0413ccbc89dd863608001
5
5
  SHA512:
6
- metadata.gz: 80070c0e2868530f8499008a603e6f32c4815916fa34c4fccc050b3fb0772c0501ea27e569ee88ada9acb68d7f3eca5fc7333201fc3b9e4358780abd9b6e5d6a
7
- data.tar.gz: 6412dc6000663fd888193bf9a1b071b9d2f1984692cac474217ebd4955f302e963103bb4bd5ad5c3407b351dfe69a5c2a7079fcdd04b65d73dfed34092c60765
6
+ metadata.gz: bd7a2129ec09c6b33a9f623f201037f5bc6639f7b097c95f521a6bf7ba4999b8dc8e11f53f2b922f5e617d0dfd8e2b0185cc5676e20f040a9caf97637d2d9438
7
+ data.tar.gz: b6e611eab9a2ab3f3d3a6afff9c4780061ba07321ddb5db89970e076facc9d7a374f7809fe8bc38958040c34303a42484eefe25109495bdfb2b6f894a597ae3e
data/CHANGELOG.md CHANGED
@@ -31,6 +31,18 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
31
31
 
32
32
  ### Fixed
33
33
 
34
+ - **Model-scoped form fields feed a nested param (issue #21).** A Rails
35
+ `Form(model: @invoice)` posts flat bracketed keys (`invoice[date]`,
36
+ `invoice[status]`) because the client keeps each input's `name` verbatim. The
37
+ server did exact-key matching, so a nested schema (`params: { invoice: { date:
38
+ :string } }`) looked for the literal key `"invoice"`, never found it, and
39
+ dropped the whole param. Param normalization now expands bracket notation
40
+ before coercion — `invoice[date]` nests under `invoice`, and
41
+ `items_attributes[0][qty]` becomes the Rails index-hash form the array coercer
42
+ already understands. A nested schema matches a normal Rails form with zero
43
+ field renaming, which is what makes the issue #16 nested-param types useful for
44
+ real forms. Pre-nested objects, plain scalars, and non-string values (a
45
+ checkbox boolean) pass through unchanged.
34
46
  - **Nested reactive roots no longer leak fields (issue #15).** When a reactive
35
47
  component is rendered inside another (both are `data-controller="reactive"`
36
48
  roots), an action on the outer root previously swept *every* descendant named
data/README.md CHANGED
@@ -294,6 +294,18 @@ end
294
294
  Nested coercion recurses per field, drops undeclared nested keys, and accepts an
295
295
  array as either a JSON array or a Rails index hash (`{ "0" => …, "1" => … }`).
296
296
 
297
+ **Model-scoped form fields just work.** A standard Rails `Form(model: @invoice)`
298
+ names its inputs `invoice[date]`, `invoice[status]`, … and the client posts those
299
+ names verbatim. A nested schema matches them with zero field renaming — the
300
+ endpoint expands bracket notation before coercion, so `invoice[date]` nests under
301
+ `invoice` and `invoice_items_attributes[0][qty]` becomes the index-hash form
302
+ above:
303
+
304
+ ```ruby
305
+ action :save, params: {invoice: {date: :string, status: :string}}
306
+ # client posts { "invoice[date]": "…", "invoice[status]": "…" } → save(invoice: { date:, status: })
307
+ ```
308
+
297
309
  **Nested reactive components compose.** A reactive component rendered inside
298
310
  another is its own root — field collection stops at nested
299
311
  `data-controller="reactive"` roots, so an outer action collects only *its own*
@@ -201,12 +201,69 @@ module Phlex
201
201
  end
202
202
 
203
203
  # Unwrap ActionController::Parameters (or a plain Hash) to a string-keyed
204
- # Hash so coercion can index it uniformly.
204
+ # Hash so coercion can index it uniformly, then expand bracket notation so
205
+ # a model-scoped Rails form's flat keys nest (issue #21).
205
206
  def to_param_hash(value)
206
- return value.to_unsafe_h.stringify_keys if value.respond_to?(:to_unsafe_h)
207
- return value.stringify_keys if value.is_a?(Hash)
207
+ flat =
208
+ if value.respond_to?(:to_unsafe_h)
209
+ value.to_unsafe_h.stringify_keys
210
+ elsif value.is_a?(Hash)
211
+ value.stringify_keys
212
+ else
213
+ return {}
214
+ end
215
+
216
+ expand_bracket_keys(flat)
217
+ end
218
+
219
+ # The client's #collectFields keeps a form input's name verbatim, so a
220
+ # Rails Form(model: @invoice) posts FLAT bracketed keys like
221
+ # "invoice[date]". Coercion does exact-key matching, so without this a
222
+ # nested schema (params: { invoice: { date: … } }) never finds "invoice"
223
+ # and drops everything. Expand "invoice[date]" => "2026-01-02" into
224
+ # { "invoice" => { "date" => "2026-01-02" } } — and "items[0][qty]" into
225
+ # the Rails index-hash form coerce_array already understands — deep-merging
226
+ # so sibling keys (invoice[date], invoice[status]) coalesce. Keys WITHOUT
227
+ # brackets and already-nested values pass through untouched, so a
228
+ # pre-nested object (issue #16) and plain scalars still work. Value types
229
+ # (a checkbox boolean, an explicit array) are preserved verbatim — unlike a
230
+ # round-trip through parse_nested_query, which only handles strings.
231
+ def expand_bracket_keys(flat)
232
+ flat.each_with_object({}) do |(key, value), out|
233
+ deep_assign(out, bracket_path(key), value)
234
+ end
235
+ end
236
+
237
+ # "invoice[items_attributes][0][qty]" => ["invoice", "items_attributes",
238
+ # "0", "qty"]. A key with no brackets is a single-element path.
239
+ def bracket_path(key)
240
+ return [key] unless key.include?("[")
241
+
242
+ head, rest = key.split("[", 2)
243
+ [head, *rest.scan(/[^\[\]]+/)]
244
+ end
245
+
246
+ # Walk/create nested hashes along `path`, then merge `value` at the leaf so
247
+ # a bracket key and a sibling pre-nested object coalesce regardless of which
248
+ # arrives first ({ "invoice[date]" => …, invoice: { status: … } } keeps
249
+ # both). #merge_value deep-merges hash/hash collisions and otherwise lets
250
+ # the later value win (a bracket key colliding with a flat scalar).
251
+ def deep_assign(hash, path, value)
252
+ *parents, leaf = path
253
+ node = parents.reduce(hash) do |acc, segment|
254
+ acc[segment] = {} unless acc[segment].is_a?(Hash)
255
+ acc[segment]
256
+ end
257
+ node[leaf] = merge_value(node[leaf], value)
258
+ end
259
+
260
+ # Combine an existing leaf value with a new one. Two hashes deep-merge (so
261
+ # bracket-expanded fields and a pre-nested object for the same key both
262
+ # survive); any other collision takes the new value.
263
+ def merge_value(existing, value)
264
+ return value unless existing.is_a?(Hash) && value.is_a?(Hash)
208
265
 
209
- {}
266
+ existing.merge(value.stringify_keys) { |_k, old, new| merge_value(old, new) }
210
267
  end
211
268
 
212
269
  # Only components that opt into Reactive may be resolved. The signature
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Phlex
4
4
  module Reactive
5
- VERSION = "0.2.7"
5
+ VERSION = "0.2.8"
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.2.7
4
+ version: 0.2.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson