Skip to content

Commit 658b257

Browse files
authored
Merge pull request #99 from inertiajs/deep-merge-shared-props
Deep merge props passed to render method with the shared data
2 parents 1ff089d + 5ce3149 commit 658b257

File tree

10 files changed

+201
-16
lines changed

10 files changed

+201
-16
lines changed

README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,76 @@ class EventsController < ApplicationController
101101
end
102102
```
103103

104+
#### Deep Merging Shared Data
105+
106+
By default, Inertia will shallow merge data defined in an action with the shared data. You might want a deep merge. Imagine using shared data to represent defaults you'll override sometimes.
107+
108+
```ruby
109+
class ApplicationController
110+
inertia_share do
111+
{ basketball_data: { points: 50, rebounds: 100 } }
112+
end
113+
end
114+
```
115+
116+
Let's say we want a particular action to change only part of that data structure. The renderer accepts a `deep_merge` option:
117+
118+
```ruby
119+
class CrazyScorersController < ApplicationController
120+
def index
121+
render inertia: 'CrazyScorersComponent',
122+
props: { basketball_data: { points: 100 } },
123+
deep_merge: true
124+
end
125+
end
126+
127+
# The renderer will send this to the frontend:
128+
{
129+
basketball_data: {
130+
points: 100,
131+
rebounds: 100,
132+
}
133+
}
134+
```
135+
136+
Deep merging can be set as the project wide default via the InertiaRails configuration:
137+
138+
```ruby
139+
# config/initializers/some_initializer.rb
140+
InertiaRails.configure do |config|
141+
config.deep_merge_shared_data = true
142+
end
143+
144+
```
145+
146+
If deep merging is enabled by default, it's possible to opt out within the action:
147+
148+
```ruby
149+
class CrazyScorersController < ApplicationController
150+
inertia_share do
151+
{
152+
basketball_data: {
153+
points: 50,
154+
rebounds: 10,
155+
}
156+
}
157+
end
158+
159+
def index
160+
render inertia: 'CrazyScorersComponent',
161+
props: { basketball_data: { points: 100 } },
162+
deep_merge: false
163+
end
164+
end
165+
166+
# Even if deep merging is set by default, since the renderer has `deep_merge: false`, it will send a shallow merge to the frontend:
167+
{
168+
basketball_data: {
169+
points: 100,
170+
}
171+
}
172+
```
173+
104174
### Lazy Props
105175

106176
On the front end, Inertia supports the concept of "partial reloads" where only the props requested are returned by the server. Sometimes, you may want to use this flow to avoid processing a particularly slow prop on the intial load. In this case, you can use Lazy props. Lazy props aren't evaluated unless they're specifically requested by name in a partial reload.
@@ -139,6 +209,8 @@ InertiaRails.configure do |config|
139209
# ssr specific options
140210
config.ssr_enabled = false
141211
config.ssr_url = 'http://localhost:13714'
212+
213+
config.deep_merge_shared_data = false
142214

143215
end
144216
```

lib/inertia_rails.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
method(:render),
1616
props: options[:props],
1717
view_data: options[:view_data],
18+
deep_merge: options[:deep_merge],
1819
).render
1920
end
2021

lib/inertia_rails/inertia_rails.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ def self.configure
1313

1414
# "Getters"
1515
def self.shared_data(controller)
16-
shared_plain_data.merge!(evaluated_blocks(controller, shared_blocks))
16+
shared_plain_data.
17+
merge!(evaluated_blocks(controller, shared_blocks)).
18+
with_indifferent_access
1719
end
1820

1921
def self.version
@@ -40,6 +42,10 @@ def self.html_headers
4042
self.threadsafe_html_headers || []
4143
end
4244

45+
def self.deep_merge_shared_data?
46+
Configuration.deep_merge_shared_data
47+
end
48+
4349
# "Setters"
4450
def self.share(**args)
4551
self.shared_plain_data = self.shared_plain_data.merge(args)
@@ -71,6 +77,7 @@ module Configuration
7177
mattr_accessor(:ssr_enabled) { false }
7278
mattr_accessor(:ssr_url) { 'http://localhost:13714' }
7379
mattr_accessor(:default_render) { false }
80+
mattr_accessor(:deep_merge_shared_data) { false }
7481

7582
def self.evaluated_version
7683
self.version.respond_to?(:call) ? self.version.call : self.version

lib/inertia_rails/renderer.rb

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ module InertiaRails
66
class Renderer
77
attr_reader :component, :view_data
88

