Skip to content

Commit 04b8f90

Browse files
authored
Merge pull request #254 from kieraneglin/ke/prop-transformer
[Feature] Add a configuration option to transform props coming from Inertia
2 parents 554f7ec + 31558c4 commit 04b8f90

File tree

6 files changed

+143
-9
lines changed

6 files changed

+143
-9
lines changed

docs/guide/configuration.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,33 @@ Inertia Rails supports setting any configuration option via environment variable
4444

4545
Use `component_path_resolver` to customize component path resolution when [`default_render`](#default_render) config value is set to `true`. The value should be callable and will receive the `path` and `action` parameters, returning a string component path. See [Automatically determine component name](/guide/responses#automatically-determine-component-name).
4646

47+
### `prop_transformer`
48+
49+
**Default**: `->(props:) { props }`
50+
51+
Use `prop_transformer` to apply a transformation to your props before they're sent to the view. One use-case this enables is to work with `snake_case` props within Rails while working with `camelCase` in your view:
52+
53+
```ruby
54+
inertia_config(
55+
prop_transformer: lambda do |props:|
56+
props.deep_transform_keys { |key| key.to_s.camelize(:lower) }
57+
end
58+
)
59+
```
60+
61+
> [!NOTE]
62+
> This controls the props provided by Inertia Rails but does not concern itself with props coming _into_ Rails. You may want to add a global `before_action` to `ApplicationController`:
63+
64+
```ruby
65+
before_action :underscore_params
66+
67+
# ...
68+
69+
def underscore_params
70+
params.deep_transform_keys! { |key| key.to_s.underscore }
71+
end
72+
```
73+
4774
### `deep_merge_shared_data`
4875

4976
**Default**: `false`

lib/inertia_rails/configuration.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ class Configuration
1212
# Allows the user to hook into the default rendering behavior and change it to fit their needs
1313
component_path_resolver: ->(path:, action:) { "#{path}/#{action}" },
1414

15+
# A function that transforms the props before they are sent to the client.
16+
prop_transformer: ->(props:) { props },
17+
1518
# DEPRECATED: Let Rails decide which layout should be used based on the
1619
# controller configuration.
1720
layout: true,
@@ -92,6 +95,10 @@ def component_path_resolver(path:, action:)
9295
@options[:component_path_resolver].call(path: path, action: action)
9396
end
9497

98+
def prop_transformer(props:)
99+
@options[:prop_transformer].call(props: props)
100+
end
101+
95102
OPTION_NAMES.each do |option|
96103
unless method_defined?(option)
97104
define_method(option) do

lib/inertia_rails/renderer.rb

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -90,15 +90,20 @@ def merge_props(shared_props, props)
9090
end
9191

9292
def computed_props
93-
merged_props = merge_props(shared_data, props)
94-
# Always keep errors in the props
95-
if merged_props.key?(:errors) && !merged_props[:errors].is_a?(BaseProp)
96-
errors = merged_props[:errors]
97-
merged_props[:errors] = InertiaRails.always { errors }
98-
end
99-
deep_transform_props(merged_props).tap do |transformed_props|
100-
transformed_props[:_inertia_meta] = meta_tags if meta_tags.present?
101-
end
93+
# rubocop:disable Style/MultilineBlockChain
94+
merge_props(shared_data, props)
95+
.then do |merged_props| # Always keep errors in the props
96+
if merged_props.key?(:errors) && !merged_props[:errors].is_a?(BaseProp)
97+
errors = merged_props[:errors]
98+
merged_props[:errors] = InertiaRails.always { errors }
99+
end
100+
merged_props
101+
end
102+
.then { |props| deep_transform_props(props) } # Internal hydration/filtering
103+
.then { |props| configuration.prop_transformer(props: props) } # Apply user-defined prop transformer
104+
.tap { |props| props[:_inertia_meta] = meta_tags if meta_tags.present? } # Add meta tags last (never transformed)
105+
106+
# rubocop:enable Style/MultilineBlockChain
102107
end
103108

104109
def page
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# frozen_string_literal: true
2+
3+
class InertiaPropTransformerController < ApplicationController
4+
inertia_config(
5+
prop_transformer: lambda do |props:|
6+
props.deep_transform_keys { |key| key.to_s.upcase }
7+
end
8+
)
9+
10+
def just_props
11+
render inertia: 'TestComponent', props: {
12+
lower_prop: 'lower_value',
13+
parent_hash: {
14+
lower_child_prop: 'lower_child_value',
15+
},
16+
}
17+
end
18+
19+
def props_and_meta
20+
render inertia: 'TestComponent',
21+
props: {
22+
lower_prop: 'lower_value',
23+
},
24+
meta: [
25+
{ name: 'description', content: "Don't transform me!" }
26+
]
27+
end
28+
29+
def no_props
30+
render inertia: 'TestComponent'
31+
end
32+
end

spec/dummy/config/routes.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@
4242
get 'instance_props_test' => 'inertia_rails_mimic#instance_props_test'
4343
get 'default_render_test' => 'inertia_rails_mimic#default_render_test'
4444
get 'transformed_default_render_test' => 'transformed_inertia_rails_mimic#render_test'
45+
get 'prop_transformer_test' => 'inertia_prop_transformer#just_props'
46+
get 'prop_transformer_with_meta_test' => 'inertia_prop_transformer#props_and_meta'
47+
get 'prop_transformer_no_props_test' => 'inertia_prop_transformer#no_props'
4548
get 'default_component_test' => 'inertia_rails_mimic#default_component_test'
4649
get 'default_component_with_props_test' => 'inertia_rails_mimic#default_component_with_props_test'
4750
get 'default_component_with_duplicated_props_test' => 'inertia_rails_mimic#default_component_with_duplicated_props_test'
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# frozen_string_literal: true
2+
3+
require_relative '../../lib/inertia_rails/rspec'
4+
RSpec.describe 'props can be transformed', type: :request, inertia: true do
5+
let(:headers) do
6+
{
7+
'X-Inertia' => true,
8+
'X-Inertia-Partial-Component' => 'TestComponent',
9+
}
10+
end
11+
12+
context 'props are provided' do
13+
it 'transforms the props' do
14+
get prop_transformer_test_path, headers: headers
15+
16+
expect_inertia.to render_component('TestComponent')
17+
.and have_exact_props({
18+
'LOWER_PROP' => 'lower_value',
19+
'PARENT_HASH' => {
20+
'LOWER_CHILD_PROP' => 'lower_child_value',
21+
},
22+
})
23+
end
24+
end
25+
26+
context 'props and meta are provided' do
27+
it 'transforms the props' do
28+
get prop_transformer_with_meta_test_path, headers: headers
29+
30+
expect_inertia.to render_component('TestComponent')
31+
.and include_props({
32+
'LOWER_PROP' => 'lower_value',
33+
})
34+
end
35+
36+
it 'does not transform the meta' do
37+
get prop_transformer_with_meta_test_path, headers: headers
38+
39+
expect(response.parsed_body['props']['_inertia_meta']).to eq(
40+
[
41+
{
42+
'tagName' => 'meta',
43+
'name' => 'description',
44+
'content' => "Don't transform me!",
45+
'headKey' => 'meta-name-description',
46+
}
47+
]
48+
)
49+
end
50+
end
51+
52+
context 'no props are provided' do
53+
it 'does not error' do
54+
get prop_transformer_no_props_test_path, headers: headers
55+
56+
expect_inertia.to render_component('TestComponent')
57+
.and have_exact_props({})
58+
end
59+
end
60+
end

0 commit comments

Comments
 (0)