Skip to content

[Feature] Add a configuration option to transform props coming from Inertia #254

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,33 @@ Inertia Rails supports setting any configuration option via environment variable

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).

### `prop_transformer`

**Default**: `->(props:) { props }`

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:

```ruby
inertia_config(
prop_transformer: lambda do |props:|
props.deep_transform_keys { |key| key.to_s.camelize(:lower) }
end
)
```

> [!NOTE]
> 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`:
```ruby
before_action :underscore_params

# ...

def underscore_params
params.deep_transform_keys! { |key| key.to_s.underscore }
end
```
Comment on lines +47 to +72
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably needs work. Open to suggestions!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would drop this additional section about input props tbh, this configuration option is not only about casing, so it looks misplaced to me

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see where you're coming from, but I think it's very likely that prop casing is the principal usage of this feature. I can try adjusting the wording to be a little more generic but it makes sense to me to highlight the transformation isn't bidirectional


### `deep_merge_shared_data`

**Default**: `false`
Expand Down
7 changes: 7 additions & 0 deletions lib/inertia_rails/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ class Configuration
# Allows the user to hook into the default rendering behavior and change it to fit their needs
component_path_resolver: ->(path:, action:) { "#{path}/#{action}" },

# A function that transforms the props before they are sent to the client.
prop_transformer: ->(props:) { props },

# DEPRECATED: Let Rails decide which layout should be used based on the
# controller configuration.
layout: true,
Expand Down Expand Up @@ -89,6 +92,10 @@ def component_path_resolver(path:, action:)
@options[:component_path_resolver].call(path: path, action: action)
end

def prop_transformer(props:)
@options[:prop_transformer].call(props: props)
end

OPTION_NAMES.each do |option|
unless method_defined?(option)
define_method(option) do
Expand Down
11 changes: 7 additions & 4 deletions lib/inertia_rails/renderer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,13 @@ def merge_props(shared_props, props)
end

def computed_props
merged_props = merge_props(shared_data, props)
deep_transform_props(merged_props).tap do |transformed_props|
transformed_props[:_inertia_meta] = meta_tags if meta_tags.present?
end
merge_props(shared_data, props)
# This performs the internal work of hydrating/filtering props
.then { |props| deep_transform_props(props) }
# Then we apply the user-defined prop transformer
.then { |props| configuration.prop_transformer(props: props) }
# Then we add meta tags after everything since they must not be transformed
.tap { |props| props[:_inertia_meta] = meta_tags if meta_tags.present? }
Comment on lines +93 to +99
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This order ensures that any current or future internal props (like _inertia_meta) are untouched by the user-provided transformer. As long as they're added later in the chain than the call to prop_transformer, they're good to go

end

def page
Expand Down
32 changes: 32 additions & 0 deletions spec/dummy/app/controllers/inertia_prop_transformer_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

class InertiaPropTransformerController < ApplicationController
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was kind of winging it with this. Open to feedback!

inertia_config(
prop_transformer: lambda do |props:|
props.deep_transform_keys { |key| key.to_s.upcase }
end
)

def just_props
render inertia: 'TestComponent', props: {
lower_prop: 'lower_value',
parent_hash: {
lower_child_prop: 'lower_child_value',
},
}
end

def props_and_meta
render inertia: 'TestComponent',
props: {
lower_prop: 'lower_value',
},
meta: [
{ name: 'description', content: "Don't transform me!" }
]
end

def no_props
render inertia: 'TestComponent'
end
end
3 changes: 3 additions & 0 deletions spec/dummy/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
get 'instance_props_test' => 'inertia_rails_mimic#instance_props_test'
get 'default_render_test' => 'inertia_rails_mimic#default_render_test'
get 'transformed_default_render_test' => 'transformed_inertia_rails_mimic#render_test'
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 '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'
Expand Down
60 changes: 60 additions & 0 deletions spec/inertia/prop_transformer_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal: true

require_relative '../../lib/inertia_rails/rspec'
RSpec.describe 'props can be transformed', type: :request, inertia: true do
let(:headers) do
{
'X-Inertia' => true,
'X-Inertia-Partial-Component' => 'TestComponent',
}
end

context 'props are provided' do
it 'transforms the props' do
get prop_transformer_test_path, headers: headers

expect_inertia.to render_component('TestComponent')
.and have_exact_props({
'LOWER_PROP' => 'lower_value',
'PARENT_HASH' => {
'LOWER_CHILD_PROP' => 'lower_child_value',
},
})
end
end

context 'props and meta are provided' do
it 'transforms the props' do
get prop_transformer_with_meta_test_path, headers: headers

expect_inertia.to render_component('TestComponent')
.and include_props({
'LOWER_PROP' => 'lower_value',
})
end

it 'does not transform the meta' do
get prop_transformer_with_meta_test_path, headers: headers

expect(response.parsed_body['props']['_inertia_meta']).to eq(
[
{
'tagName' => 'meta',
'name' => 'description',
'content' => "Don't transform me!",
'headKey' => 'meta-name-description',
}
]
)
end
end

context 'no props are provided' do
it 'does not error' do
get prop_transformer_no_props_test_path, headers: headers

expect_inertia.to render_component('TestComponent')
.and have_exact_props({})
end
end
end