Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/public-knives-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/svelte-form': patch
---

prevent full array re-renders in array mode
2 changes: 1 addition & 1 deletion docs/framework/svelte/guides/arrays.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Finally, you can use a subfield like so:
form.handleSubmit()
}}
>
<form.Field name="people">
<form.Field name="people" mode="array">
{#snippet children(field)}
<div>
{#each field.state.value as person, i}
Expand Down
2 changes: 1 addition & 1 deletion examples/svelte/array/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
>
<h1>TanStack Form - Svelte Demo</h1>

<form.Field name="people">
<form.Field name="people" mode="array">
{#snippet children(field)}
<div>
{#each field.state.value as person, i}
Expand Down
53 changes: 51 additions & 2 deletions packages/svelte-form/src/Field.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,59 @@
api.update(current)
})

const storeSub = useStore(api.store)
const storeSub = useStore(api.store, (state) =>
options.mode === 'array'
? Object.keys((state.value as unknown) ?? []).length
: state.value,
)
const metaIsTouchedSub = useStore(
api.store,
(state) => state.meta.isTouched,
)
const metaIsBlurredSub = useStore(
api.store,
(state) => state.meta.isBlurred,
)
const metaIsDirtySub = useStore(api.store, (state) => state.meta.isDirty)
const metaErrorMapSub = useStore(api.store, (state) => state.meta.errorMap)
const metaErrorSourceMapSub = useStore(
api.store,
(state) => state.meta.errorSourceMap,
)
const metaIsValidatingSub = useStore(
api.store,
(state) => state.meta.isValidating,
)
Object.defineProperty(extendedApi, 'state', {
get() {
return storeSub.current
// Read all reactive sources to track them as dependencies. For array
// mode, `storeSub.current` is the array length so we still pull the
// actual value from the underlying api state.
// See: https://github.com/TanStack/form/issues/1961
// Note: we read from `api.store.state` (not `api.state`) to avoid
// infinite recursion since this getter shadows the prototype's `state`
// getter on the same object.
const trackedValue = storeSub.current
const isTouched = metaIsTouchedSub.current
const isBlurred = metaIsBlurredSub.current
const isDirty = metaIsDirtySub.current
const errorMap = metaErrorMapSub.current
const errorSourceMap = metaErrorSourceMapSub.current
const isValidating = metaIsValidatingSub.current
const baseState = api.store.state
return {
...baseState,
value: options.mode === 'array' ? baseState.value : trackedValue,
meta: {
...baseState.meta,
isTouched,
isBlurred,
isDirty,
errorMap,
errorSourceMap,
isValidating,
},
}
},
})

Expand Down
20 changes: 20 additions & 0 deletions packages/svelte-form/tests/array.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<svelte:options runes />

<script lang="ts">
import { createForm } from '../src/index.js'

const form = createForm(() => ({
defaultValues: {
test: ['a'] as string[],
},
}))
</script>

<form.Field name="test" mode="array">
{#snippet children(field)}
<div id="val">{JSON.stringify(field.state.value)}</div>
<button id="push" type="button" onclick={() => field.pushValue('b')}>
push
</button>
{/snippet}
</form.Field>
27 changes: 27 additions & 0 deletions packages/svelte-form/tests/array.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { userEvent } from '@testing-library/user-event'
import { mount, unmount } from 'svelte'
import TestForm from './array.svelte'

describe('Svelte Field array mode', () => {
let element: HTMLDivElement
let instance: any
beforeEach(async () => {
element = document.createElement('div')
document.body.appendChild(element)
instance = mount(TestForm, {
target: element,
})
})

afterEach(() => {
unmount(instance)
element.remove()
})

it('should support array mode', async () => {
expect(element.querySelector('#val')!.textContent).toBe('["a"]')
await userEvent.click(element.querySelector<HTMLButtonElement>('#push')!)
expect(element.querySelector('#val')!.textContent).toBe('["a","b"]')
})
})
Loading