Skip to content

Commit

Permalink
Merge pull request #99 from inertiajs/deep-merge-shared-props
Browse files Browse the repository at this point in the history
Deep merge props passed to render method with the shared data
  • Loading branch information
bknoles authored Aug 21, 2023
2 parents 1ff089d + 5ce3149 commit 658b257
Show file tree
Hide file tree
Showing 10 changed files with 201 additions and 16 deletions.
72 changes: 72 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,76 @@ class EventsController < ApplicationController
end
```

#### Deep Merging Shared Data

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.

```ruby
class ApplicationController
inertia_share do
{ basketball_data: { points: 50, rebounds: 100 } }
end
end
```

Let's say we want a particular action to change only part of that data structure. The renderer accepts a `deep_merge` option:

```ruby
class CrazyScorersController < ApplicationController
def index
render inertia: 'CrazyScorersComponent',
props: { basketball_data: { points: 100 } },
deep_merge: true
end
end

# The renderer will send this to the frontend:
{
basketball_data: {
points: 100,
rebounds: 100,
}
}
```

Deep merging can be set as the project wide default via the InertiaRails configuration:

```ruby
# config/initializers/some_initializer.rb
InertiaRails.configure do |config|
config.deep_merge_shared_data = true
end

```

If deep merging is enabled by default, it's possible to opt out within the action:

```ruby
class CrazyScorersController < ApplicationController
inertia_share do
{
basketball_data: {
points: 50,
rebounds: 10,
}
}
end

def index
render inertia: 'CrazyScorersComponent',
props: { basketball_data: { points: 100 } },
deep_merge: false
end
end

# Even if deep merging is set by default, since the renderer has `deep_merge: false`, it will send a shallow merge to the frontend:
{
basketball_data: {
points: 100,
}
}
```

### Lazy Props

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.
Expand Down Expand Up @@ -139,6 +209,8 @@ InertiaRails.configure do |config|
# ssr specific options
config.ssr_enabled = false
config.ssr_url = 'http://localhost:13714'

config.deep_merge_shared_data = false

end
```
Expand Down
1 change: 1 addition & 0 deletions lib/inertia_rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
method(:render),
props: options[:props],
view_data: options[:view_data],
deep_merge: options[:deep_merge],
).render
end

Expand Down
9 changes: 8 additions & 1 deletion lib/inertia_rails/inertia_rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ def self.configure

# "Getters"
def self.shared_data(controller)
shared_plain_data.merge!(evaluated_blocks(controller, shared_blocks))
shared_plain_data.
merge!(evaluated_blocks(controller, shared_blocks)).
with_indifferent_access
end

def self.version
Expand All @@ -40,6 +42,10 @@ def self.html_headers
self.threadsafe_html_headers || []
end

def self.deep_merge_shared_data?
Configuration.deep_merge_shared_data
end

# "Setters"
def self.share(**args)
self.shared_plain_data = self.shared_plain_data.merge(args)
Expand Down Expand Up @@ -71,6 +77,7 @@ module Configuration
mattr_accessor(:ssr_enabled) { false }
mattr_accessor(:ssr_url) { 'http://localhost:13714' }
mattr_accessor(:default_render) { false }
mattr_accessor(:deep_merge_shared_data) { false }

def self.evaluated_version
self.version.respond_to?(:call) ? self.version.call : self.version
Expand Down
17 changes: 11 additions & 6 deletions lib/inertia_rails/renderer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ module InertiaRails
class Renderer
attr_reader :component, :view_data

def initialize(component, controller, request, response, render_method, props:, view_data:)
def initialize(component, controller, request, response, render_method, props: nil, view_data: nil, deep_merge: nil)
@component = component.is_a?(TrueClass) ? "#{controller.controller_path}/#{controller.action_name}" : component
@controller = controller
@request = request
@response = response
@render_method = render_method
@props = props || controller.inertia_view_assigns
@props = props ? props.with_indifferent_access : controller.inertia_view_assigns.with_indifferent_access
@view_data = view_data || {}
@deep_merge = !deep_merge.nil? ? deep_merge : InertiaRails.deep_merge_shared_data?
end

def render
Expand Down Expand Up @@ -41,22 +42,22 @@ def layout
@controller.send(:inertia_layout)
end

def props
_props = ::InertiaRails.shared_data(@controller).merge(@props).select do |key, prop|
def computed_props
_props = ::InertiaRails.shared_data(@controller).send(prop_merge_method, @props).select do |key, prop|
if rendering_partial_component?
key.in? partial_keys
else
!prop.is_a?(InertiaRails::Lazy)
end
end

deep_transform_values(_props, lambda {|prop| prop.respond_to?(:call) ? @controller.instance_exec(&prop) : prop })
deep_transform_values(_props, lambda {|prop| prop.respond_to?(:call) ? @controller.instance_exec(&prop) : prop }).with_indifferent_access
end

def page
{
component: component,
props: props,
props: computed_props,
url: @request.original_fullpath,
version: ::InertiaRails.version,
}
Expand All @@ -75,5 +76,9 @@ def partial_keys
def rendering_partial_component?
@request.inertia_partial? && @request.headers['X-Inertia-Partial-Component'] == component
end

def prop_merge_method
@deep_merge ? :deep_merge : :merge
end
end
end
4 changes: 2 additions & 2 deletions lib/inertia_rails/rspec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def inertia_tests_setup?

RSpec::Matchers.define :have_exact_props do |expected_props|
match do |inertia|
expect(inertia.props).to eq expected_props
expect(inertia.props).to eq expected_props.with_indifferent_access
end

