Skip to content

Commit b2e959e

Browse files
authored
Merge pull request #213 from skryukov/deep-merge
Deep merge props
2 parents aad1e9d + 7d6e092 commit b2e959e

File tree

10 files changed

+215
-58
lines changed

10 files changed

+215
-58
lines changed

.rubocop_todo.yml

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This configuration was generated by
22
# `rubocop --auto-gen-config --exclude-limit 10000`
3-
# on 2025-04-01 18:59:20 UTC using RuboCop version 1.75.1.
3+
# on 2025-04-10 07:56:29 UTC using RuboCop version 1.75.2.
44
# The point is for the user to remove these configuration records
55
# one by one as the offenses are removed from the code base.
66
# Note that changes in the inspected code, or installation of new
@@ -51,14 +51,13 @@ Layout/ExtraSpacing:
5151
Layout/FirstHashElementIndentation:
5252
EnforcedStyle: consistent
5353

54-
# Offense count: 6
54+
# Offense count: 5
5555
# This cop supports safe autocorrection (--autocorrect).
5656
# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces.
5757
# SupportedStyles: space, no_space
5858
# SupportedStylesForEmptyBraces: space, no_space
5959
Layout/SpaceBeforeBlockBraces:
6060
Exclude:
61-
- 'spec/dummy/app/controllers/inertia_render_test_controller.rb'
6261
- 'spec/inertia/error_sharing_spec.rb'
6362
- 'spec/inertia/request_spec.rb'
6463

@@ -106,13 +105,12 @@ Layout/TrailingEmptyLines:
106105
- 'lib/tasks/inertia_rails.rake'
107106
- 'spec/inertia/rails_mimic_spec.rb'
108107

109-
# Offense count: 10
108+
# Offense count: 9
110109
# This cop supports safe autocorrection (--autocorrect).
111110
# Configuration parameters: AllowInHeredoc.
112111
Layout/TrailingWhitespace:
113112
Exclude:
114113
- 'lib/inertia_rails/rspec.rb'
115-
- 'spec/dummy/app/controllers/inertia_render_test_controller.rb'
116114
- 'spec/dummy/config/environments/test.rb'
117115

118116
# Offense count: 1
@@ -227,7 +225,7 @@ Style/ExpandPathArguments:
227225
Exclude:
228226
- 'spec/rails_helper.rb'
229227

230-
# Offense count: 65
228+
# Offense count: 63
231229
# This cop supports unsafe autocorrection (--autocorrect-all).
232230
# Configuration parameters: EnforcedStyle.
233231
# SupportedStyles: always, always_true, never
@@ -239,7 +237,6 @@ Style/FrozenStringLiteralComment:
239237
- 'lib/inertia_rails/controller.rb'
240238
- 'lib/inertia_rails/engine.rb'
241239
- 'lib/inertia_rails/helper.rb'
242-
- 'lib/inertia_rails/inertia_rails.rb'
243240
- 'lib/inertia_rails/rspec.rb'
244241
- 'lib/inertia_rails/version.rb'
245242
- 'lib/patches/better_errors.rb'
@@ -259,7 +256,6 @@ Style/FrozenStringLiteralComment:
259256
- 'spec/dummy/app/controllers/inertia_merge_shared_controller.rb'
260257
- 'spec/dummy/app/controllers/inertia_multithreaded_share_controller.rb'
261258
- 'spec/dummy/app/controllers/inertia_rails_mimic_controller.rb'
262-
- 'spec/dummy/app/controllers/inertia_render_test_controller.rb'
263259
- 'spec/dummy/app/controllers/inertia_responders_test_controller.rb'
264260
- 'spec/dummy/app/controllers/inertia_session_continuity_test_controller.rb'
265261
- 'spec/dummy/app/controllers/inertia_share_test_controller.rb'
@@ -337,14 +333,13 @@ Style/IfUnlessModifierOfIfUnless:
337333
Exclude:
338334
- 'lib/inertia_rails/controller.rb'
339335

340-
# Offense count: 3
336+
# Offense count: 2
341337
# This cop supports safe autocorrection (--autocorrect).
342338
# Configuration parameters: EnforcedStyle.
343339
# SupportedStyles: line_count_dependent, lambda, literal
344340
Style/Lambda:
345341
Exclude:
346342
- 'spec/dummy/app/controllers/inertia_lambda_shared_props_controller.rb'
347-
- 'spec/dummy/app/controllers/inertia_render_test_controller.rb'
348343
- 'spec/dummy/app/controllers/transformed_inertia_rails_mimic_controller.rb'
349344

350345
# Offense count: 1
@@ -456,7 +451,7 @@ Style/TrailingCommaInArguments:
456451
- 'spec/dummy/app/controllers/inertia_config_test_controller.rb'
457452
- 'spec/dummy/app/controllers/inertia_rails_mimic_controller.rb'
458453

