From ec94e0a82c82a85abf3b90b3ec992cb07bf76d6e Mon Sep 17 00:00:00 2001 From: basteln3rk Date: Mon, 11 Aug 2025 20:59:54 +0000 Subject: [PATCH 1/3] feat: add dynamic form property support to useForm adapters and tests --- .../react/test-app/Pages/FormHelper/Data.jsx | 39 ++++++++++++++ packages/svelte/src/useForm.ts | 19 +++++-- .../test-app/Pages/FormHelper/Data.svelte | 30 +++++++++++ packages/vue3/src/useForm.ts | 11 +++- .../vue3/test-app/Pages/FormHelper/Data.vue | 26 ++++++++++ tests/form-component.spec.ts | 51 ++++++++++++++++++- 6 files changed, 168 insertions(+), 8 deletions(-) diff --git a/packages/react/test-app/Pages/FormHelper/Data.jsx b/packages/react/test-app/Pages/FormHelper/Data.jsx index ac18e1bb3..bb6f3f154 100644 --- a/packages/react/test-app/Pages/FormHelper/Data.jsx +++ b/packages/react/test-app/Pages/FormHelper/Data.jsx @@ -1,10 +1,12 @@ import { useForm, usePage } from '@inertiajs/react' +import { useEffect, useState } from 'react' export default (props) => { const form = useForm({ name: 'foo', handle: 'example', remember: false, + custom: {}, }) const page = usePage() @@ -33,6 +35,23 @@ export default (props) => { form.setDefaults('name', 'single value') } + const addCustomOtherProp = () => { + form.setData((prevData) => ({ + ...prevData, + custom: { + ...prevData.custom, + other_prop: 'dynamic_value', + }, + })) + } + + const [formDataOutput, setFormDataOutput] = useState('') + + // Effect to watch form.data and update formDataOutput + useEffect(() => { + setFormDataOutput(JSON.stringify(form.data)) + }, [form.data]) + return (
{form.errors.remember && {form.errors.remember}} + + {form.errors.accept_tos && {form.errors.accept_tos}} + @@ -90,7 +121,15 @@ export default (props) => { Reassign single default + + Form has {form.hasErrors ? '' : 'no '}errors + +
+ {formDataOutput} +
) } diff --git a/packages/svelte/src/useForm.ts b/packages/svelte/src/useForm.ts index 83992854a..d7f096c69 100644 --- a/packages/svelte/src/useForm.ts +++ b/packages/svelte/src/useForm.ts @@ -70,8 +70,10 @@ export default function useForm( let recentlySuccessfulTimeoutId: ReturnType | null = null let transform = (data: TForm) => data as object - const store = writable>({ - ...(restored ? restored.data : data), + const initialData = restored ? restored.data : cloneDeep(defaults) + + const formObject: InertiaForm = { + ...initialData, isDirty: false, errors: restored ? restored.errors : {}, hasErrors: false, @@ -85,7 +87,10 @@ export default function useForm( }) }, data() { - return Object.keys(data).reduce((carry, key) => { + return (Object.keys(this) as Array>).reduce((carry, key) => { + if (RESERVED_KEYS.includes(key)) { + return carry + } return set(carry, key, get(this, key)) }, {} as FormDataType) as TForm }, @@ -253,9 +258,13 @@ export default function useForm( cancel() { cancelToken?.cancel() }, - } as InertiaForm) + } as InertiaForm + + const store = writable>(formObject) + + const RESERVED_KEYS = Object.keys(formObject).filter((key) => !(key in initialData)) - store.subscribe((form) => { + store.subscribe((form: InertiaForm) => { if (form.isDirty === isEqual(form.data(), defaults)) { form.setStore('isDirty', !form.isDirty) } diff --git a/packages/svelte/test-app/Pages/FormHelper/Data.svelte b/packages/svelte/test-app/Pages/FormHelper/Data.svelte index ebeb1ca5f..446f20f52 100644 --- a/packages/svelte/test-app/Pages/FormHelper/Data.svelte +++ b/packages/svelte/test-app/Pages/FormHelper/Data.svelte @@ -5,6 +5,7 @@ name: 'foo', handle: 'example', remember: false, + custom: {}, }) const submit = () => { @@ -33,6 +34,22 @@ const reassignSingle = () => { $form.defaults('name', 'single value') } + + // New functions for dynamic properties + const addAcceptTos = () => { + $form.accept_tos = true // Add root-level dynamic property + } + + const addCustomOtherProp = () => { + $form.custom.other_prop = 'dynamic_value' // Add nested dynamic property + } + + // Reactive property to hold the stringified form.data() output + import { writable } from 'svelte/store' + const formDataOutput = writable('') + + // Watch $form.data() and update formDataOutput + $: formDataOutput.set(JSON.stringify($form.data()))
@@ -58,6 +75,14 @@ {$form.errors.remember} {/if} + + {#if $form.errors.accept_tos} + {$form.errors.accept_tos} + {/if} + @@ -67,5 +92,10 @@ + + Form has {$form.hasErrors ? '' : 'no '}errors + + +
diff --git a/packages/vue3/src/useForm.ts b/packages/vue3/src/useForm.ts index 89b5541bc..1b971a458 100644 --- a/packages/vue3/src/useForm.ts +++ b/packages/vue3/src/useForm.ts @@ -54,8 +54,10 @@ export default function useForm( let recentlySuccessfulTimeoutId = null let transform = (data) => data + const initialData = restored ? restored.data : cloneDeep(defaults) + const form = reactive({ - ...(restored ? restored.data : cloneDeep(defaults)), + ...initialData, isDirty: false, errors: restored ? restored.errors : {}, hasErrors: false, @@ -64,7 +66,10 @@ export default function useForm( wasSuccessful: false, recentlySuccessful: false, data() { - return (Object.keys(defaults) as Array>).reduce((carry, key) => { + return (Object.keys(this) as Array>).reduce((carry, key) => { + if (RESERVED_KEYS.includes(key)) { + return carry + } return set(carry, key, get(this, key)) }, {} as Partial) as TForm }, @@ -249,6 +254,8 @@ export default function useForm( }, }) + const RESERVED_KEYS = Object.keys(form).filter((key) => !(key in initialData)) + watch( form, (newValue) => { diff --git a/packages/vue3/test-app/Pages/FormHelper/Data.vue b/packages/vue3/test-app/Pages/FormHelper/Data.vue index 573136810..02c753ed9 100644 --- a/packages/vue3/test-app/Pages/FormHelper/Data.vue +++ b/packages/vue3/test-app/Pages/FormHelper/Data.vue @@ -1,10 +1,12 @@ diff --git a/tests/form-component.spec.ts b/tests/form-component.spec.ts index a61b0b730..3dc6b6c3c 100644 --- a/tests/form-component.spec.ts +++ b/tests/form-component.spec.ts @@ -109,6 +109,56 @@ test.describe('Form Component', () => { }) }) + test.describe('Dynamic Properties', () => { + test.beforeEach(async ({ page }) => { + pageLoads.watch(page) + await page.goto('/form-helper/data') // Navigate to the FormHelper/Data page + }) + + test('initial data() output contains only initial properties', async ({ page }) => { + const formDataOutput = await page.locator('#form-data-output').innerText() + const data = JSON.parse(formDataOutput) + + expect(data).toEqual({ + name: 'foo', + handle: 'example', + remember: false, + custom: {}, + }) + }) + + test('data() output includes root-level dynamic property', async ({ page }) => { + await page.check('input[name="accept_tos"]') + + const formDataOutput = await page.locator('#form-data-output').innerText() + const data = JSON.parse(formDataOutput) + + expect(data).toEqual({ + name: 'foo', + handle: 'example', + remember: false, + custom: {}, + accept_tos: true, + }) + }) + + test('data() output includes nested dynamic property', async ({ page }) => { + await page.getByRole('button', { name: 'Add custom.other_prop' }).click() + + const formDataOutput = await page.locator('#form-data-output').innerText() + const data = JSON.parse(formDataOutput) + + expect(data).toEqual({ + name: 'foo', + handle: 'example', + remember: false, + custom: { + other_prop: 'dynamic_value', + }, + }) + }) + }) + test.describe('Headers', () => { test.beforeEach(async ({ page }) => { pageLoads.watch(page) @@ -653,6 +703,5 @@ test.describe('Form Component', () => { }, }) }) - }) }) From fe1abdc2efce48ddb04ac78f2be6c39d14ed2ff3 Mon Sep 17 00:00:00 2001 From: Hinnerk Gnutzmann Date: Thu, 14 Aug 2025 15:07:36 +0200 Subject: [PATCH 2/3] vue: move dynamic components test to dedicated file (which uses javascript) --- .../vue3/test-app/Pages/FormHelper/Data.vue | 30 +---------- .../test-app/Pages/FormHelper/DataDynamic.vue | 54 +++++++++++++++++++ tests/app/server.js | 19 +++++-- tests/form-component.spec.ts | 2 +- 4 files changed, 73 insertions(+), 32 deletions(-) create mode 100644 packages/vue3/test-app/Pages/FormHelper/DataDynamic.vue diff --git a/packages/vue3/test-app/Pages/FormHelper/Data.vue b/packages/vue3/test-app/Pages/FormHelper/Data.vue index 99335efcd..573136810 100644 --- a/packages/vue3/test-app/Pages/FormHelper/Data.vue +++ b/packages/vue3/test-app/Pages/FormHelper/Data.vue @@ -1,12 +1,10 @@ - diff --git a/packages/vue3/test-app/Pages/FormHelper/DataDynamic.vue b/packages/vue3/test-app/Pages/FormHelper/DataDynamic.vue new file mode 100644 index 000000000..36a862cbb --- /dev/null +++ b/packages/vue3/test-app/Pages/FormHelper/DataDynamic.vue @@ -0,0 +1,54 @@ + + + diff --git a/tests/app/server.js b/tests/app/server.js index 0a8c769e6..fc8f99e65 100644 --- a/tests/app/server.js +++ b/tests/app/server.js @@ -157,6 +157,13 @@ app.post('/form-helper/data', (req, res) => }), ) +app.post('/form-helper/data-dynamic', (req, res) => + inertia.render(req, res, { + component: 'FormHelper/DataDynamic', + props: {}, + }), +) + app.get('/form-helper/nested', (req, res) => inertia.render(req, res, { component: 'FormHelper/Nested', @@ -549,11 +556,17 @@ app.post('/form-component/progress', async (req, res) => setTimeout(() => inertia.render(req, res, { component: 'FormComponent/Progress' }), 500), ) app.get('/form-component/state', (req, res) => inertia.render(req, res, { component: 'FormComponent/State' })) -app.get('/form-component/dotted-keys', (req, res) => inertia.render(req, res, { component: 'FormComponent/DottedKeys' })) +app.get('/form-component/dotted-keys', (req, res) => + inertia.render(req, res, { component: 'FormComponent/DottedKeys' }), +) app.get('/form-component/ref', (req, res) => inertia.render(req, res, { component: 'FormComponent/Ref' })) -app.get('/form-component/uppercase-method', (req, res) => inertia.render(req, res, { component: 'FormComponent/UppercaseMethod' })) +app.get('/form-component/uppercase-method', (req, res) => + inertia.render(req, res, { component: 'FormComponent/UppercaseMethod' }), +) -app.get('/form-component/url/with/segements', (req, res) => inertia.render(req, res, { component: 'FormComponent/EmptyAction' })) +app.get('/form-component/url/with/segements', (req, res) => + inertia.render(req, res, { component: 'FormComponent/EmptyAction' }), +) app.post('/form-component/url/with/segements', async (req, res) => inertia.render(req, res, { component: 'FormComponent/EmptyAction', diff --git a/tests/form-component.spec.ts b/tests/form-component.spec.ts index 0febb40d2..72937b35f 100644 --- a/tests/form-component.spec.ts +++ b/tests/form-component.spec.ts @@ -112,7 +112,7 @@ test.describe('Form Component', () => { test.describe('Dynamic Properties', () => { test.beforeEach(async ({ page }) => { pageLoads.watch(page) - await page.goto('/form-helper/data') // Navigate to the FormHelper/Data page + await page.goto('/form-helper/data-dynamic') }) test('initial data() output contains only initial properties', async ({ page }) => { From 3a37751869bf665101b0c6d328669be8eb5ad808 Mon Sep 17 00:00:00 2001 From: Hinnerk Gnutzmann Date: Thu, 14 Aug 2025 15:35:51 +0200 Subject: [PATCH 3/3] restore Data.vue from master --- packages/vue3/test-app/Pages/FormHelper/Data.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vue3/test-app/Pages/FormHelper/Data.vue b/packages/vue3/test-app/Pages/FormHelper/Data.vue index 573136810..e2f4b0402 100644 --- a/packages/vue3/test-app/Pages/FormHelper/Data.vue +++ b/packages/vue3/test-app/Pages/FormHelper/Data.vue @@ -1,10 +1,10 @@ -