diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 36f9f8d..93158f8 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -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 +``` + ### `deep_merge_shared_data` **Default**: `false` diff --git a/lib/inertia_rails/configuration.rb b/lib/inertia_rails/configuration.rb index 9cd7459..c2415ca 100644 --- a/lib/inertia_rails/configuration.rb +++ b/lib/inertia_rails/configuration.rb @@ -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, @@ -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 diff --git a/lib/inertia_rails/renderer.rb b/lib/inertia_rails/renderer.rb index 12e5bb2..46c7de8 100644 --- a/lib/inertia_rails/renderer.rb +++ b/lib/inertia_rails/renderer.rb @@ -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? } end def page diff --git a/spec/dummy/app/controllers/inertia_prop_transformer_controller.rb b/spec/dummy/app/controllers/inertia_prop_transformer_controller.rb new file mode 100644 index 0000000..403d75e --- /dev/null +++ b/spec/dummy/app/controllers/inertia_prop_transformer_controller.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class InertiaPropTransformerController < ApplicationController + 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 diff --git a/spec/dummy/config/routes.rb b/spec/dummy/config/routes.rb index 717fbaf..8dae460 100644 --- a/spec/dummy/config/routes.rb +++ b/spec/dummy/config/routes.rb @@ -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' diff --git a/spec/inertia/prop_transformer_spec.rb b/spec/inertia/prop_transformer_spec.rb new file mode 100644 index 0000000..5ccb1fa --- /dev/null +++ b/spec/inertia/prop_transformer_spec.rb @@ -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