459-
# Offense count: 22
454+
# Offense count: 12
460455
# This cop supports safe autocorrection (--autocorrect).
461456
# Configuration parameters: EnforcedStyleForMultiline.
462457
# SupportedStylesForMultiline: comma, consistent_comma, diff_comma, no_comma
@@ -466,13 +461,12 @@ Style/TrailingCommaInHashLiteral:
466461
- 'spec/dummy/app/controllers/inertia_lambda_shared_props_controller.rb'
467462
- 'spec/dummy/app/controllers/inertia_merge_instance_props_controller.rb'
468463
- 'spec/dummy/app/controllers/inertia_merge_shared_controller.rb'
469-
- 'spec/dummy/app/controllers/inertia_render_test_controller.rb'
470464
- 'spec/dummy/config/environments/development.rb'
471465
- 'spec/dummy/config/environments/test.rb'
472466
- 'spec/inertia/response_spec.rb'
473467
- 'spec/inertia/rspec_helper_spec.rb'
474468

475-
# Offense count: 24
469+
# Offense count: 19
476470
# This cop supports safe autocorrection (--autocorrect).
477471
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
478472
# URISchemes: http, https

docs/guide/merging-props.md

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,37 @@ By default, Inertia overwrites props with the same name when reloading a page. H
44

55
## Server side
66

7-
To specify that a prop should be merged, you can use the `merge` method on the prop value.
7+
> `deep_merge` requires `@inertiajs/core` v2.0.8 or higher, and `inertia_rails` v3.8.0 or higher.
8+
9+
To specify that a prop should be merged, use the `merge` or `deep_merge` method on the prop's value.
10+
11+
Use `merge` for merging simple arrays, and `deep_merge` for handling nested objects that include arrays or complex structures, such as pagination objects.
812

913
```ruby
1014
class UsersController < ApplicationController
1115
include Pagy::Backend
1216

1317
def index
14-
_pagy, records = pagy(User.all)
15-
16-
render inertia: 'Users/Index', props: {
17-
results: InertiaRails.merge { records },
18+
pagy, records = pagy(User.all)
19+
20+
render inertia: {
21+
# simple array:
22+
users: InertiaRails.merge { records.as_json(...) },
23+
# pagination object:
24+
data: InertiaRails.deep_merge {
25+
{
26+
records: records.as_json(...),
27+
pagy: pagy_metadata(pagy)
28+
}
29+
}
1830
}
1931
end
2032
end
2133
```
2234

23-
On the client side, Inertia detects that this prop should be merged. If the prop returns an array, it will append the response to the current prop value. If it's an object, it will merge the response with the current prop value.
35+
On the client side, Inertia detects that this prop should be merged. If the prop returns an array, it will append the response to the current prop value. If it's an object, it will merge the response with the current prop value. If you have opted to `deepMerge`, Inertia ensures a deep merge of the entire structure.
36+
37+
**Of note:** During the merging process, if the value is an array, the incoming items will be _appended_ to the existing array, not merged by index.
2438

2539
You can also combine [deferred props](/guide/deferred-props) with mergeable props to defer the loading of the prop and ultimately mark it as mergeable once it's loaded.
2640

@@ -29,8 +43,18 @@ class UsersController < ApplicationController
2943
include Pagy::Backend
3044