failure_message do |inertia|
Expand All @@ -84,7 +84,7 @@ def inertia_tests_setup?

RSpec::Matchers.define :include_props do |expected_props|
match do |inertia|
expect(inertia.props).to include expected_props
expect(inertia.props).to include expected_props.with_indifferent_access
end

failure_message do |inertia|
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class InertiaMergeInstancePropsController < ApplicationController
use_inertia_instance_props
inertia_share do
{
nested: {
points: 55,
rebounds: 10,
}
}
end

def merge_instance_props
@nested = {
points: 100,
}

render inertia: 'InertiaTestComponent', deep_merge: true
end
end
34 changes: 34 additions & 0 deletions spec/dummy/app/controllers/inertia_merge_shared_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
class InertiaMergeSharedController < ApplicationController
inertia_share do
{
nested: {
goals: 100,
assists: 100,
}
}
end

def merge_shared
render inertia: 'ShareTestComponent', props: {
nested: {
assists: 200,
}
}
end

def deep_merge_shared
render inertia: 'ShareTestComponent', props: {
nested: {
assists: 300,
}
}, deep_merge: true
end

def shallow_merge_shared
render inertia: 'ShareTestComponent', props: {
nested: {
assists: 200,
}
}, deep_merge: false
end
end
5 changes: 5 additions & 0 deletions spec/dummy/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,9 @@
get 'provided_props_test' => 'inertia_rails_mimic#provided_props_test'

inertia 'inertia_route' => 'TestComponent'

get 'merge_shared' => 'inertia_merge_shared#merge_shared'
get 'deep_merge_shared' => 'inertia_merge_shared#deep_merge_shared'
get 'shallow_merge_shared' => 'inertia_merge_shared#shallow_merge_shared'
get 'merge_instance_props' => 'inertia_merge_instance_props#merge_instance_props'
end
12 changes: 6 additions & 6 deletions spec/inertia/rendering_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
let(:controller) { double('Controller', inertia_view_assigns: {})}

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

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

it { is_expected.to include inertia_div(page) }
Expand Down Expand Up @@ -39,7 +39,7 @@
end

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

before { get props_path, headers: headers }
Expand All @@ -64,7 +64,7 @@

context 'partial rendering' do
let (:page) {
InertiaRails::Renderer.new('TestComponent', controller, request, response, '', props: { sport: 'hockey'}, view_data: nil).send(:page)
InertiaRails::Renderer.new('TestComponent', controller, request, response, '', props: { sport: 'hockey'}).send(:page)
}
let(:headers) {{
'X-Inertia' => true,
Expand Down Expand Up @@ -94,7 +94,7 @@
context 'lazy prop rendering' do
context 'on first load' do
let (:page) {
InertiaRails::Renderer.new('TestComponent', controller, request, response, '', props: { name: 'Brian'}, view_data: nil).send(:page)
InertiaRails::Renderer.new('TestComponent', controller, request, response, '', props: { name: 'Brian'}).send(:page)
}
before { get lazy_props_path }

Expand All @@ -103,7 +103,7 @@

context 'with a partial reload' do
let (:page) {
InertiaRails::Renderer.new('TestComponent', controller, request, response, '', props: { sport: 'basketball', level: 'worse than he believes', grit: 'intense'}, view_data: nil).send(:page)
InertiaRails::Renderer.new('TestComponent', controller, request, response, '', props: { sport: 'basketball', level: 'worse than he believes', grit: 'intense'}).send(:page)
}
let(:headers) {{
'X-Inertia' => true,
Expand Down
44 changes: 43 additions & 1 deletion spec/inertia/sharing_spec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
RSpec.describe 'using inertia share when rendering views', type: :request do
subject { JSON.parse(response.body)['props'].symbolize_keys }
subject { JSON.parse(response.body)['props'].deep_symbolize_keys }

context 'using inertia share' do
let(:props) { {name: 'Brandon', sport: 'hockey', position: 'center', number: 29} }
Expand Down Expand Up @@ -99,4 +99,46 @@
expect(InertiaRails.shared_blocks).to be_empty
end
end

describe 'deep or shallow merging shared data' do
context 'with default settings (shallow merge)' do
describe 'shallow merging by default' do
let(:props) { { nested: { assists: 200 } } }
before { get merge_shared_path, headers: {'X-Inertia' => true} }
it { is_expected.to eq props }
end

context 'with deep merge added to the renderer' do
let(:props) { { nested: { goals: 100, assists: 300 } } }
before { get deep_merge_shared_path, headers: {'X-Inertia' => true} }
it { is_expected.to eq props }
end
end

context 'with deep merge configured as the default' do
before {
InertiaRails.configure { |config| config.deep_merge_shared_data = true }
}
after {
InertiaRails.configure { |config| config.deep_merge_shared_data = false }
}
describe 'deep merging by default' do
let(:props) { { nested: { goals: 100, assists: 200 } } }
before { get merge_shared_path, headers: {'X-Inertia' => true} }
it { is_expected.to eq props }
end

describe 'overriding deep merge in a specific action' do
let(:props) { { nested: { assists: 200 } } }
before { get shallow_merge_shared_path, headers: {'X-Inertia' => true} }
it { is_expected.to eq props }
end
end

context 'merging with instance props' do
let(:props) { { nested: { points: 100, rebounds: 10 } } }
before { get merge_instance_props_path, headers: {'X-Inertia' => true} }
it { is_expected.to eq props }
end
end
end

0 comments on commit 658b257

Please sign in to comment.