9-
def initialize(component, controller, request, response, render_method, props:, view_data:)
9+
def initialize(component, controller, request, response, render_method, props: nil, view_data: nil, deep_merge: nil)
1010
@component = component.is_a?(TrueClass) ? "#{controller.controller_path}/#{controller.action_name}" : component
1111
@controller = controller
1212
@request = request
1313
@response = response
1414
@render_method = render_method
15-
@props = props || controller.inertia_view_assigns
15+
@props = props ? props.with_indifferent_access : controller.inertia_view_assigns.with_indifferent_access
1616
@view_data = view_data || {}
17+
@deep_merge = !deep_merge.nil? ? deep_merge : InertiaRails.deep_merge_shared_data?
1718
end
1819

1920
def render
@@ -41,22 +42,22 @@ def layout
4142
@controller.send(:inertia_layout)
4243
end
4344

44-
def props
45-
_props = ::InertiaRails.shared_data(@controller).merge(@props).select do |key, prop|
45+
def computed_props
46+
_props = ::InertiaRails.shared_data(@controller).send(prop_merge_method, @props).select do |key, prop|
4647
if rendering_partial_component?
4748
key.in? partial_keys
4849
else
4950
!prop.is_a?(InertiaRails::Lazy)
5051
end
5152
end
5253

53-
deep_transform_values(_props, lambda {|prop| prop.respond_to?(:call) ? @controller.instance_exec(&prop) : prop })
54+
deep_transform_values(_props, lambda {|prop| prop.respond_to?(:call) ? @controller.instance_exec(&prop) : prop }).with_indifferent_access
5455
end
5556

5657
def page
5758
{
5859
component: component,
59-
props: props,
60+
props: computed_props,
6061
url: @request.original_fullpath,
6162
version: ::InertiaRails.version,
6263
}
@@ -75,5 +76,9 @@ def partial_keys
7576
def rendering_partial_component?
7677
@request.inertia_partial? && @request.headers['X-Inertia-Partial-Component'] == component
7778
end
79+
80+
def prop_merge_method
81+
@deep_merge ? :deep_merge : :merge
82+
end
7883
end
7984
end

lib/inertia_rails/rspec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def inertia_tests_setup?
7474

7575
RSpec::Matchers.define :have_exact_props do |expected_props|
7676
match do |inertia|
77-
expect(inertia.props).to eq expected_props
77+
expect(inertia.props).to eq expected_props.with_indifferent_access
7878
end
7979

8080
failure_message do |inertia|
@@ -84,7 +84,7 @@ def inertia_tests_setup?
8484

8585
RSpec::Matchers.define :include_props do |expected_props|
8686
match do |inertia|
87-
expect(inertia.props).to include expected_props
87+
expect(inertia.props).to include expected_props.with_indifferent_access
8888
end
8989

9090
failure_message do |inertia|
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
class InertiaMergeInstancePropsController < ApplicationController
2+
use_inertia_instance_props
3+
inertia_share do
4+
{
5+
nested: {
6+
points: 55,
7+
rebounds: 10,
8+
}
9+
}
10+
end
11+
12+
def merge_instance_props
13+
@nested = {
14+
points: 100,
15+
}
16+
17+
render inertia: 'InertiaTestComponent', deep_merge: true
18+
end
19+
end
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
class InertiaMergeSharedController < ApplicationController
2+
inertia_share do
3+
{
4+
nested: {
5+
goals: 100,
6+
assists: 100,
7+
}
8+
}
9+
end
10+
11+
def merge_shared
12+
render inertia: 'ShareTestComponent', props: {
13+
nested: {
14+
assists: 200,
15+
}
16+
}
17+
end
18+
19+
def deep_merge_shared
20+
render inertia: 'ShareTestComponent', props: {
21+
nested: {
22+
assists: 300,
23+
}
24+
}, deep_merge: true
25+
end
26+
27+
def shallow_merge_shared
28+
render inertia: 'ShareTestComponent', props: {
29+
nested: {
30+
assists: 200,
31+
}
32+
}, deep_merge: false
33+
end
34+
end

spec/dummy/config/routes.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,9 @@
3434
get 'provided_props_test' => 'inertia_rails_mimic#provided_props_test'
3535

3636
inertia 'inertia_route' => 'TestComponent'
37+
38+
get 'merge_shared' => 'inertia_merge_shared#merge_shared'
39+
get 'deep_merge_shared' => 'inertia_merge_shared#deep_merge_shared'
40+
get 'shallow_merge_shared' => 'inertia_merge_shared#shallow_merge_shared'
41+
get 'merge_instance_props' => 'inertia_merge_instance_props#merge_instance_props'
3742
end

spec/inertia/rendering_spec.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
let(:controller) { double('Controller', inertia_view_assigns: {})}
55

66
context 'first load' do
7-
let(:page) { InertiaRails::Renderer.new('TestComponent', controller, request, response, '', props: nil, view_data: nil).send(:page) }
7+
let(:page) { InertiaRails::Renderer.new('TestComponent', controller, request, response, '').send(:page) }
88