3145
def index
32-
render inertia: 'Users/Index', props: {
33-
results: InertiaRails.defer(merge: true) { pagy(User.all)[1] },
46+
pagy, records = pagy(User.all)
47+
48+
render inertia: {
49+
# simple array:
50+
users: InertiaRails.defer(merge: true) { records.as_json(...) },
51+
# pagination object:
52+
data: InertiaRails.defer(deep_merge: true) {
53+
{
54+
records: records.as_json(...),
55+
pagy: pagy_metadata(pagy)
56+
}
57+
}
3458
}
3559
end
3660
end
@@ -48,23 +72,23 @@ The `reset` request option accepts an array of the props keys you would like to
4872
```js
4973
import { router } from '@inertiajs/vue3'
5074

51-
router.reload({ reset: ['results'] })
75+
router.reload({ reset: ['users'] })
5276
```
5377

5478
== React
5579

5680
```js
5781
import { router } from '@inertiajs/react'
5882

59-
router.reload({ reset: ['results'] })
83+
router.reload({ reset: ['users'] })
6084
```
6185

6286
== Svelte 4|Svelte 5
6387

6488
```js
6589
import { router } from '@inertiajs/svelte'
6690

67-
router.reload({ reset: ['results'] })
91+
router.reload({ reset: ['users'] })
6892
```
6993

7094
:::

lib/inertia_rails/defer_prop.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,22 @@ class DeferProp < IgnoreOnFirstLoadProp
66

77
attr_reader :group
88

9-
def initialize(group: nil, merge: nil, &block)
9+
def initialize(group: nil, merge: nil, deep_merge: nil, &block)
10+
raise ArgumentError, 'Cannot set both `deep_merge` and `merge` to true' if deep_merge && merge
11+
1012
super(&block)
1113

1214
@group = group || DEFAULT_GROUP
13-
@merge = merge
14-
@block = block
15+
@merge = merge || deep_merge
16+
@deep_merge = deep_merge
1517
end
1618

1719
def merge?
1820
@merge
1921
end
22+
23+
def deep_merge?
24+
@deep_merge
25+
end
2026
end
2127
end

lib/inertia_rails/inertia_rails.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# frozen_string_literal: true
2+
13
require 'inertia_rails/base_prop'
24
require 'inertia_rails/ignore_on_first_load_prop'
35
require 'inertia_rails/always_prop'
@@ -35,8 +37,12 @@ def merge(&block)
3537
MergeProp.new(&block)
3638
end
3739

38-
def defer(group: nil, merge: nil, &block)
39-
DeferProp.new(group: group, merge: merge, &block)
40+
def deep_merge(&block)
41+
MergeProp.new(deep_merge: true, &block)
42+
end
43+
44+
def defer(group: nil, merge: nil, deep_merge: nil, &block)
45+
DeferProp.new(group: group, merge: merge, deep_merge: deep_merge, &block)
4046
end
4147
end
4248
end

lib/inertia_rails/merge_prop.rb

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22

33
module InertiaRails
44
class MergeProp < BaseProp
5-
def initialize(*)
6-
super
7-
@merge = true
5+
def initialize(deep_merge: false, &block)
6+
super(&block)
7+
@deep_merge = deep_merge
88
end
99

1010
def merge?
11-
@merge
11+
true
12+
end
13+
14+
def deep_merge?
15+
@deep_merge
1216
end
1317
end
1418
end

lib/inertia_rails/renderer.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,14 @@ def page
106106
deferred_props = deferred_props_keys
107107
default_page[:deferredProps] = deferred_props if deferred_props.present?
108108

109-
merge_props = merge_props_keys
109+
all_merge_props = merge_props_keys
110+
111+
deep_merge_props, merge_props = all_merge_props.partition do |key|
112+
@props[key].deep_merge?
113+
end
114+
110115
default_page[:mergeProps] = merge_props if merge_props.present?
116+
default_page[:deepMergeProps] = deep_merge_props if deep_merge_props.present?
111117

112118
default_page
113119
end

spec/dummy/app/controllers/inertia_render_test_controller.rb

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
# frozen_string_literal: true
2+
13
class InertiaRenderTestController < ApplicationController
2-
34
def props
45
render inertia: 'TestComponent', props: {
56
name: 'Brandon',
6-
sport: -> { 'hockey' }
7+
sport: -> { 'hockey' },
78
}
89
end
910

@@ -18,9 +19,9 @@ def except_props
1819
end,
1920
nested: {
2021
first: 'first nested param',
21-
second: 'second nested param'
22+
second: 'second nested param',
2223
},
23-
always: InertiaRails.always { 'always prop' }
24+
always: InertiaRails.always { 'always prop' },
2425
}
2526
end
2627

@@ -36,10 +37,10 @@ def deeply_nested_props
3637
nested: {
3738
first: 'first nested param',
3839
second: 'second nested param',
39-
evaluated: -> do
40+
evaluated: lambda do
4041
{
4142
first: 'first evaluated nested param',
42-
second: 'second evaluated nested param'
43+
second: 'second evaluated nested param',
4344
}
4445
end,
4546
deeply_nested: {
@@ -48,10 +49,10 @@ def deeply_nested_props
4849
what_about_nil: nil,
4950
what_about_empty_hash: {},
5051
deeply_nested_always: InertiaRails.always { 'deeply nested always prop' },
51-
deeply_nested_lazy: InertiaRails.lazy { 'deeply nested lazy prop' }
52-
}
52+
deeply_nested_lazy: InertiaRails.lazy { 'deeply nested lazy prop' },
53+
},
5354
},
54-
always: InertiaRails.always { 'always prop' }
55+
always: InertiaRails.always { 'always prop' },
5556
}
5657
end
5758

@@ -79,7 +80,7 @@ def lazy_props
7980
level: InertiaRails.lazy do
8081
'worse than he believes'
8182
end,
82-
grit: InertiaRails.lazy(->{ 'intense' })
83+
grit: InertiaRails.lazy(-> { 'intense' }),
8384
}
8485
end
8586

@@ -98,15 +99,17 @@ def always_props
9899
optional: InertiaRails.optional do
99100
'optional prop'
100101
end,
101-
another_optional: InertiaRails.optional { 'another optional prop' }
102+
another_optional: InertiaRails.optional { 'another optional prop' },
102103
}
103104
end
104105

105106
def merge_props
106107
render inertia: 'TestComponent', props: {
107108
merge: InertiaRails.merge { 'merge prop' },
109+
deep_merge: InertiaRails.deep_merge { { deep: 'merge prop' } },
108110
regular: 'regular prop',
109111
deferred_merge: InertiaRails.defer(merge: true) { 'deferred and merge prop' },
112+
deferred_deep_merge: InertiaRails.defer(deep_merge: true) { { deep: 'deferred and merge prop' } },
110113
deferred: InertiaRails.defer { 'deferred' },
111114
}
112115
end
@@ -118,7 +121,7 @@ def deferred_props
118121
level: InertiaRails.defer do
119122
'worse than he believes'
120123
end,
121-
grit: InertiaRails.defer { 'intense' }
124+
grit: InertiaRails.defer { 'intense' },
122125
}
123126
end
124127
end

0 commit comments

Comments
 (0)