Skip to content

Commit 2269b48

Browse files
rdjanuarbenjamincanac
authored andcommitted
fix(Form): handling race condition on clear function (#4843)
1 parent ae6f625 commit 2269b48

File tree

2 files changed

+77
-9
lines changed

2 files changed

+77
-9
lines changed

src/runtime/components/Form.vue

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -201,15 +201,21 @@ async function _validate<T extends boolean>(opts: ValidateOpts<boolean, boolean>
201201
: []
202202
203203
if (names) {
204-
const otherErrors = errors.value.filter(error => !names.some((name) => {
205-
const pattern = inputs.value?.[name]?.pattern
206-
return name === error.name || (pattern && error.name?.match(pattern))
207-
}))
208-
209-
const pathErrors = (await getErrors()).filter(error => names.some((name) => {
210-
const pattern = inputs.value?.[name]?.pattern
211-
return name === error.name || (pattern && error.name?.match(pattern))
212-
}))
204+
const namesSet = new Set(names)
205+
const patterns = names
206+
.map(name => inputs.value?.[name]?.pattern)
207+
.filter(Boolean) as RegExp[]
208+
209+
const isErrorForPath = (error: FormErrorWithId): boolean => {
210+
if (!error.name) return false
211+
if (namesSet.has(error.name)) return true
212+
return patterns.some(pattern => pattern.test(error.name!))
213+
}
214+
215+
const allNewErrors = await getErrors()
216+
217+
const otherErrors = errors.value.filter(error => !isErrorForPath(error))
218+
const pathErrors = allNewErrors.filter(isErrorForPath)
213219
214220
errors.value = otherErrors.concat(pathErrors)
215221
} else {

test/components/Form.spec.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { renderForm } from '../utils/form'
1313
import {
1414
UForm,
1515
UInput,
16+
URadioGroup,
1617
UFormField
1718
} from '#components'
1819
import { flushPromises } from '@vue/test-utils'
@@ -632,4 +633,65 @@ describe('Form', () => {
632633
await flushPromises()
633634
expect(wrapper.html()).toContain('Error message')
634635
})
636+
it('should not have race condition when clear is called in watchers', async () => {
637+
const wrapper = await mountSuspended({
638+
components: {
639+
UForm,
640+
URadioGroup,
641+
UFormField
642+
},
643+
setup() {
644+
const form = ref()
645+
const schema = z.object({
646+
hello: z.string().optional(),
647+
world: z.string().optional(),
648+
hi: z.string().optional(),
649+
pathForACustomError: z.string().optional()
650+
})
651+
652+
const state = reactive({
653+
hello: 'hello-1',
654+
world: 'world-1',
655+
hi: 'hi-1',
656+
pathForACustomError: ''
657+
})
658+
659+
return { form, state, schema }
660+
},
661+
template: `
662+
<UForm ref="form" :schema="schema" :state="state">
663+
<UFormField name="hello">
664+
<URadioGroup v-model="state.hello" :items="[{ value: 'foo-1', label: 'Foo 1' }, { value: 'foo-2', label: 'Foo 2' }]" />
665+
</UFormField>
666+
</UForm>
667+
`
668+
})
669+
670+
const form = wrapper.setupState.form
671+
672+
const input = wrapper.findComponent({
673+
name: 'RadioGroupRoot'
674+
})
675+
676+
const state = wrapper.setupState.state
677+
678+
watch(() => state.hello, () => {
679+
form.value?.clear('pathForACustomError')
680+
})
681+
682+
form.value.setErrors([
683+
{
684+
name: 'pathForACustomError',
685+
message: 'This is a custom error message.'
686+
}
687+
])
688+
689+
expect(form.value.errors).toHaveLength(1)
690+
691+
input.setValue('foo-2')
692+
693+
await flushPromises()
694+
695+
expect(form.value.errors).toHaveLength(0)
696+
})
635697
})

0 commit comments

Comments
 (0)