Skip to content

Commit ac3fdb8

Browse files
authored
fix: deleting a primitive value in an array now works (#635)
1 parent 039f6b8 commit ac3fdb8

File tree

5 files changed

+354
-5
lines changed

5 files changed

+354
-5
lines changed

Diff for: packages/form-core/src/util-types.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ export type DeepKeys<T, TDepth extends any[] = []> = TDepth['length'] extends 5
7878
? never
7979
: T extends object
8080
? PrefixObjectAccessor<T, TDepth>
81-
: never
81+
: T extends string | number | boolean | bigint
82+
? ''
83+
: never
8284

8385
type PrefixFromDepth<
8486
T extends string | number,

Diff for: packages/form-core/src/utils.ts

+3
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ export function deleteBy(obj: any, _path: any) {
7979
if (!parent) return
8080
if (path.length === 1) {
8181
const finalPath = path[0]!
82+
if (Array.isArray(parent) && typeof finalPath === 'number') {
83+
return parent.filter((_, i) => i !== finalPath)
84+
}
8285
const { [finalPath]: remove, ...rest } = parent
8386
return rest
8487
}

Diff for: packages/react-form/src/tests/useField.test.tsx

+115
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,102 @@ describe('useField', () => {
608608
expect(queryByText('A first name is required')).not.toBeInTheDocument()
609609
})
610610

611+
it('should handle arrays with primitive values', async () => {
612+
const fn = vi.fn()
613+
function Comp() {
614+
const form = useForm({
615+
defaultValues: {
616+
people: [] as Array<string>,
617+
},
618+
onSubmit: ({ value }) => fn(value),
619+
})
620+
621+
return (
622+
<div>
623+
<form
624+
onSubmit={(e) => {
625+
e.preventDefault()
626+
e.stopPropagation()
627+
void form.handleSubmit()
628+
}}
629+
>
630+
<form.Field name="people">
631+
{(field) => {
632+
return (
633+
<div>
634+
{field.state.value.map((_, i) => {
635+
return (
636+
<form.Field key={i} name={`people[${i}]`}>
637+
{(subField) => {
638+
return (
639+
<div>
640+
<label>
641+
<div>Name for person {i}</div>
642+
<input
643+
value={subField.state.value}
644+
onChange={(e) =>
645+
subField.handleChange(e.target.value)
646+
}
647+
/>
648+
</label>
649+
<button
650+
onClick={() => field.removeValue(i)}
651+
type="button"
652+
>
653+
Remove person {i}
654+
</button>
655+
</div>
656+
)
657+
}}
658+
</form.Field>
659+
)
660+
})}
661+
<button onClick={() => field.pushValue('')} type="button">
662+
Add person
663+
</button>
664+
</div>
665+
)
666+
}}
667+
</form.Field>
668+
<form.Subscribe
669+
selector={(state) => [state.canSubmit, state.isSubmitting]}
670+
children={([canSubmit, isSubmitting]) => (
671+
<button type="submit" disabled={!canSubmit}>
672+
{isSubmitting ? '...' : 'Submit'}
673+
</button>
674+
)}
675+
/>
676+
</form>
677+
</div>
678+
)
679+
}
680+
681+
const { getByText, findByLabelText, queryByText, findByText } = render(
682+
<Comp />,
683+
)
684+
685+
expect(queryByText('Name for person 0')).not.toBeInTheDocument()
686+
expect(queryByText('Name for person 1')).not.toBeInTheDocument()
687+
await user.click(getByText('Add person'))
688+
const input = await findByLabelText('Name for person 0')
689+
expect(input).toBeInTheDocument()
690+
await user.type(input, 'John')
691+
692+
await user.click(getByText('Add person'))
693+
const input2 = await findByLabelText('Name for person 1')
694+
expect(input).toBeInTheDocument()
695+
await user.type(input2, 'Jack')
696+
697+
expect(queryByText('Name for person 0')).toBeInTheDocument()
698+
expect(queryByText('Name for person 1')).toBeInTheDocument()
699+
await user.click(getByText('Remove person 1'))
700+
expect(queryByText('Name for person 0')).toBeInTheDocument()
701+
expect(queryByText('Name for person 1')).not.toBeInTheDocument()
702+
703+
await user.click(await findByText('Submit'))
704+
expect(fn).toHaveBeenCalledWith({ people: ['John'] })
705+
})
706+
611707
it('should handle arrays with subvalues', async () => {
612708
const fn = vi.fn()
613709
function Comp() {
@@ -646,6 +742,12 @@ describe('useField', () => {
646742
}
647743
/>
648744
</label>
745+
<button
746+
onClick={() => field.removeValue(i)}
747+
type="button"
748+
>
749+
Remove person {i}
750+
</button>
649751
</div>
650752
)
651753
}}
@@ -680,10 +782,23 @@ describe('useField', () => {
680782
)
681783

682784
expect(queryByText('Name for person 0')).not.toBeInTheDocument()
785+
expect(queryByText('Name for person 1')).not.toBeInTheDocument()
683786
await user.click(getByText('Add person'))
684787
const input = await findByLabelText('Name for person 0')
685788
expect(input).toBeInTheDocument()
686789
await user.type(input, 'John')
790+
791+
await user.click(getByText('Add person'))
792+
const input2 = await findByLabelText('Name for person 1')
793+
expect(input).toBeInTheDocument()
794+
await user.type(input2, 'Jack')
795+
796+
expect(queryByText('Name for person 0')).toBeInTheDocument()
797+
expect(queryByText('Name for person 1')).toBeInTheDocument()
798+
await user.click(getByText('Remove person 1'))
799+
expect(queryByText('Name for person 0')).toBeInTheDocument()
800+
expect(queryByText('Name for person 1')).not.toBeInTheDocument()
801+
687802
await user.click(await findByText('Submit'))
688803
expect(fn).toHaveBeenCalledWith({ people: [{ name: 'John', age: 0 }] })
689804
})

Diff for: packages/solid-form/src/tests/createField.test.tsx

+114-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest'
22
import { render, waitFor } from '@solidjs/testing-library'
33
import userEvent from '@testing-library/user-event'
44
import '@testing-library/jest-dom/vitest'
5-
import { Index, Show } from 'solid-js'
5+
import { Index, Show, createEffect } from 'solid-js'
66
import { createForm, createFormFactory } from '../index'
77
import { sleep } from './utils'
88

@@ -386,6 +386,99 @@ describe('createField', () => {
386386
expect(getByText(error)).toBeInTheDocument()
387387
})
388388

389+
it('should handle arrays with primitive values', async () => {
390+
const fn = vi.fn()
391+
392+
function Comp() {
393+
const form = createForm(() => ({
394+
defaultValues: {
395+
people: [] as Array<string>,
396+
},
397+
onSubmit: ({ value }) => fn(value),
398+
}))
399+
return (
400+
<div>
401+
<form
402+
onSubmit={(e) => {
403+
e.preventDefault()
404+
e.stopPropagation()
405+
void form.handleSubmit()
406+
}}
407+
>
408+
<form.Field name="people">
409+
{(field) => (
410+
<div>
411+
<Show when={field().state.value.length > 0}>
412+
{/* Do not change this to For or the test will fail */}
413+
<Index each={field().state.value}>
414+
{(_, i) => {
415+
return (
416+
<form.Field name={`people[${i}]`}>
417+
{(subField) => (
418+
<div>
419+
<label>
420+
<div>Name for person {i}</div>
421+
<input
422+
value={subField().state.value}
423+
onInput={(e) => {
424+
subField().handleChange(
425+
e.currentTarget.value,
426+
)
427+
}}
428+
/>
429+
</label>
430+
<button
431+
onClick={() => field().removeValue(i)}
432+
type="button"
433+
>
434+
Remove person {i}
435+
</button>
436+
</div>
437+
)}
438+
</form.Field>
439+
)
440+
}}
441+
</Index>
442+
</Show>
443+
444+
<button onClick={() => field().pushValue('')} type="button">
445+
Add person
446+
</button>
447+
</div>
448+
)}
449+
</form.Field>
450+
<button type="submit">Submit</button>
451+
</form>
452+
</div>
453+
)
454+
}
455+
456+
const { getByText, findByLabelText, queryByText, findByText } = render(
457+
() => <Comp />,
458+
)
459+
460+
expect(queryByText('Name for person 0')).not.toBeInTheDocument()
461+
expect(queryByText('Name for person 1')).not.toBeInTheDocument()
462+
await user.click(getByText('Add person'))
463+
const input = await findByLabelText('Name for person 0')
464+
expect(input).toBeInTheDocument()
465+
await user.type(input, 'John')
466+
467+
await user.click(getByText('Add person'))
468+
const input2 = await findByLabelText('Name for person 1')
469+
expect(input).toBeInTheDocument()
470+
await user.type(input2, 'Jack')
471+
472+
expect(queryByText('Name for person 0')).toBeInTheDocument()
473+
expect(queryByText('Name for person 1')).toBeInTheDocument()
474+
await user.click(getByText('Remove person 1'))
475+
expect(queryByText('Name for person 0')).toBeInTheDocument()
476+
expect(queryByText('Name for person 1')).not.toBeInTheDocument()
477+
478+
await user.click(await findByText('Submit'))
479+
expect(fn).toHaveBeenCalledWith({ people: ['John'] })
480+
})
481+
389482
it('should handle arrays with subvalues', async () => {
390483
const fn = vi.fn()
391484

@@ -428,6 +521,12 @@ describe('createField', () => {
428521
}}
429522
/>
430523
</label>
524+
<button
525+
onClick={() => field().removeValue(i)}
526+
type="button"
527+
>
528+
Remove person {i}
529+
</button>
431530
</div>
432531
)}
433532
</form.Field>
@@ -456,13 +555,24 @@ describe('createField', () => {
456555
)
457556

458557
expect(queryByText('Name for person 0')).not.toBeInTheDocument()
558+
expect(queryByText('Name for person 1')).not.toBeInTheDocument()
459559
await user.click(getByText('Add person'))
460560
const input = await findByLabelText('Name for person 0')
461561
expect(input).toBeInTheDocument()
462562
await user.type(input, 'John')
563+
564+
await user.click(getByText('Add person'))
565+
const input2 = await findByLabelText('Name for person 1')
566+
expect(input).toBeInTheDocument()
567+
await user.type(input2, 'Jack')
568+
569+
expect(queryByText('Name for person 0')).toBeInTheDocument()
570+
expect(queryByText('Name for person 1')).toBeInTheDocument()
571+
await user.click(getByText('Remove person 1'))
572+
expect(queryByText('Name for person 0')).toBeInTheDocument()
573+
expect(queryByText('Name for person 1')).not.toBeInTheDocument()
574+
463575
await user.click(await findByText('Submit'))
464-
expect(fn).toHaveBeenCalledWith({
465-
people: [{ name: 'John', age: 0 }],
466-
})
576+
expect(fn).toHaveBeenCalledWith({ people: [{ name: 'John', age: 0 }] })
467577
})
468578
})

0 commit comments

Comments
 (0)