From 6022d9d8f4c2367d9f93587fb1d0cf11191c755a Mon Sep 17 00:00:00 2001 From: Pedro Fernandes Steimbruch Date: Mon, 15 Sep 2025 14:56:50 -0300 Subject: [PATCH 1/3] Implement merge_prop_transformer configuration --- lib/inertia_rails/configuration.rb | 7 ++ lib/inertia_rails/renderer.rb | 10 ++- ...ertia_merge_prop_transformer_controller.rb | 39 ++++++++++ spec/dummy/config/routes.rb | 4 + spec/inertia/merge_prop_transformer_spec.rb | 75 +++++++++++++++++++ 5 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 spec/dummy/app/controllers/inertia_merge_prop_transformer_controller.rb create mode 100644 spec/inertia/merge_prop_transformer_spec.rb diff --git a/lib/inertia_rails/configuration.rb b/lib/inertia_rails/configuration.rb index 482802c3..5c523ea0 100644 --- a/lib/inertia_rails/configuration.rb +++ b/lib/inertia_rails/configuration.rb @@ -15,6 +15,9 @@ class Configuration # A function that transforms the props before they are sent to the client. prop_transformer: ->(props:) { props }, + # A function that transforms the array of merge props before they are sent to the client. + merge_prop_transformer: ->(merge_props:) { merge_props }, + # DEPRECATED: Let Rails decide which layout should be used based on the # controller configuration. layout: true, @@ -99,6 +102,10 @@ def prop_transformer(props:) @options[:prop_transformer].call(props: props) end + def merge_prop_transformer(merge_props:) + @options[:merge_prop_transformer].call(merge_props: merge_props) + end + OPTION_NAMES.each do |option| unless method_defined?(option) define_method(option) do diff --git a/lib/inertia_rails/renderer.rb b/lib/inertia_rails/renderer.rb index 36356fbb..346fca7d 100644 --- a/lib/inertia_rails/renderer.rb +++ b/lib/inertia_rails/renderer.rb @@ -127,8 +127,14 @@ def page prop.match_on.map { |ms| "#{key}.#{ms}" } if prop.match_on.present? end.flatten - default_page[:mergeProps] = merge_props.map(&:first) if merge_props.present? - default_page[:deepMergeProps] = deep_merge_props.map(&:first) if deep_merge_props.present? + if merge_props.present? + default_page[:mergeProps] = configuration.merge_prop_transformer(merge_props: merge_props.map(&:first)) + end + + if deep_merge_props.present? + default_page[:deepMergeProps] = configuration.merge_prop_transformer(merge_props: deep_merge_props.map(&:first)) + end + default_page[:matchPropsOn] = match_props_on if match_props_on.present? default_page diff --git a/spec/dummy/app/controllers/inertia_merge_prop_transformer_controller.rb b/spec/dummy/app/controllers/inertia_merge_prop_transformer_controller.rb new file mode 100644 index 00000000..00bba54d --- /dev/null +++ b/spec/dummy/app/controllers/inertia_merge_prop_transformer_controller.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class InertiaMergePropTransformerController < ApplicationController + inertia_config( + merge_prop_transformer: lambda do |merge_props:| + merge_props.map { |prop| prop.to_s.camelize(:lower) } + end + ) + + def with_merge_props + render inertia: 'TestComponent', props: { + snake_case_merge: InertiaRails.merge { 'merge prop' }, + another_snake_merge: InertiaRails.merge(match_on: 'id') { [id: 1] }, + regular_prop: 'regular prop', + } + end + + def with_deep_merge_props + render inertia: 'TestComponent', props: { + snake_case_deep_merge: InertiaRails.deep_merge { { deep: 'merge prop' } }, + another_snake_deep_merge: InertiaRails.deep_merge(match_on: 'deep.id') { { deep: [id: 1] } }, + regular_prop: 'regular prop', + } + end + + def with_both_merge_types + render inertia: 'TestComponent', props: { + snake_case_merge: InertiaRails.merge { 'merge prop' }, + snake_case_deep_merge: InertiaRails.deep_merge { { deep: 'merge prop' } }, + regular_prop: 'regular prop', + } + end + + def no_merge_props + render inertia: 'TestComponent', props: { + regular_prop: 'regular prop', + } + end +end diff --git a/spec/dummy/config/routes.rb b/spec/dummy/config/routes.rb index 8dae460e..b017cc0f 100644 --- a/spec/dummy/config/routes.rb +++ b/spec/dummy/config/routes.rb @@ -45,6 +45,10 @@ get 'prop_transformer_test' => 'inertia_prop_transformer#just_props' get 'prop_transformer_with_meta_test' => 'inertia_prop_transformer#props_and_meta' get 'prop_transformer_no_props_test' => 'inertia_prop_transformer#no_props' + get 'merge_prop_transformer_test' => 'inertia_merge_prop_transformer#with_merge_props' + get 'merge_prop_transformer_deep_test' => 'inertia_merge_prop_transformer#with_deep_merge_props' + get 'merge_prop_transformer_both_test' => 'inertia_merge_prop_transformer#with_both_merge_types' + get 'merge_prop_transformer_no_merge_test' => 'inertia_merge_prop_transformer#no_merge_props' get 'default_component_test' => 'inertia_rails_mimic#default_component_test' get 'default_component_with_props_test' => 'inertia_rails_mimic#default_component_with_props_test' get 'default_component_with_duplicated_props_test' => 'inertia_rails_mimic#default_component_with_duplicated_props_test' diff --git a/spec/inertia/merge_prop_transformer_spec.rb b/spec/inertia/merge_prop_transformer_spec.rb new file mode 100644 index 00000000..a806c515 --- /dev/null +++ b/spec/inertia/merge_prop_transformer_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require_relative '../../lib/inertia_rails/rspec' +RSpec.describe 'merge props can be transformed', type: :request, inertia: true do + let(:headers) do + { + 'X-Inertia' => true, + 'X-Inertia-Partial-Component' => 'TestComponent', + } + end + + context 'merge props are provided' do + it 'transforms the merge props from snake_case to camelCase' do + get merge_prop_transformer_test_path, headers: headers + + expect_inertia.to render_component('TestComponent') + .and have_exact_props({ + 'snake_case_merge' => 'merge prop', + 'another_snake_merge' => [{ 'id' => 1 }], + 'regular_prop' => 'regular prop', + }) + + # Check that mergeProps array contains transformed keys + expect(response.parsed_body['mergeProps']).to eq(['snakeCaseMerge', 'anotherSnakeMerge']) + end + end + + context 'deep merge props are provided' do + it 'transforms the deep merge props from snake_case to camelCase' do + get merge_prop_transformer_deep_test_path, headers: headers + + expect_inertia.to render_component('TestComponent') + .and have_exact_props({ + 'snake_case_deep_merge' => { 'deep' => 'merge prop' }, + 'another_snake_deep_merge' => { 'deep' => [{ 'id' => 1 }] }, + 'regular_prop' => 'regular prop', + }) + + # Check that deepMergeProps array contains transformed keys + expect(response.parsed_body['deepMergeProps']).to eq(['snakeCaseDeepMerge', 'anotherSnakeDeepMerge']) + end + end + + context 'both merge and deep merge props are provided' do + it 'transforms both types of merge props from snake_case to camelCase' do + get merge_prop_transformer_both_test_path, headers: headers + + expect_inertia.to render_component('TestComponent') + .and have_exact_props({ + 'snake_case_merge' => 'merge prop', + 'snake_case_deep_merge' => { 'deep' => 'merge prop' }, + 'regular_prop' => 'regular prop', + }) + + # Check that both mergeProps and deepMergeProps arrays contain transformed keys + expect(response.parsed_body['mergeProps']).to eq(['snakeCaseMerge']) + expect(response.parsed_body['deepMergeProps']).to eq(['snakeCaseDeepMerge']) + end + end + + context 'no merge props are provided' do + it 'does not error and does not include mergeProps or deepMergeProps' do + get merge_prop_transformer_no_merge_test_path, headers: headers + + expect_inertia.to render_component('TestComponent') + .and have_exact_props({ + 'regular_prop' => 'regular prop', + }) + + # Check that no merge props arrays are present + expect(response.parsed_body).not_to have_key('mergeProps') + expect(response.parsed_body).not_to have_key('deepMergeProps') + end + end +end From c70d3bbae7f85c7b55ee0541d56ea35828ddf8eb Mon Sep 17 00:00:00 2001 From: Pedro Fernandes Steimbruch Date: Mon, 15 Sep 2025 15:03:16 -0300 Subject: [PATCH 2/3] Add documentation --- docs/guide/configuration.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 6529e739..6ba15a15 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -71,6 +71,25 @@ def underscore_params end ``` +### `merge_prop_transformer` + +**Default**: `->(merge_props:) { merge_props }` + +Use `merge_prop_transformer` to apply a transformation to the array of merge prop keys before they're sent to the client. This is particularly useful to maintain consistency when using `prop_transformer` to convert prop keys to `camelCase`, as it ensures that merge props (used with `InertiaRails.merge` and `InertiaRails.deep_merge`) also follow the same naming convention: + +```ruby + inertia_config( + prop_transformer: lambda do |props:| + props.deep_transform_keys { |key| key.to_s.camelize(:lower) } + end, + merge_prop_transformer: lambda do |merge_props:| + merge_props.map { |prop| prop.to_s.camelize(:lower) } + end + ) +``` + +Without `merge_prop_transformer`, you would have inconsistent naming where regular props use `camelCase` but merge prop keys remain in `snake_case`. This transformer only affects the array of merge prop keys that gets sent to the client in the `mergeProps` and `deepMergeProps` fields. + ### `deep_merge_shared_data` **Default**: `false` From ee24f05547499f38dc172587af93648aec25da36 Mon Sep 17 00:00:00 2001 From: Pedro Fernandes Steimbruch Date: Mon, 22 Sep 2025 13:50:19 -0300 Subject: [PATCH 3/3] Call the merge_prop_transformer for the match_props_on --- lib/inertia_rails/renderer.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/inertia_rails/renderer.rb b/lib/inertia_rails/renderer.rb index 346fca7d..9d06d35e 100644 --- a/lib/inertia_rails/renderer.rb +++ b/lib/inertia_rails/renderer.rb @@ -135,7 +135,9 @@ def page default_page[:deepMergeProps] = configuration.merge_prop_transformer(merge_props: deep_merge_props.map(&:first)) end - default_page[:matchPropsOn] = match_props_on if match_props_on.present? + if match_props_on.present? + default_page[:matchPropsOn] = configuration.merge_prop_transformer(merge_props: match_props_on) + end default_page end