diff --git a/packages/core/src/response.ts b/packages/core/src/response.ts index c02f72281..6db2fc50b 100644 --- a/packages/core/src/response.ts +++ b/packages/core/src/response.ts @@ -207,6 +207,7 @@ export class Response { protected mergeProps(pageResponse: Page): void { if (this.requestParams.isPartial() && pageResponse.component === currentPage.get().component) { const propsToMerge = pageResponse.mergeProps || [] + const propsToDeepMerge = pageResponse.deepMergeProps || [] propsToMerge.forEach((prop) => { const incomingProp = pageResponse.props[prop] @@ -221,6 +222,31 @@ export class Response { } }) + propsToDeepMerge.forEach((prop) => { + const incomingProp = pageResponse.props[prop]; + const currentProp = currentPage.get().props[prop]; + + // Deep merge function to handle nested objects and arrays + const deepMerge = (target: any, source: any) => { + if (Array.isArray(source)) { + // Merge arrays by concatenating the existing and incoming elements + return [...(Array.isArray(target) ? target : []), ...source]; + } else if (typeof source === 'object' && source !== null) { + // Merge objects by iterating over keys + return Object.keys(source).reduce((acc, key) => { + acc[key] = deepMerge(target ? target[key] : undefined, source[key]); + return acc; + }, { ...target }); + } + // If the source is neither an array nor an object, return it directly + return source; + }; + + // Assign the deeply merged result back to props. + pageResponse.props[prop] = deepMerge(currentProp, incomingProp); + }); + + pageResponse.props = { ...currentPage.get().props, ...pageResponse.props } } } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 8537b269b..69dc49aa8 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -42,6 +42,7 @@ export interface Page { encryptHistory: boolean deferredProps?: Record mergeProps?: string[] + deepMergeProps?: string[] /** @internal */ rememberedState: Record diff --git a/packages/vue3/src/app.ts b/packages/vue3/src/app.ts index 365465083..4e7a39c34 100755 --- a/packages/vue3/src/app.ts +++ b/packages/vue3/src/app.ts @@ -135,6 +135,7 @@ export function usePage(): Page { clearHistory: computed(() => page.value?.clearHistory), deferredProps: computed(() => page.value?.deferredProps), mergeProps: computed(() => page.value?.mergeProps), + deepMergeProps: computed(() => page.value?.deepMergeProps), rememberedState: computed(() => page.value?.rememberedState), encryptHistory: computed(() => page.value?.encryptHistory), }) diff --git a/tests/app/server.js b/tests/app/server.js index f68696c71..fda80f67d 100644 --- a/tests/app/server.js +++ b/tests/app/server.js @@ -245,14 +245,25 @@ app.get('/when-visible', (req, res) => { }) app.get('/merge-props', (req, res) => { - inertia.render(req, res, { - component: 'MergeProps', - props: { - bar: new Array(5).fill(1), - foo: new Array(5).fill(1), - }, - ...(req.headers['x-inertia-reset'] ? {} : { mergeProps: ['foo'] }), - }) + inertia.render(req, res, { + component: 'MergeProps', + props: { + bar: new Array(5).fill(1), + foo: new Array(5).fill(1), + }, + ...(req.headers['x-inertia-reset'] ? {} : { mergeProps: ['foo'] }), + }) +}) + +app.get('/deep-merge-props', (req, res) => { + inertia.render(req, res, { + component: 'MergeProps', + props: { + bar: new Array(5).fill(1), + foo: new Array(5).fill(1), + }, + ...(req.headers['x-inertia-reset'] ? {} : { deepMergeProps: ['foo'] }), + }) }) app.get('/deferred-props/page-1', (req, res) => { diff --git a/tests/deep-merge-props.spec.ts b/tests/deep-merge-props.spec.ts new file mode 100644 index 000000000..40217ca39 --- /dev/null +++ b/tests/deep-merge-props.spec.ts @@ -0,0 +1,29 @@ +import { expect, test } from '@playwright/test' +import { clickAndWaitForResponse } from './support' + +test('can deep merge props', async ({ page }) => { + await page.goto('/deep-merge-props') + + await expect(page.getByText('bar count is 5')).toBeVisible() + await expect(page.getByText('foo count is 5')).toBeVisible() + + await clickAndWaitForResponse(page, 'Reload', null, 'button') + + await expect(page.getByText('bar count is 5')).toBeVisible() + await expect(page.getByText('foo count is 10')).toBeVisible() + + await clickAndWaitForResponse(page, 'Reload', null, 'button') + + await expect(page.getByText('bar count is 5')).toBeVisible() + await expect(page.getByText('foo count is 15')).toBeVisible() + + await clickAndWaitForResponse(page, 'Get Fresh', null, 'button') + + await expect(page.getByText('bar count is 5')).toBeVisible() + await expect(page.getByText('foo count is 5')).toBeVisible() + + await clickAndWaitForResponse(page, 'Reload', null, 'button') + + await expect(page.getByText('bar count is 5')).toBeVisible() + await expect(page.getByText('foo count is 10')).toBeVisible() +})