99
context 'with props' do
10-
let(:page) { InertiaRails::Renderer.new('TestComponent', controller, request, response, '', props: {name: 'Brandon', sport: 'hockey'}, view_data: nil).send(:page) }
10+
let(:page) { InertiaRails::Renderer.new('TestComponent', controller, request, response, '', props: {name: 'Brandon', sport: 'hockey'}).send(:page) }
1111
before { get props_path }
1212

1313
it { is_expected.to include inertia_div(page) }
@@ -39,7 +39,7 @@
3939
end
4040

4141
context 'subsequent requests' do
42-
let(:page) { InertiaRails::Renderer.new('TestComponent', controller, request, response, '', props: {name: 'Brandon', sport: 'hockey'}, view_data: nil).send(:page) }
42+
let(:page) { InertiaRails::Renderer.new('TestComponent', controller, request, response, '', props: {name: 'Brandon', sport: 'hockey'}).send(:page) }
4343
let(:headers) { {'X-Inertia' => true} }
4444

4545
before { get props_path, headers: headers }
@@ -64,7 +64,7 @@
6464

6565
context 'partial rendering' do
6666
let (:page) {
67-
InertiaRails::Renderer.new('TestComponent', controller, request, response, '', props: { sport: 'hockey'}, view_data: nil).send(:page)
67+
InertiaRails::Renderer.new('TestComponent', controller, request, response, '', props: { sport: 'hockey'}).send(:page)
6868
}
6969
let(:headers) {{
7070
'X-Inertia' => true,
@@ -94,7 +94,7 @@
9494
context 'lazy prop rendering' do
9595
context 'on first load' do
9696
let (:page) {
97-
InertiaRails::Renderer.new('TestComponent', controller, request, response, '', props: { name: 'Brian'}, view_data: nil).send(:page)
97+
InertiaRails::Renderer.new('TestComponent', controller, request, response, '', props: { name: 'Brian'}).send(:page)
9898
}
9999
before { get lazy_props_path }
100100

@@ -103,7 +103,7 @@
103103

104104
context 'with a partial reload' do
105105
let (:page) {
106-
InertiaRails::Renderer.new('TestComponent', controller, request, response, '', props: { sport: 'basketball', level: 'worse than he believes', grit: 'intense'}, view_data: nil).send(:page)
106+
InertiaRails::Renderer.new('TestComponent', controller, request, response, '', props: { sport: 'basketball', level: 'worse than he believes', grit: 'intense'}).send(:page)
107107
}
108108
let(:headers) {{
109109
'X-Inertia' => true,

spec/inertia/sharing_spec.rb

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
RSpec.describe 'using inertia share when rendering views', type: :request do
2-
subject { JSON.parse(response.body)['props'].symbolize_keys }
2+
subject { JSON.parse(response.body)['props'].deep_symbolize_keys }
33

44
context 'using inertia share' do
55
let(:props) { {name: 'Brandon', sport: 'hockey', position: 'center', number: 29} }
@@ -99,4 +99,46 @@
9999
expect(InertiaRails.shared_blocks).to be_empty
100100
end
101101
end
102+
103+
describe 'deep or shallow merging shared data' do
104+
context 'with default settings (shallow merge)' do
105+
describe 'shallow merging by default' do
106+
let(:props) { { nested: { assists: 200 } } }
107+
before { get merge_shared_path, headers: {'X-Inertia' => true} }
108+
it { is_expected.to eq props }
109+
end
110+
111+
context 'with deep merge added to the renderer' do
112+
let(:props) { { nested: { goals: 100, assists: 300 } } }
113+
before { get deep_merge_shared_path, headers: {'X-Inertia' => true} }
114+
it { is_expected.to eq props }
115+
end
116+
end
117+
118+
context 'with deep merge configured as the default' do
119+
before {
120+
InertiaRails.configure { |config| config.deep_merge_shared_data = true }
121+
}
122+
after {
123+
InertiaRails.configure { |config| config.deep_merge_shared_data = false }
124+
}
125+
describe 'deep merging by default' do
126+
let(:props) { { nested: { goals: 100, assists: 200 } } }
127+
before { get merge_shared_path, headers: {'X-Inertia' => true} }
128+
it { is_expected.to eq props }
129+
end
130+
131+
describe 'overriding deep merge in a specific action' do
132+
let(:props) { { nested: { assists: 200 } } }
133+
before { get shallow_merge_shared_path, headers: {'X-Inertia' => true} }
134+
it { is_expected.to eq props }
135+
end
136+
end
137+
138+
context 'merging with instance props' do
139+
let(:props) { { nested: { points: 100, rebounds: 10 } } }
140+
before { get merge_instance_props_path, headers: {'X-Inertia' => true} }
141+
it { is_expected.to eq props }
142+
end
143+
end
102144
end

0 commit comments

Comments
 (0)