From 6ee52a219e89acab6a02af759af518eefd7e9c83 Mon Sep 17 00:00:00 2001 From: Svyatoslav Kryukov Date: Tue, 17 Jun 2025 18:36:13 +0300 Subject: [PATCH 1/3] Add match_on support --- docs/guide/deferred-props.md | 4 ++ docs/guide/merging-props.md | 27 +++++++++- lib/inertia_rails/defer_prop.rb | 5 +- lib/inertia_rails/inertia_rails.rb | 12 ++--- lib/inertia_rails/merge_prop.rb | 5 +- lib/inertia_rails/renderer.rb | 30 +++++++---- .../inertia_render_test_controller.rb | 4 ++ spec/inertia/rendering_spec.rb | 54 ++++++++++++++----- 8 files changed, 109 insertions(+), 32 deletions(-) diff --git a/docs/guide/deferred-props.md b/docs/guide/deferred-props.md index 9db7eb63..7e4a3a29 100644 --- a/docs/guide/deferred-props.md +++ b/docs/guide/deferred-props.md @@ -39,6 +39,10 @@ end In the example above, the `teams`, `projects`, and `tasks` props will be fetched in one request, while the `permissions` prop will be fetched in a separate request in parallel. Group names are arbitrary strings and can be anything you choose. +### Combining with mergeable props + +Deferred props can be combined with mergeable props. You can learn more about this feature in the [Merging props](/guide/merging-props) section. + ## Client side On the client side, Inertia provides the `Deferred` component to help you manage deferred props. This component will automatically wait for the specified deferred props to be available before rendering its children. diff --git a/docs/guide/merging-props.md b/docs/guide/merging-props.md index df843442..172aa54b 100644 --- a/docs/guide/merging-props.md +++ b/docs/guide/merging-props.md @@ -26,6 +26,15 @@ class UsersController < ApplicationController records: records.as_json(...), pagy: pagy_metadata(pagy) } + }, + # with match_on parameter for smart merging: + products: InertiaRails.merge(match_on: 'id') { Product.all.as_json(...) }, + # nested objects with match_on: + categories: InertiaRails.deep_merge(match_on: %w[items.id tags.id]) { + { + items: Category.all.as_json(...), + tags: Tag.all.as_json(...) + } } } end @@ -34,7 +43,14 @@ end On the client side, Inertia detects that this prop should be merged. If the prop returns an array, it will append the response to the current prop value. If it's an object, it will merge the response with the current prop value. If you have opted to `deepMerge`, Inertia ensures a deep merge of the entire structure. -**Of note:** During the merging process, if the value is an array, the incoming items will be _appended_ to the existing array, not merged by index. +### Smart merging with `match_on` + +By default, arrays are simply appended during merging. If you need to update specific items in an array or replace them based on a unique identifier, you can use the `match_on` parameter. + +The `match_on` parameter enables smart merging by specifying a field to match on when merging arrays of objects: + +- For `merge` with simple arrays, specify the object key to match on (e.g., `'id'`) +- For `deep_merge` with nested structures, use dot notation to specify the path (e.g., `'items.id'`) You can also combine [deferred props](/guide/deferred-props) with mergeable props to defer the loading of the prop and ultimately mark it as mergeable once it's loaded. @@ -54,6 +70,15 @@ class UsersController < ApplicationController records: records.as_json(...), pagy: pagy_metadata(pagy) } + }, + # with match_on parameter: + products: InertiaRails.defer(merge: true, match_on: 'id') { products.as_json(...) }, + # nested objects with match_on: + categories: InertiaRails.defer(deep_merge: true, match_on: %w[items.id tags.id]) { + { + items: Category.all.as_json(...), + tags: Tag.all.as_json(...) + } } } end diff --git a/lib/inertia_rails/defer_prop.rb b/lib/inertia_rails/defer_prop.rb index 2d486b51..ec85ac04 100644 --- a/lib/inertia_rails/defer_prop.rb +++ b/lib/inertia_rails/defer_prop.rb @@ -4,9 +4,9 @@ module InertiaRails class DeferProp < IgnoreOnFirstLoadProp DEFAULT_GROUP = 'default' - attr_reader :group + attr_reader :group, :match_on - def initialize(group: nil, merge: nil, deep_merge: nil, &block) + def initialize(group: nil, merge: nil, deep_merge: nil, match_on: nil, &block) raise ArgumentError, 'Cannot set both `deep_merge` and `merge` to true' if deep_merge && merge super(&block) @@ -14,6 +14,7 @@ def initialize(group: nil, merge: nil, deep_merge: nil, &block) @group = group || DEFAULT_GROUP @merge = merge || deep_merge @deep_merge = deep_merge + @match_on = match_on.nil? ? nil : Array(match_on) end def merge? diff --git a/lib/inertia_rails/inertia_rails.rb b/lib/inertia_rails/inertia_rails.rb index d2fb961f..f06bf8b2 100644 --- a/lib/inertia_rails/inertia_rails.rb +++ b/lib/inertia_rails/inertia_rails.rb @@ -33,16 +33,16 @@ def always(&block) AlwaysProp.new(&block) end - def merge(&block) - MergeProp.new(&block) + def merge(match_on: nil, &block) + MergeProp.new(match_on: match_on, &block) end - def deep_merge(&block) - MergeProp.new(deep_merge: true, &block) + def deep_merge(match_on: nil, &block) + MergeProp.new(deep_merge: true, match_on: match_on, &block) end - def defer(group: nil, merge: nil, deep_merge: nil, &block) - DeferProp.new(group: group, merge: merge, deep_merge: deep_merge, &block) + def defer(group: nil, merge: nil, deep_merge: nil, match_on: nil, &block) + DeferProp.new(group: group, merge: merge, deep_merge: deep_merge, match_on: match_on, &block) end end end diff --git a/lib/inertia_rails/merge_prop.rb b/lib/inertia_rails/merge_prop.rb index a90352b2..639a1e95 100644 --- a/lib/inertia_rails/merge_prop.rb +++ b/lib/inertia_rails/merge_prop.rb @@ -2,9 +2,12 @@ module InertiaRails class MergeProp < BaseProp - def initialize(deep_merge: false, &block) + attr_reader :match_on + + def initialize(deep_merge: false, match_on: nil, &block) super(&block) @deep_merge = deep_merge + @match_on = match_on.nil? ? nil : Array(match_on) end def merge? diff --git a/lib/inertia_rails/renderer.rb b/lib/inertia_rails/renderer.rb index 1e1b2043..641ddeeb 100644 --- a/lib/inertia_rails/renderer.rb +++ b/lib/inertia_rails/renderer.rb @@ -106,14 +106,17 @@ def page deferred_props = deferred_props_keys default_page[:deferredProps] = deferred_props if deferred_props.present? - all_merge_props = merge_props_keys - - deep_merge_props, merge_props = all_merge_props.partition do |key| - @props[key].deep_merge? + deep_merge_props, merge_props = all_merge_props.partition do |_key, prop| + prop.deep_merge? end - default_page[:mergeProps] = merge_props if merge_props.present? - default_page[:deepMergeProps] = deep_merge_props if deep_merge_props.present? + match_props_on = all_merge_props.filter_map do |key, prop| + 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? + default_page[:matchPropsOn] = match_props_on if match_props_on.present? default_page end @@ -147,9 +150,16 @@ def deferred_props_keys end end - def merge_props_keys - @props.each_with_object([]) do |(key, prop), result| - result << key if prop.try(:merge?) && reset_keys.exclude?(key) + def all_merge_props + @all_merge_props ||= @props.select do |key, prop| + next unless prop.try(:merge?) + next if reset_keys.include?(key) + next if rendering_partial_component? && ( + (partial_keys.present? && partial_keys.exclude?(key.name)) || + (partial_except_keys.present? && partial_except_keys.include?(key.name)) + ) + + true end end @@ -180,7 +190,7 @@ def resolve_component(component) def keep_prop?(prop, path) return true if prop.is_a?(AlwaysProp) - if rendering_partial_component? + if rendering_partial_component? && (partial_keys.present? || partial_except_keys.present?) path_with_prefixes = path_prefixes(path) return false if excluded_by_only_partial_keys?(path_with_prefixes) return false if excluded_by_except_partial_keys?(path_with_prefixes) diff --git a/spec/dummy/app/controllers/inertia_render_test_controller.rb b/spec/dummy/app/controllers/inertia_render_test_controller.rb index 4d5134be..85928972 100644 --- a/spec/dummy/app/controllers/inertia_render_test_controller.rb +++ b/spec/dummy/app/controllers/inertia_render_test_controller.rb @@ -106,10 +106,14 @@ def always_props def merge_props render inertia: 'TestComponent', props: { merge: InertiaRails.merge { 'merge prop' }, + match_on: InertiaRails.merge(match_on: 'id') { [id: 1] }, deep_merge: InertiaRails.deep_merge { { deep: 'merge prop' } }, + deep_match_on: InertiaRails.deep_merge(match_on: 'deep.id') { { deep: [id: 1] } }, regular: 'regular prop', deferred_merge: InertiaRails.defer(merge: true) { 'deferred and merge prop' }, + deferred_match_on: InertiaRails.defer(merge: true, match_on: 'id') { [id: 1] }, deferred_deep_merge: InertiaRails.defer(deep_merge: true) { { deep: 'deferred and merge prop' } }, + deferred_deep_match_on: InertiaRails.defer(deep_merge: true, match_on: 'deep.id') { { deep: [id: 1] } }, deferred: InertiaRails.defer { 'deferred' }, } end diff --git a/spec/inertia/rendering_spec.rb b/spec/inertia/rendering_spec.rb index fa635b69..537cb5c3 100644 --- a/spec/inertia/rendering_spec.rb +++ b/spec/inertia/rendering_spec.rb @@ -530,30 +530,37 @@ before { get merge_props_path, headers: headers } it 'returns non-optional props and meta on first load' do - expect(response.parsed_body['props']).to eq('merge' => 'merge prop', 'deep_merge' => { 'deep' => 'merge prop' }, - 'regular' => 'regular prop') - expect(response.parsed_body['mergeProps']).to match_array(%w[merge deferred_merge]) - expect(response.parsed_body['deepMergeProps']).to match_array(%w[deep_merge deferred_deep_merge]) - expect(response.parsed_body['deferredProps']).to eq('default' => %w[deferred_merge deferred_deep_merge deferred]) + expect(response.parsed_body['props']).to eq( + 'merge' => 'merge prop', 'match_on' => [{ 'id' => 1 }], + 'deep_merge' => { 'deep' => 'merge prop' }, 'deep_match_on' => { 'deep' => [{ 'id' => 1 }] }, + 'regular' => 'regular prop' + ) + expect(response.parsed_body['mergeProps']).to match_array(%w[merge match_on deferred_merge deferred_match_on]) + expect(response.parsed_body['deepMergeProps']).to match_array(%w[deep_merge deep_match_on deferred_deep_merge deferred_deep_match_on]) + expect(response.parsed_body['deferredProps']).to eq('default' => %w[deferred_merge deferred_match_on deferred_deep_merge deferred_deep_match_on deferred]) + expect(response.parsed_body['matchPropsOn']).to match_array(%w[deep_match_on.deep.id deferred_deep_match_on.deep.id deferred_match_on.id match_on.id]) end context 'with a partial reload' do let(:headers) do { 'X-Inertia' => true, - 'X-Inertia-Partial-Data' => 'deferred_merge,deferred_deep_merge', + 'X-Inertia-Partial-Data' => 'deferred_merge,deferred_deep_merge,deferred_deep_match_on,deferred_match_on', 'X-Inertia-Partial-Component' => 'TestComponent', } end - it 'returns listed and merge props' do + it 'returns listed merge props' do expect(response.parsed_body['props']).to eq( 'deferred_merge' => 'deferred and merge prop', - 'deferred_deep_merge' => { 'deep' => 'deferred and merge prop' } + 'deferred_deep_merge' => { 'deep' => 'deferred and merge prop' }, + 'deferred_deep_match_on' => { 'deep' => [{ 'id' => 1 }] }, + 'deferred_match_on' => [{ 'id' => 1 }] ) - expect(response.parsed_body['mergeProps']).to match_array(%w[merge deferred_merge]) - expect(response.parsed_body['deepMergeProps']).to match_array(%w[deep_merge deferred_deep_merge]) + expect(response.parsed_body['mergeProps']).to match_array(%w[deferred_merge deferred_match_on]) + expect(response.parsed_body['deepMergeProps']).to match_array(%w[deferred_deep_merge deferred_deep_match_on]) expect(response.parsed_body['deferredProps']).to be_nil + expect(response.parsed_body['matchPropsOn']).to match_array(%w[deferred_deep_match_on.deep.id deferred_match_on.id]) end end @@ -567,13 +574,36 @@ } end - it 'returns listed and merge props' do + it 'returns listed props' do expect(response.parsed_body['props']).to eq( 'deferred_merge' => 'deferred and merge prop', 'deferred_deep_merge' => { 'deep' => 'deferred and merge prop' } ) - expect(response.parsed_body['mergeProps']).to match_array(%w[merge]) + expect(response.parsed_body['mergeProps']).to be_nil + expect(response.parsed_body['deferredProps']).to be_nil + expect(response.parsed_body['matchPropsOn']).to be_nil + end + end + + context 'with an except header' do + let(:headers) do + { + 'X-Inertia' => true, + 'X-Inertia-Partial-Data' => 'deferred_merge,deferred_deep_merge,deep_match_on', + 'X-Inertia-Partial-Except' => 'deferred_merge', + 'X-Inertia-Partial-Component' => 'TestComponent', + } + end + + it 'returns only the excepted props' do + expect(response.parsed_body['props']).to eq( + 'deferred_deep_merge' => { 'deep' => 'deferred and merge prop' }, + 'deep_match_on' => { 'deep' => [{ 'id' => 1 }] } + ) + expect(response.parsed_body['mergeProps']).to be_nil + expect(response.parsed_body['deepMergeProps']).to match_array(%w[deferred_deep_merge deep_match_on]) expect(response.parsed_body['deferredProps']).to be_nil + expect(response.parsed_body['matchPropsOn']).to match_array(%w[deep_match_on.deep.id]) end end end From 58349959b9a3d226c3cb2649488a053acdef1cca Mon Sep 17 00:00:00 2001 From: Svyatoslav Kryukov Date: Fri, 20 Jun 2025 22:37:06 +0300 Subject: [PATCH 2/3] Make RuboCop happy --- lib/inertia_rails/generators/controller_template_base.rb | 2 +- lib/inertia_rails/generators/helper.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/inertia_rails/generators/controller_template_base.rb b/lib/inertia_rails/generators/controller_template_base.rb index 75d261b5..aa452646 100644 --- a/lib/inertia_rails/generators/controller_template_base.rb +++ b/lib/inertia_rails/generators/controller_template_base.rb @@ -11,7 +11,7 @@ class ControllerTemplateBase < Rails::Generators::NamedBase default: Helper.guess_the_default_framework class_option :typescript, type: :boolean, desc: 'Whether to use TypeScript', - default: Helper.guess_typescript + default: Helper.uses_typescript? argument :actions, type: :array, default: [], banner: 'action action' diff --git a/lib/inertia_rails/generators/helper.rb b/lib/inertia_rails/generators/helper.rb index fb37cdb9..51434c31 100644 --- a/lib/inertia_rails/generators/helper.rb +++ b/lib/inertia_rails/generators/helper.rb @@ -23,7 +23,7 @@ def guess_the_default_framework(package_json_path = DEFAULT_PACKAGE_PATH) end end - def guess_typescript + def uses_typescript? Rails.root.join('tsconfig.json').exist? end From a558085a05149856b52769332152a120e02b6027 Mon Sep 17 00:00:00 2001 From: Svyatoslav Kryukov Date: Sat, 21 Jun 2025 00:45:57 +0300 Subject: [PATCH 3/3] Docs: add Available since component --- .../availableSinceMarkdownPlugin.ts | 60 ++++ docs/.vitepress/config.mts | 2 + .../theme/components/AvailableSince.vue | 257 ++++++++++++++++++ docs/.vitepress/theme/components/index.ts | 5 + docs/.vitepress/theme/index.ts | 2 + docs/guide/configuration.md | 9 +- docs/guide/deferred-props.md | 2 + docs/guide/merging-props.md | 37 ++- 8 files changed, 366 insertions(+), 8 deletions(-) create mode 100644 docs/.vitepress/availableSinceMarkdownPlugin.ts create mode 100644 docs/.vitepress/theme/components/AvailableSince.vue create mode 100644 docs/.vitepress/theme/components/index.ts diff --git a/docs/.vitepress/availableSinceMarkdownPlugin.ts b/docs/.vitepress/availableSinceMarkdownPlugin.ts new file mode 100644 index 00000000..a19f0be7 --- /dev/null +++ b/docs/.vitepress/availableSinceMarkdownPlugin.ts @@ -0,0 +1,60 @@ +import MarkdownIt from 'markdown-it' + +export interface AvailableSinceParams { + rails?: string + core?: string + description?: string +} + +function parseAvailableSinceParams(info: string): AvailableSinceParams { + const basicMatch = info.trim().match(/^available_since(?:\s+(.*))?$/) + if (!basicMatch) return {} + + const allParams = basicMatch[1] || '' + const params: AvailableSinceParams = {} + + // Parse out key=value pairs first + const keyValueMatches = [...allParams.matchAll(/([a-z]+)(?:=("[^"]*"|[^\s"]+))?/g)] + for (const [, key, value] of keyValueMatches) { + let cleanValue = value ? value.replace(/^"|"$/g, '') : true + + if (key === 'rails') params.rails = cleanValue as string + if (key === 'core') params.core = cleanValue as string + if (key === 'description') params.description = cleanValue as string + } + + return params +} + +export function availableSinceMarkdownPlugin(md: MarkdownIt) { + md.block.ruler.before('paragraph', 'available_since_oneliner', (state, start, end, silent) => { + const line = state.getLines(start, start + 1, 0, false).trim() + + const match = line.match(/^@available_since\s+(.+)$/) + if (!match) return false + + if (silent) return true + + const params = parseAvailableSinceParams(`available_since ${match[1]}`) + const token = state.push('available_since_oneliner', '', 0) + + token.content = renderAvailableSince(params, md) + token.map = [start, start + 1] + + state.line = start + 1 + return true + }) + + // Render the one-liner available_since token + md.renderer.rules.available_since_oneliner = (tokens, idx) => { + return tokens[idx].content + '\n' + } +} + +function renderAvailableSince(params: AvailableSinceParams, md: MarkdownIt): string { + const railsAttr = params.rails ? `rails="${md.utils.escapeHtml(params.rails)}"` : '' + const coreAttr = params.core ? `core="${md.utils.escapeHtml(params.core)}"` : '' + const descriptionAttr = params.description ? `description="${md.utils.escapeHtml(params.description)}"` : '' + + return `` +} diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index b1a60432..3111779b 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -1,4 +1,5 @@ import { defineConfig } from 'vitepress' +import { availableSinceMarkdownPlugin } from './availableSinceMarkdownPlugin' import { tabsMarkdownPlugin } from './vitepress-plugin-tabs/tabsMarkdownPlugin' const title = 'Inertia Rails' @@ -16,6 +17,7 @@ export default defineConfig({ markdown: { config(md) { md.use(tabsMarkdownPlugin) + md.use(availableSinceMarkdownPlugin) }, }, diff --git a/docs/.vitepress/theme/components/AvailableSince.vue b/docs/.vitepress/theme/components/AvailableSince.vue new file mode 100644 index 00000000..492a9839 --- /dev/null +++ b/docs/.vitepress/theme/components/AvailableSince.vue @@ -0,0 +1,257 @@ + + + + + diff --git a/docs/.vitepress/theme/components/index.ts b/docs/.vitepress/theme/components/index.ts new file mode 100644 index 00000000..1d3e8e6b --- /dev/null +++ b/docs/.vitepress/theme/components/index.ts @@ -0,0 +1,5 @@ +import AvailableSince from './AvailableSince.vue' + +export { + AvailableSince, +} diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index fc1accec..ab4fb10c 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -3,6 +3,7 @@ import type { Theme } from 'vitepress' import { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client' import DefaultTheme from 'vitepress/theme' import { h } from 'vue' +import { AvailableSince } from './components' import { setupFrameworksTabs } from './frameworksTabs' import './style.css' @@ -15,6 +16,7 @@ export default { }, enhanceApp({ app, router, siteData }) { enhanceAppWithTabs(app) + app.component('AvailableSince', AvailableSince) }, setup() { setupFrameworksTabs() diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 6667671c..9835df2d 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -40,11 +40,14 @@ Use `component_path_resolver` to customize component path resolution when [`defa ### `deep_merge_shared_data` +@available_since rails=3.8.0 + When enabled, props will be deep merged with shared data, combining hashes with the same keys instead of replacing them. **Default**: `false` + ### `default_render` Overrides Rails default rendering behavior to render using Inertia by default. @@ -53,6 +56,8 @@ Overrides Rails default rendering behavior to render using Inertia by default. ### `encrypt_history` +@available_since rails=3.7.0 core=2.0.0 + When enabled, you instruct Inertia to encrypt your app's history, it uses the browser's built-in [`crypto` api](https://developer.mozilla.org/en-US/docs/Web/API/Crypto) to encrypt the current page's data before pushing it to the history state. @@ -61,11 +66,13 @@ to encrypt the current page's data before pushing it to the history state. ### `ssr_enabled` _(experimental)_ +@available_since rails=3.6.0 core=2.0.0 + Whether to use a JavaScript server to pre-render your JavaScript pages, allowing your visitors to receive fully rendered HTML when they first visit your application. -Requires a JS server to be available at `ssr_url`. [_Example_](https://github.com/ElMassimo/inertia-rails-ssr-template) +Requires a JavaScript server to be available at `ssr_url`. [_Example_](https://github.com/ElMassimo/inertia-rails-ssr-template) **Default**: `false` diff --git a/docs/guide/deferred-props.md b/docs/guide/deferred-props.md index 7e4a3a29..c11a3135 100644 --- a/docs/guide/deferred-props.md +++ b/docs/guide/deferred-props.md @@ -1,5 +1,7 @@ # Deferred props +@available_since rails=3.6.0 core=2.0.0 + Inertia's deferred props feature allows you to defer the loading of certain page data until after the initial page render. This can be useful for improving the perceived performance of your app by allowing the initial page render to happen as quickly as possible. ## Server side diff --git a/docs/guide/merging-props.md b/docs/guide/merging-props.md index 172aa54b..4e08939c 100644 --- a/docs/guide/merging-props.md +++ b/docs/guide/merging-props.md @@ -4,22 +4,45 @@ By default, Inertia overwrites props with the same name when reloading a page. H ## Server side -> `deep_merge` requires `@inertiajs/core` v2.0.8 or higher, and `inertia_rails` v3.8.0 or higher. +### Using `merge` -To specify that a prop should be merged, use the `merge` or `deep_merge` method on the prop's value. +@available_since rails=3.8.0 core=2.0.8 -Use `merge` for merging simple arrays, and `deep_merge` for handling nested objects that include arrays or complex structures, such as pagination objects. +To specify that a prop should be merged, use the `merge` method on the prop's value. This is ideal for merging simple arrays. + +On the client side, Inertia detects that this prop should be merged. If the prop returns an array, it will append the response to the current prop value. If it's an object, it will merge the response with the current prop value. ```ruby class UsersController < ApplicationController include Pagy::Backend def index - pagy, records = pagy(User.all) + _pagy, records = pagy(User.all) render inertia: { # simple array: users: InertiaRails.merge { records.as_json(...) }, + # with match_on parameter for smart merging: + products: InertiaRails.merge(match_on: 'id') { Product.all.as_json(...) }, + } + end +end +``` + +### Using `deep_merge` + +@available_since rails=3.8.0 core=2.0.8 + +For handling nested objects that include arrays or complex structures, such as pagination objects, use the `deep_merge` method. + +```ruby +class UsersController < ApplicationController + include Pagy::Backend + + def index + pagy, records = pagy(User.all) + + render inertia: { # pagination object: data: InertiaRails.deep_merge { { @@ -27,8 +50,6 @@ class UsersController < ApplicationController pagy: pagy_metadata(pagy) } }, - # with match_on parameter for smart merging: - products: InertiaRails.merge(match_on: 'id') { Product.all.as_json(...) }, # nested objects with match_on: categories: InertiaRails.deep_merge(match_on: %w[items.id tags.id]) { { @@ -41,10 +62,12 @@ class UsersController < ApplicationController end ``` -On the client side, Inertia detects that this prop should be merged. If the prop returns an array, it will append the response to the current prop value. If it's an object, it will merge the response with the current prop value. If you have opted to `deepMerge`, Inertia ensures a deep merge of the entire structure. +If you have opted to use `deep_merge`, Inertia ensures a deep merge of the entire structure, including nested objects and arrays. ### Smart merging with `match_on` +@available_since rails=master core=2.0.13 + By default, arrays are simply appended during merging. If you need to update specific items in an array or replace them based on a unique identifier, you can use the `match_on` parameter. The `match_on` parameter enables smart merging by specifying a field to match on when merging arrays of objects: