mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-14 12:14:41 +01:00
feat(Form): add validate-on error-input
Adds `error-input` option to the validate on prop to validate on input only when the field has an error. Resolves #2599
This commit is contained in:
@@ -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 `input` occurs **as you type**.
|
||||||
- Validation on `change` occurs when you **commit to a value**.
|
- Validation on `change` occurs when you **commit to a value**.
|
||||||
- Validation on `blur` happens when an input **loses focus**.
|
- 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.
|
You can control when validation happens this using the `validate-on` prop.
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const disabled = ref(false)
|
|||||||
<div class="border border-default rounded-lg">
|
<div class="border border-default rounded-lg">
|
||||||
<div class="py-2 px-4 flex gap-4 items-center">
|
<div class="py-2 px-4 flex gap-4 items-center">
|
||||||
<UFormField label="Validate on" class="flex items-center gap-2">
|
<UFormField label="Validate on" class="flex items-center gap-2">
|
||||||
<USelectMenu v-model="validateOn" :items="['input', 'change', 'blur']" multiple class="w-48" />
|
<USelectMenu v-model="validateOn" :items="['input', 'change', 'blur', 'error-input']" multiple class="w-48" />
|
||||||
</UFormField>
|
</UFormField>
|
||||||
<UCheckbox v-model="disabled" label="Disabled" />
|
<UCheckbox v-model="disabled" label="Disabled" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import type { DeepReadonly } from 'vue'
|
import type { DeepReadonly } from 'vue'
|
||||||
import type { AppConfig } from '@nuxt/schema'
|
import type { AppConfig } from '@nuxt/schema'
|
||||||
import theme from '#build/ui/form'
|
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'
|
import type { ComponentConfig } from '../types/utils'
|
||||||
|
|
||||||
type FormConfig = ComponentConfig<typeof theme, AppConfig, 'form'>
|
type FormConfig = ComponentConfig<typeof theme, AppConfig, 'form'>
|
||||||
@@ -23,7 +23,8 @@ export interface FormProps<S extends FormSchema, T extends boolean = true> {
|
|||||||
* The list of input events that trigger the form validation.
|
* The list of input events that trigger the form validation.
|
||||||
* @defaultValue `['blur', 'change', 'input']`
|
* @defaultValue `['blur', 'change', 'input']`
|
||||||
*/
|
*/
|
||||||
validateOn?: FormInputEvents[]
|
validateOn?: FormValidateOn[]
|
||||||
|
|
||||||
/** Disable all inputs inside the form. */
|
/** Disable all inputs inside the form. */
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
/**
|
/**
|
||||||
@@ -77,7 +78,7 @@ type O = InferOutput<S>
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<FormProps<S, T>>(), {
|
const props = withDefaults(defineProps<FormProps<S, T>>(), {
|
||||||
validateOn() {
|
validateOn() {
|
||||||
return ['input', 'blur', 'change'] as FormInputEvents[]
|
return ['input', 'blur', 'change'] as FormValidateOn[]
|
||||||
},
|
},
|
||||||
validateOnInputDelay: 300,
|
validateOnInputDelay: 300,
|
||||||
attach: true,
|
attach: true,
|
||||||
@@ -110,12 +111,14 @@ onMounted(async () => {
|
|||||||
nestedForms.value.set(event.formId, { validate: event.validate })
|
nestedForms.value.set(event.formId, { validate: event.validate })
|
||||||
} else if (event.type === 'detach') {
|
} else if (event.type === 'detach') {
|
||||||
nestedForms.value.delete(event.formId)
|
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') {
|
if (event.type !== 'input') {
|
||||||
await _validate({ name: event.name, silent: true, nested: false })
|
await _validate({ name: event.name, silent: true, nested: false })
|
||||||
} else if (event.eager || blurredFields.has(event.name)) {
|
} else if (event.eager || blurredFields.has(event.name)) {
|
||||||
await _validate({ name: event.name, silent: true, nested: false })
|
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') {
|
if (event.type === 'blur') {
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ export type FormData<S extends FormSchema, T extends boolean = true> = T extends
|
|||||||
|
|
||||||
export type FormInputEvents = 'input' | 'blur' | 'change' | 'focus'
|
export type FormInputEvents = 'input' | 'blur' | 'change' | 'focus'
|
||||||
|
|
||||||
|
export type FormValidateOn = 'input' | 'blur' | 'change' | 'error-input'
|
||||||
|
|
||||||
export interface FormError<P extends string = string> {
|
export interface FormError<P extends string = string> {
|
||||||
name?: P
|
name?: P
|
||||||
message: string
|
message: string
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import ComponentRender from '../component-render'
|
|||||||
import theme from '#build/ui/checkbox'
|
import theme from '#build/ui/checkbox'
|
||||||
import { renderForm } from '../utils/form'
|
import { renderForm } from '../utils/form'
|
||||||
import { mount, flushPromises } from '@vue/test-utils'
|
import { mount, flushPromises } from '@vue/test-utils'
|
||||||
import type { FormInputEvents } from '~/src/module'
|
import type { FormValidateOn } from '~/src/module'
|
||||||
|
|
||||||
describe('Checkbox', () => {
|
describe('Checkbox', () => {
|
||||||
const sizes = Object.keys(theme.variants.size) as any
|
const sizes = Object.keys(theme.variants.size) as any
|
||||||
@@ -58,7 +58,7 @@ describe('Checkbox', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('form integration', async () => {
|
describe('form integration', async () => {
|
||||||
async function createForm(validateOn?: FormInputEvents[]) {
|
async function createForm(validateOn?: FormValidateOn[]) {
|
||||||
const wrapper = await renderForm({
|
const wrapper = await renderForm({
|
||||||
props: {
|
props: {
|
||||||
validateOn,
|
validateOn,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import theme from '#build/ui/checkbox-group'
|
|||||||
import themeCheckbox from '#build/ui/checkbox'
|
import themeCheckbox from '#build/ui/checkbox'
|
||||||
import { flushPromises, mount } from '@vue/test-utils'
|
import { flushPromises, mount } from '@vue/test-utils'
|
||||||
import { renderForm } from '../utils/form'
|
import { renderForm } from '../utils/form'
|
||||||
import type { FormInputEvents } from '~/src/module'
|
import type { FormValidateOn } from '~/src/module'
|
||||||
|
|
||||||
describe('CheckboxGroup', () => {
|
describe('CheckboxGroup', () => {
|
||||||
const sizes = Object.keys(theme.variants.size) as any
|
const sizes = Object.keys(theme.variants.size) as any
|
||||||
@@ -65,7 +65,7 @@ describe('CheckboxGroup', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('form integration', async () => {
|
describe('form integration', async () => {
|
||||||
async function createForm(validateOn?: FormInputEvents[]) {
|
async function createForm(validateOn?: FormValidateOn[]) {
|
||||||
const wrapper = await renderForm({
|
const wrapper = await renderForm({
|
||||||
props: {
|
props: {
|
||||||
validateOn,
|
validateOn,
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import Input, { type InputProps, type InputSlots } from '../../src/runtime/compo
|
|||||||
import ComponentRender from '../component-render'
|
import ComponentRender from '../component-render'
|
||||||
import theme from '#build/ui/input'
|
import theme from '#build/ui/input'
|
||||||
|
|
||||||
|
import type { FormValidateOn } from '~/src/module'
|
||||||
import { renderForm } from '../utils/form'
|
import { renderForm } from '../utils/form'
|
||||||
import type { FormInputEvents } from '~/src/module'
|
|
||||||
|
|
||||||
describe('Input', () => {
|
describe('Input', () => {
|
||||||
const sizes = Object.keys(theme.variants.size) as any
|
const sizes = Object.keys(theme.variants.size) as any
|
||||||
@@ -104,7 +104,7 @@ describe('Input', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('form integration', async () => {
|
describe('form integration', async () => {
|
||||||
async function createForm(validateOn?: FormInputEvents[], eagerValidation?: boolean) {
|
async function createForm(validateOn?: FormValidateOn[], eagerValidation?: boolean) {
|
||||||
const wrapper = await renderForm({
|
const wrapper = await renderForm({
|
||||||
props: {
|
props: {
|
||||||
validateOn,
|
validateOn,
|
||||||
@@ -160,6 +160,18 @@ describe('Input', () => {
|
|||||||
expect(wrapper.text()).not.toContain('Error message')
|
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 () => {
|
test('validate on input without eager validation works', async () => {
|
||||||
const { input, wrapper } = await createForm(['input'])
|
const { input, wrapper } = await createForm(['input'])
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import ComponentRender from '../component-render'
|
|||||||
import theme from '#build/ui/input'
|
import theme from '#build/ui/input'
|
||||||
import { renderForm } from '../utils/form'
|
import { renderForm } from '../utils/form'
|
||||||
import { flushPromises, mount } from '@vue/test-utils'
|
import { flushPromises, mount } from '@vue/test-utils'
|
||||||
import type { FormInputEvents } from '~/src/module'
|
import type { FormValidateOn } from '~/src/module'
|
||||||
import { expectEmitPayloadType } from '../utils/types'
|
import { expectEmitPayloadType } from '../utils/types'
|
||||||
|
|
||||||
describe('InputMenu', () => {
|
describe('InputMenu', () => {
|
||||||
@@ -110,7 +110,7 @@ describe('InputMenu', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('form integration', async () => {
|
describe('form integration', async () => {
|
||||||
async function createForm(validateOn?: FormInputEvents[]) {
|
async function createForm(validateOn?: FormValidateOn[]) {
|
||||||
const wrapper = await renderForm({
|
const wrapper = await renderForm({
|
||||||
props: {
|
props: {
|
||||||
validateOn,
|
validateOn,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { reactive } from 'vue'
|
|||||||
import InputNumber, { type InputNumberProps, type InputNumberSlots } from '../../src/runtime/components/InputNumber.vue'
|
import InputNumber, { type InputNumberProps, type InputNumberSlots } from '../../src/runtime/components/InputNumber.vue'
|
||||||
import ComponentRender from '../component-render'
|
import ComponentRender from '../component-render'
|
||||||
import theme from '#build/ui/input-number'
|
import theme from '#build/ui/input-number'
|
||||||
import type { FormInputEvents } from '~/src/module'
|
import type { FormValidateOn } from '~/src/module'
|
||||||
import { renderForm } from '../utils/form'
|
import { renderForm } from '../utils/form'
|
||||||
|
|
||||||
describe('InputNumber', () => {
|
describe('InputNumber', () => {
|
||||||
@@ -61,7 +61,7 @@ describe('InputNumber', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('form integration', async () => {
|
describe('form integration', async () => {
|
||||||
async function createForm(validateOn?: FormInputEvents[]) {
|
async function createForm(validateOn?: FormValidateOn[]) {
|
||||||
const wrapper = await renderForm({
|
const wrapper = await renderForm({
|
||||||
state: reactive({ value: 0 }),
|
state: reactive({ value: 0 }),
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import ComponentRender from '../component-render'
|
|||||||
import theme from '#build/ui/pin-input'
|
import theme from '#build/ui/pin-input'
|
||||||
|
|
||||||
import { renderForm } from '../utils/form'
|
import { renderForm } from '../utils/form'
|
||||||
import type { FormInputEvents } from '~/src/module'
|
import type { FormValidateOn } from '~/src/module'
|
||||||
|
|
||||||
describe('PinInput', () => {
|
describe('PinInput', () => {
|
||||||
const sizes = Object.keys(theme.variants.size) as any
|
const sizes = Object.keys(theme.variants.size) as any
|
||||||
@@ -65,7 +65,7 @@ describe('PinInput', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('form integration', async () => {
|
describe('form integration', async () => {
|
||||||
async function createForm(validateOn?: FormInputEvents[]) {
|
async function createForm(validateOn?: FormValidateOn[]) {
|
||||||
const wrapper = await renderForm({
|
const wrapper = await renderForm({
|
||||||
props: {
|
props: {
|
||||||
validateOn,
|
validateOn,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import ComponentRender from '../component-render'
|
|||||||
import theme from '#build/ui/radio-group'
|
import theme from '#build/ui/radio-group'
|
||||||
import { flushPromises, mount } from '@vue/test-utils'
|
import { flushPromises, mount } from '@vue/test-utils'
|
||||||
import { renderForm } from '../utils/form'
|
import { renderForm } from '../utils/form'
|
||||||
import type { FormInputEvents } from '~/src/module'
|
import type { FormValidateOn } from '~/src/module'
|
||||||
|
|
||||||
describe('RadioGroup', () => {
|
describe('RadioGroup', () => {
|
||||||
const sizes = Object.keys(theme.variants.size) as any
|
const sizes = Object.keys(theme.variants.size) as any
|
||||||
@@ -67,7 +67,7 @@ describe('RadioGroup', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('form integration', async () => {
|
describe('form integration', async () => {
|
||||||
async function createForm(validateOn?: FormInputEvents[]) {
|
async function createForm(validateOn?: FormValidateOn[]) {
|
||||||
const wrapper = await renderForm({
|
const wrapper = await renderForm({
|
||||||
props: {
|
props: {
|
||||||
validateOn,
|
validateOn,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Select, { type SelectProps, type SelectSlots } from '../../src/runtime/co
|
|||||||
import ComponentRender from '../component-render'
|
import ComponentRender from '../component-render'
|
||||||
import theme from '#build/ui/input'
|
import theme from '#build/ui/input'
|
||||||
import { renderForm } from '../utils/form'
|
import { renderForm } from '../utils/form'
|
||||||
import type { FormInputEvents } from '~/src/module'
|
import type { FormValidateOn } from '~/src/module'
|
||||||
import { expectEmitPayloadType } from '../utils/types'
|
import { expectEmitPayloadType } from '../utils/types'
|
||||||
|
|
||||||
describe('Select', () => {
|
describe('Select', () => {
|
||||||
@@ -107,7 +107,7 @@ describe('Select', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('form integration', async () => {
|
describe('form integration', async () => {
|
||||||
async function createForm(validateOn?: FormInputEvents[]) {
|
async function createForm(validateOn?: FormValidateOn[]) {
|
||||||
const wrapper = await renderForm({
|
const wrapper = await renderForm({
|
||||||
props: {
|
props: {
|
||||||
validateOn,
|
validateOn,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import ComponentRender from '../component-render'
|
|||||||
import theme from '#build/ui/input'
|
import theme from '#build/ui/input'
|
||||||
import { renderForm } from '../utils/form'
|
import { renderForm } from '../utils/form'
|
||||||
import { flushPromises, mount } from '@vue/test-utils'
|
import { flushPromises, mount } from '@vue/test-utils'
|
||||||
import type { FormInputEvents } from '~/src/module'
|
import type { FormValidateOn } from '~/src/module'
|
||||||
import { expectEmitPayloadType } from '../utils/types'
|
import { expectEmitPayloadType } from '../utils/types'
|
||||||
|
|
||||||
describe('SelectMenu', () => {
|
describe('SelectMenu', () => {
|
||||||
@@ -113,7 +113,7 @@ describe('SelectMenu', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('form integration', async () => {
|
describe('form integration', async () => {
|
||||||
async function createForm(validateOn?: FormInputEvents[]) {
|
async function createForm(validateOn?: FormValidateOn[]) {
|
||||||
const wrapper = await renderForm({
|
const wrapper = await renderForm({
|
||||||
props: {
|
props: {
|
||||||
validateOn,
|
validateOn,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import ComponentRender from '../component-render'
|
|||||||
import theme from '#build/ui/slider'
|
import theme from '#build/ui/slider'
|
||||||
import { flushPromises, mount } from '@vue/test-utils'
|
import { flushPromises, mount } from '@vue/test-utils'
|
||||||
import { renderForm } from '../utils/form'
|
import { renderForm } from '../utils/form'
|
||||||
import type { FormInputEvents } from '~/src/module'
|
import type { FormValidateOn } from '~/src/module'
|
||||||
|
|
||||||
describe('Slider', () => {
|
describe('Slider', () => {
|
||||||
const sizes = Object.keys(theme.variants.size) as any
|
const sizes = Object.keys(theme.variants.size) as any
|
||||||
@@ -52,7 +52,7 @@ describe('Slider', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('form integration', async () => {
|
describe('form integration', async () => {
|
||||||
async function createForm(validateOn?: FormInputEvents[]) {
|
async function createForm(validateOn?: FormValidateOn[]) {
|
||||||
const wrapper = await renderForm({
|
const wrapper = await renderForm({
|
||||||
props: {
|
props: {
|
||||||
validateOn,
|
validateOn,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import ComponentRender from '../component-render'
|
|||||||
import theme from '#build/ui/switch'
|
import theme from '#build/ui/switch'
|
||||||
import { flushPromises, mount } from '@vue/test-utils'
|
import { flushPromises, mount } from '@vue/test-utils'
|
||||||
import { renderForm } from '../utils/form'
|
import { renderForm } from '../utils/form'
|
||||||
import type { FormInputEvents } from '~/src/module'
|
import type { FormValidateOn } from '~/src/module'
|
||||||
|
|
||||||
describe('Switch', () => {
|
describe('Switch', () => {
|
||||||
const sizes = Object.keys(theme.variants.size) as any
|
const sizes = Object.keys(theme.variants.size) as any
|
||||||
@@ -55,7 +55,7 @@ describe('Switch', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('form integration', async () => {
|
describe('form integration', async () => {
|
||||||
async function createForm(validateOn?: FormInputEvents[]) {
|
async function createForm(validateOn?: FormValidateOn[]) {
|
||||||
const wrapper = await renderForm({
|
const wrapper = await renderForm({
|
||||||
props: {
|
props: {
|
||||||
validateOn,
|
validateOn,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Textarea, { type TextareaProps, type TextareaSlots } from '../../src/runt
|
|||||||
import ComponentRender from '../component-render'
|
import ComponentRender from '../component-render'
|
||||||
import theme from '#build/ui/textarea'
|
import theme from '#build/ui/textarea'
|
||||||
import { renderForm } from '../utils/form'
|
import { renderForm } from '../utils/form'
|
||||||
import type { FormInputEvents } from '~/src/module'
|
import type { FormValidateOn } from '~/src/module'
|
||||||
|
|
||||||
describe('Textarea', () => {
|
describe('Textarea', () => {
|
||||||
const sizes = Object.keys(theme.variants.size) as any
|
const sizes = Object.keys(theme.variants.size) as any
|
||||||
@@ -107,7 +107,7 @@ describe('Textarea', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('form integration', async () => {
|
describe('form integration', async () => {
|
||||||
async function createForm(validateOn?: FormInputEvents[], eagerValidation?: boolean) {
|
async function createForm(validateOn?: FormValidateOn[], eagerValidation?: boolean) {
|
||||||
const wrapper = await renderForm({
|
const wrapper = await renderForm({
|
||||||
props: {
|
props: {
|
||||||
validateOn,
|
validateOn,
|
||||||
|
|||||||
Reference in New Issue
Block a user