diff --git a/packages/react/test-app/Pages/FormHelper/Data.tsx b/packages/react/test-app/Pages/FormHelper/Data.tsx
index a867ee549..26b408afb 100644
--- a/packages/react/test-app/Pages/FormHelper/Data.tsx
+++ b/packages/react/test-app/Pages/FormHelper/Data.tsx
@@ -1,10 +1,12 @@
import { useForm, usePage } from '@inertiajs/react'
+import { useEffect, useState } from 'react'
export default ({ errors }: { errors?: { name?: string; handle?: string } }) => {
const form = useForm({
name: 'foo',
handle: 'example',
remember: false,
+ custom: {},
})
const page = usePage()
@@ -33,6 +35,23 @@ export default ({ errors }: { errors?: { name?: string; handle?: string } }) =>
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 ({ errors }: { errors?: { name?: string; handle?: string } }) =>
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 6d8df8095..8ee7fe611 100644
--- a/packages/svelte/src/useForm.ts
+++ b/packages/svelte/src/useForm.ts
@@ -74,8 +74,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 : {}) as FormDataErrors,
hasErrors: false,
@@ -89,7 +91,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 TForm)
},
@@ -257,9 +262,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 81f827a0b..776280969 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
+
+
+
{$formDataOutput}
diff --git a/packages/vue3/src/useForm.ts b/packages/vue3/src/useForm.ts
index bbb0f58e5..b6c107547 100644
--- a/packages/vue3/src/useForm.ts
+++ b/packages/vue3/src/useForm.ts
@@ -63,8 +63,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 : {}) as FormDataErrors,
hasErrors: false,
@@ -73,7 +75,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
},
@@ -258,6 +263,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/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 @@
+
+
+
+
+
+
{{ form.errors.name }}
+
+
+
{{ form.errors.accept_tos }}
+
+
+
+
+
+
{{ formDataOutput.json }}
+
+
diff --git a/tests/app/server.js b/tests/app/server.js
index 27efa3255..2cea7a432 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',
diff --git a/tests/form-component.spec.ts b/tests/form-component.spec.ts
index 3a2f25c4e..82589e839 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-dynamic')
+ })
+
+ 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)