diff --git a/docs/content/3.components/form.md b/docs/content/3.components/form.md index 0ee769b7fd..885a86f1c6 100644 --- a/docs/content/3.components/form.md +++ b/docs/content/3.components/form.md @@ -98,6 +98,7 @@ The Form component automatically triggers validation when an input emits an `inp - Validation on `input` occurs **as you type**. - Validation on `change` occurs when you **commit to a value**. - Validation on `blur` happens when an input **loses focus**. +- Validation on `error-input` happens when as you type on an input with an error. You can control when validation happens this using the `validate-on` prop. diff --git a/playground/app/pages/components/form.vue b/playground/app/pages/components/form.vue index 9641ce79d0..3f399175a0 100644 --- a/playground/app/pages/components/form.vue +++ b/playground/app/pages/components/form.vue @@ -57,7 +57,7 @@ const disabled = ref(false)
- +
diff --git a/src/runtime/components/Form.vue b/src/runtime/components/Form.vue index 3213c5f22c..86cd526a71 100644 --- a/src/runtime/components/Form.vue +++ b/src/runtime/components/Form.vue @@ -2,7 +2,7 @@ import type { DeepReadonly } from 'vue' import type { AppConfig } from '@nuxt/schema' import theme from '#build/ui/form' -import type { FormSchema, FormError, FormInputEvents, FormErrorEvent, FormSubmitEvent, FormEvent, Form, FormErrorWithId, InferInput, InferOutput, FormData } from '../types/form' +import type { FormSchema, FormError, FormErrorEvent, FormSubmitEvent, FormEvent, Form, FormErrorWithId, InferInput, InferOutput, FormData, FormValidateOn } from '../types/form' import type { ComponentConfig } from '../types/utils' type FormConfig = ComponentConfig @@ -23,7 +23,8 @@ export interface FormProps { * The list of input events that trigger the form validation. * @defaultValue `['blur', 'change', 'input']` */ - validateOn?: FormInputEvents[] + validateOn?: FormValidateOn[] + /** Disable all inputs inside the form. */ disabled?: boolean /** @@ -77,7 +78,7 @@ type O = InferOutput const props = withDefaults(defineProps>(), { validateOn() { - return ['input', 'blur', 'change'] as FormInputEvents[] + return ['input', 'blur', 'change'] as FormValidateOn[] }, validateOnInputDelay: 300, attach: true, @@ -110,12 +111,14 @@ onMounted(async () => { nestedForms.value.set(event.formId, { validate: event.validate }) } else if (event.type === 'detach') { nestedForms.value.delete(event.formId) - } else if (props.validateOn?.includes(event.type) && !loading.value) { + } else if (props.validateOn?.includes(event.type as FormValidateOn) && !loading.value) { if (event.type !== 'input') { await _validate({ name: event.name, silent: true, nested: false }) } else if (event.eager || blurredFields.has(event.name)) { await _validate({ name: event.name, silent: true, nested: false }) } + } else if (props.validateOn?.includes('error-input') && errors.value?.find(e => e.name === event.name)) { + await _validate({ name: event.name, silent: true, nested: false }) } if (event.type === 'blur') { diff --git a/src/runtime/types/form.ts b/src/runtime/types/form.ts index 3749c37061..db52039973 100644 --- a/src/runtime/types/form.ts +++ b/src/runtime/types/form.ts @@ -46,6 +46,8 @@ export type FormData = T extends export type FormInputEvents = 'input' | 'blur' | 'change' | 'focus' +export type FormValidateOn = 'input' | 'blur' | 'change' | 'error-input' + export interface FormError

{ name?: P message: string diff --git a/test/components/Checkbox.spec.ts b/test/components/Checkbox.spec.ts index 4ac957b80c..99d11cc46e 100644 --- a/test/components/Checkbox.spec.ts +++ b/test/components/Checkbox.spec.ts @@ -4,7 +4,7 @@ import ComponentRender from '../component-render' import theme from '#build/ui/checkbox' import { renderForm } from '../utils/form' import { mount, flushPromises } from '@vue/test-utils' -import type { FormInputEvents } from '~/src/module' +import type { FormValidateOn } from '~/src/module' describe('Checkbox', () => { const sizes = Object.keys(theme.variants.size) as any @@ -58,7 +58,7 @@ describe('Checkbox', () => { }) describe('form integration', async () => { - async function createForm(validateOn?: FormInputEvents[]) { + async function createForm(validateOn?: FormValidateOn[]) { const wrapper = await renderForm({ props: { validateOn, diff --git a/test/components/CheckboxGroup.spec.ts b/test/components/CheckboxGroup.spec.ts index 01cac48f06..dabc125b76 100644 --- a/test/components/CheckboxGroup.spec.ts +++ b/test/components/CheckboxGroup.spec.ts @@ -5,7 +5,7 @@ import theme from '#build/ui/checkbox-group' import themeCheckbox from '#build/ui/checkbox' import { flushPromises, mount } from '@vue/test-utils' import { renderForm } from '../utils/form' -import type { FormInputEvents } from '~/src/module' +import type { FormValidateOn } from '~/src/module' describe('CheckboxGroup', () => { const sizes = Object.keys(theme.variants.size) as any @@ -65,7 +65,7 @@ describe('CheckboxGroup', () => { }) describe('form integration', async () => { - async function createForm(validateOn?: FormInputEvents[]) { + async function createForm(validateOn?: FormValidateOn[]) { const wrapper = await renderForm({ props: { validateOn, diff --git a/test/components/Input.spec.ts b/test/components/Input.spec.ts index f655cdd56b..e07d5f46af 100644 --- a/test/components/Input.spec.ts +++ b/test/components/Input.spec.ts @@ -4,8 +4,8 @@ import Input, { type InputProps, type InputSlots } from '../../src/runtime/compo import ComponentRender from '../component-render' import theme from '#build/ui/input' +import type { FormValidateOn } from '~/src/module' import { renderForm } from '../utils/form' -import type { FormInputEvents } from '~/src/module' describe('Input', () => { const sizes = Object.keys(theme.variants.size) as any @@ -104,7 +104,7 @@ describe('Input', () => { }) describe('form integration', async () => { - async function createForm(validateOn?: FormInputEvents[], eagerValidation?: boolean) { + async function createForm(validateOn?: FormValidateOn[], eagerValidation?: boolean) { const wrapper = await renderForm({ props: { validateOn, @@ -160,6 +160,18 @@ describe('Input', () => { expect(wrapper.text()).not.toContain('Error message') }) + test('validate on error-input works', async () => { + const { input, wrapper } = await createForm(['error-input', 'blur'], true) + await input.setValue('value') + expect(wrapper.text()).not.toContain('Error message') + + await input.trigger('blur') + expect(wrapper.text()).toContain('Error message') + + await input.setValue('valid') + expect(wrapper.text()).not.toContain('Error message') + }) + test('validate on input without eager validation works', async () => { const { input, wrapper } = await createForm(['input']) diff --git a/test/components/InputMenu.spec.ts b/test/components/InputMenu.spec.ts index dd2f1d5c7c..c1bf223a64 100644 --- a/test/components/InputMenu.spec.ts +++ b/test/components/InputMenu.spec.ts @@ -4,7 +4,7 @@ import ComponentRender from '../component-render' import theme from '#build/ui/input' import { renderForm } from '../utils/form' import { flushPromises, mount } from '@vue/test-utils' -import type { FormInputEvents } from '~/src/module' +import type { FormValidateOn } from '~/src/module' import { expectEmitPayloadType } from '../utils/types' describe('InputMenu', () => { @@ -110,7 +110,7 @@ describe('InputMenu', () => { }) describe('form integration', async () => { - async function createForm(validateOn?: FormInputEvents[]) { + async function createForm(validateOn?: FormValidateOn[]) { const wrapper = await renderForm({ props: { validateOn, diff --git a/test/components/InputNumber.spec.ts b/test/components/InputNumber.spec.ts index 9f2d6754a2..b98258481c 100644 --- a/test/components/InputNumber.spec.ts +++ b/test/components/InputNumber.spec.ts @@ -5,7 +5,7 @@ import { reactive } from 'vue' import InputNumber, { type InputNumberProps, type InputNumberSlots } from '../../src/runtime/components/InputNumber.vue' import ComponentRender from '../component-render' import theme from '#build/ui/input-number' -import type { FormInputEvents } from '~/src/module' +import type { FormValidateOn } from '~/src/module' import { renderForm } from '../utils/form' describe('InputNumber', () => { @@ -61,7 +61,7 @@ describe('InputNumber', () => { }) describe('form integration', async () => { - async function createForm(validateOn?: FormInputEvents[]) { + async function createForm(validateOn?: FormValidateOn[]) { const wrapper = await renderForm({ state: reactive({ value: 0 }), props: { diff --git a/test/components/PinInput.spec.ts b/test/components/PinInput.spec.ts index 771bbb8ce9..7799f89fe0 100644 --- a/test/components/PinInput.spec.ts +++ b/test/components/PinInput.spec.ts @@ -5,7 +5,7 @@ import ComponentRender from '../component-render' import theme from '#build/ui/pin-input' import { renderForm } from '../utils/form' -import type { FormInputEvents } from '~/src/module' +import type { FormValidateOn } from '~/src/module' describe('PinInput', () => { const sizes = Object.keys(theme.variants.size) as any @@ -65,7 +65,7 @@ describe('PinInput', () => { }) describe('form integration', async () => { - async function createForm(validateOn?: FormInputEvents[]) { + async function createForm(validateOn?: FormValidateOn[]) { const wrapper = await renderForm({ props: { validateOn, diff --git a/test/components/RadioGroup.spec.ts b/test/components/RadioGroup.spec.ts index d0be84a9d0..fbe6876f62 100644 --- a/test/components/RadioGroup.spec.ts +++ b/test/components/RadioGroup.spec.ts @@ -4,7 +4,7 @@ import ComponentRender from '../component-render' import theme from '#build/ui/radio-group' import { flushPromises, mount } from '@vue/test-utils' import { renderForm } from '../utils/form' -import type { FormInputEvents } from '~/src/module' +import type { FormValidateOn } from '~/src/module' describe('RadioGroup', () => { const sizes = Object.keys(theme.variants.size) as any @@ -67,7 +67,7 @@ describe('RadioGroup', () => { }) describe('form integration', async () => { - async function createForm(validateOn?: FormInputEvents[]) { + async function createForm(validateOn?: FormValidateOn[]) { const wrapper = await renderForm({ props: { validateOn, diff --git a/test/components/Select.spec.ts b/test/components/Select.spec.ts index 9231cd6e1b..12b0a06279 100644 --- a/test/components/Select.spec.ts +++ b/test/components/Select.spec.ts @@ -4,7 +4,7 @@ import Select, { type SelectProps, type SelectSlots } from '../../src/runtime/co import ComponentRender from '../component-render' import theme from '#build/ui/input' import { renderForm } from '../utils/form' -import type { FormInputEvents } from '~/src/module' +import type { FormValidateOn } from '~/src/module' import { expectEmitPayloadType } from '../utils/types' describe('Select', () => { @@ -107,7 +107,7 @@ describe('Select', () => { }) describe('form integration', async () => { - async function createForm(validateOn?: FormInputEvents[]) { + async function createForm(validateOn?: FormValidateOn[]) { const wrapper = await renderForm({ props: { validateOn, diff --git a/test/components/SelectMenu.spec.ts b/test/components/SelectMenu.spec.ts index e843735ef7..4a039819f3 100644 --- a/test/components/SelectMenu.spec.ts +++ b/test/components/SelectMenu.spec.ts @@ -4,7 +4,7 @@ import ComponentRender from '../component-render' import theme from '#build/ui/input' import { renderForm } from '../utils/form' import { flushPromises, mount } from '@vue/test-utils' -import type { FormInputEvents } from '~/src/module' +import type { FormValidateOn } from '~/src/module' import { expectEmitPayloadType } from '../utils/types' describe('SelectMenu', () => { @@ -113,7 +113,7 @@ describe('SelectMenu', () => { }) describe('form integration', async () => { - async function createForm(validateOn?: FormInputEvents[]) { + async function createForm(validateOn?: FormValidateOn[]) { const wrapper = await renderForm({ props: { validateOn, diff --git a/test/components/Slider.spec.ts b/test/components/Slider.spec.ts index 435edfe808..80a7d31636 100644 --- a/test/components/Slider.spec.ts +++ b/test/components/Slider.spec.ts @@ -4,7 +4,7 @@ import ComponentRender from '../component-render' import theme from '#build/ui/slider' import { flushPromises, mount } from '@vue/test-utils' import { renderForm } from '../utils/form' -import type { FormInputEvents } from '~/src/module' +import type { FormValidateOn } from '~/src/module' describe('Slider', () => { const sizes = Object.keys(theme.variants.size) as any @@ -52,7 +52,7 @@ describe('Slider', () => { }) describe('form integration', async () => { - async function createForm(validateOn?: FormInputEvents[]) { + async function createForm(validateOn?: FormValidateOn[]) { const wrapper = await renderForm({ props: { validateOn, diff --git a/test/components/Switch.spec.ts b/test/components/Switch.spec.ts index 59cb75a419..0cb240cb5d 100644 --- a/test/components/Switch.spec.ts +++ b/test/components/Switch.spec.ts @@ -4,7 +4,7 @@ import ComponentRender from '../component-render' import theme from '#build/ui/switch' import { flushPromises, mount } from '@vue/test-utils' import { renderForm } from '../utils/form' -import type { FormInputEvents } from '~/src/module' +import type { FormValidateOn } from '~/src/module' describe('Switch', () => { const sizes = Object.keys(theme.variants.size) as any @@ -55,7 +55,7 @@ describe('Switch', () => { }) describe('form integration', async () => { - async function createForm(validateOn?: FormInputEvents[]) { + async function createForm(validateOn?: FormValidateOn[]) { const wrapper = await renderForm({ props: { validateOn, diff --git a/test/components/Textarea.spec.ts b/test/components/Textarea.spec.ts index 90280b2103..58dcffca54 100644 --- a/test/components/Textarea.spec.ts +++ b/test/components/Textarea.spec.ts @@ -4,7 +4,7 @@ import Textarea, { type TextareaProps, type TextareaSlots } from '../../src/runt import ComponentRender from '../component-render' import theme from '#build/ui/textarea' import { renderForm } from '../utils/form' -import type { FormInputEvents } from '~/src/module' +import type { FormValidateOn } from '~/src/module' describe('Textarea', () => { const sizes = Object.keys(theme.variants.size) as any @@ -107,7 +107,7 @@ describe('Textarea', () => { }) describe('form integration', async () => { - async function createForm(validateOn?: FormInputEvents[], eagerValidation?: boolean) { + async function createForm(validateOn?: FormValidateOn[], eagerValidation?: boolean) { const wrapper = await renderForm({ props: { validateOn,