mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-20 15:01:46 +01:00
feat(Form): form validation properties (#3137)
This commit is contained in:
@@ -5,6 +5,7 @@ import theme from '#build/ui/form'
|
||||
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
|
||||
import { tv } from '../utils/tv'
|
||||
import type { FormSchema, FormError, FormInputEvents, FormErrorEvent, FormSubmitEvent, FormEvent, Form, FormErrorWithId } from '../types/form'
|
||||
import type { DeepReadonly } from 'vue'
|
||||
|
||||
const appConfig = _appConfig as AppConfig & { ui: { form: Partial<typeof theme> } }
|
||||
|
||||
@@ -52,7 +53,7 @@ defineSlots<FormSlots>()
|
||||
|
||||
const formId = props.id ?? useId() as string
|
||||
|
||||
const bus = useEventBus<FormEvent>(`form-${formId}`)
|
||||
const bus = useEventBus<FormEvent<T>>(`form-${formId}`)
|
||||
const parentBus = inject(
|
||||
formBusInjectionKey,
|
||||
undefined
|
||||
@@ -68,8 +69,24 @@ 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 as FormInputEvents)) {
|
||||
await _validate({ name: event.name, silent: true, nested: false })
|
||||
} else if (props.validateOn?.includes(event.type)) {
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === 'blur') {
|
||||
blurredFields.add(event.name)
|
||||
}
|
||||
|
||||
if (event.type === 'change' || event.type === 'input' || event.type === 'blur' || event.type === 'focus') {
|
||||
touchedFields.add(event.name)
|
||||
}
|
||||
|
||||
if (event.type === 'change' || event.type === 'input') {
|
||||
dirtyFields.add(event.name)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -94,8 +111,12 @@ onUnmounted(() => {
|
||||
const errors = ref<FormErrorWithId[]>([])
|
||||
provide('form-errors', errors)
|
||||
|
||||
const inputs = ref<Record<string, { id?: string, pattern?: RegExp }>>({})
|
||||
provide(formInputsInjectionKey, inputs)
|
||||
const inputs = ref<{ [P in keyof T]?: { id?: string, pattern?: RegExp } }>({})
|
||||
provide(formInputsInjectionKey, inputs as any)
|
||||
|
||||
const dirtyFields = new Set<keyof T>()
|
||||
const touchedFields = new Set<keyof T>()
|
||||
const blurredFields = new Set<keyof T>()
|
||||
|
||||
function resolveErrorIds(errs: FormError[]): FormErrorWithId[] {
|
||||
return errs.map(err => ({
|
||||
@@ -121,8 +142,8 @@ async function getErrors(): Promise<FormErrorWithId[]> {
|
||||
return resolveErrorIds(errs)
|
||||
}
|
||||
|
||||
async function _validate(opts: { name?: string | string[], silent?: boolean, nested?: boolean, transform?: boolean } = { silent: false, nested: true, transform: false }): Promise<T | false> {
|
||||
const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name as string[]
|
||||
async function _validate(opts: { name?: keyof T | (keyof T)[], silent?: boolean, nested?: boolean, transform?: boolean } = { silent: false, nested: true, transform: false }): Promise<T | false> {
|
||||
const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name as (keyof T)[]
|
||||
|
||||
const nestedValidatePromises = !names && opts.nested
|
||||
? Array.from(nestedForms.value.values()).map(
|
||||
@@ -203,7 +224,7 @@ defineExpose<Form<T>>({
|
||||
validate: _validate,
|
||||
errors,
|
||||
|
||||
setErrors(errs: FormError[], name?: string) {
|
||||
setErrors(errs: FormError[], name?: keyof T) {
|
||||
if (name) {
|
||||
errors.value = errors.value
|
||||
.filter(error => error.name !== name)
|
||||
@@ -217,7 +238,7 @@ defineExpose<Form<T>>({
|
||||
await onSubmitWrapper(new Event('submit'))
|
||||
},
|
||||
|
||||
getErrors(name?: string) {
|
||||
getErrors(name?: keyof T) {
|
||||
if (name) {
|
||||
return errors.value.filter(err => err.name === name)
|
||||
}
|
||||
@@ -232,7 +253,12 @@ defineExpose<Form<T>>({
|
||||
}
|
||||
},
|
||||
|
||||
disabled
|
||||
disabled,
|
||||
dirty: computed(() => !!dirtyFields.size),
|
||||
|
||||
dirtyFields: readonly(dirtyFields) as DeepReadonly<Set<keyof T>>,
|
||||
blurredFields: readonly(blurredFields) as DeepReadonly<Set<keyof T>>,
|
||||
touchedFields: readonly(touchedFields) as DeepReadonly<Set<keyof T>>
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ const slots = defineSlots<InputSlots>()
|
||||
|
||||
const [modelValue, modelModifiers] = defineModel<string | number>()
|
||||
|
||||
const { emitFormBlur, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs } = useFormField<InputProps>(props, { deferInputValidation: true })
|
||||
const { emitFormBlur, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, emitFormFocus, ariaAttrs } = useFormField<InputProps>(props, { deferInputValidation: true })
|
||||
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps>(props)
|
||||
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props)
|
||||
|
||||
@@ -170,6 +170,7 @@ onMounted(() => {
|
||||
@input="onInput"
|
||||
@blur="onBlur"
|
||||
@change="onChange"
|
||||
@focus="emitFormFocus"
|
||||
>
|
||||
|
||||
<slot />
|
||||
|
||||
@@ -178,7 +178,7 @@ const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', '
|
||||
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, collisionPadding: 8, position: 'popper' }) as ComboboxContentProps)
|
||||
const arrowProps = toRef(() => props.arrow as ComboboxArrowProps)
|
||||
|
||||
const { emitFormBlur, emitFormChange, emitFormInput, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs } = useFormField<InputProps>(props)
|
||||
const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs } = useFormField<InputProps>(props)
|
||||
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps>(props)
|
||||
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(toRef(() => defu(props, { trailingIcon: appConfig.ui.icons.chevronDown })))
|
||||
|
||||
@@ -279,6 +279,7 @@ function onBlur(event: FocusEvent) {
|
||||
|
||||
function onFocus(event: FocusEvent) {
|
||||
emits('focus', event)
|
||||
emitFormFocus()
|
||||
}
|
||||
|
||||
function onUpdateOpen(value: boolean) {
|
||||
|
||||
@@ -92,7 +92,7 @@ defineSlots<InputNumberSlots>()
|
||||
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'min', 'max', 'step', 'formatOptions'), emits)
|
||||
|
||||
const { emitFormBlur, emitFormChange, emitFormInput, id, color, size, name, highlight, disabled, ariaAttrs } = useFormField<InputNumberProps>(props)
|
||||
const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, id, color, size, name, highlight, disabled, ariaAttrs } = useFormField<InputNumberProps>(props)
|
||||
|
||||
const { t, code: codeLocale } = useLocale()
|
||||
const locale = computed(() => props.locale || codeLocale.value)
|
||||
@@ -158,6 +158,7 @@ defineExpose({
|
||||
:required="required"
|
||||
:class="ui.base({ class: props.ui?.base })"
|
||||
@blur="onBlur"
|
||||
@focus="emitFormFocus"
|
||||
/>
|
||||
|
||||
<div :class="ui.increment({ class: props.ui?.increment })">
|
||||
|
||||
@@ -50,7 +50,7 @@ const props = withDefaults(defineProps<PinInputProps>(), {
|
||||
const emits = defineEmits<PinInputEmits>()
|
||||
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'defaultValue', 'disabled', 'id', 'mask', 'modelValue', 'name', 'otp', 'placeholder', 'required', 'type'), emits)
|
||||
const { emitFormInput, emitFormChange, emitFormBlur, size, color, id, name, highlight, disabled, ariaAttrs } = useFormField<PinInputProps>(props)
|
||||
const { emitFormInput, emitFormFocus, emitFormChange, emitFormBlur, size, color, id, name, highlight, disabled, ariaAttrs } = useFormField<PinInputProps>(props)
|
||||
|
||||
const ui = computed(() => pinInput({
|
||||
color: color.value,
|
||||
@@ -92,6 +92,7 @@ function onBlur(event: FocusEvent) {
|
||||
v-bind="$attrs"
|
||||
:disabled="disabled"
|
||||
@blur="onBlur"
|
||||
@focus="emitFormFocus"
|
||||
/>
|
||||
</PinInputRoot>
|
||||
</template>
|
||||
|
||||
@@ -133,7 +133,7 @@ const rootProps = useForwardPropsEmits(reactivePick(props, 'open', 'defaultOpen'
|
||||
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, collisionPadding: 8, position: 'popper' }) as SelectContentProps)
|
||||
const arrowProps = toRef(() => props.arrow as SelectArrowProps)
|
||||
|
||||
const { emitFormChange, emitFormInput, emitFormBlur, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs } = useFormField<InputProps>(props)
|
||||
const { emitFormChange, emitFormInput, emitFormBlur, emitFormFocus, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs } = useFormField<InputProps>(props)
|
||||
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps>(props)
|
||||
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(toRef(() => defu(props, { trailingIcon: appConfig.ui.icons.chevronDown })))
|
||||
|
||||
@@ -179,6 +179,7 @@ function onUpdateOpen(value: boolean) {
|
||||
} else {
|
||||
const event = new FocusEvent('focus')
|
||||
emits('focus', event)
|
||||
emitFormFocus()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -168,7 +168,7 @@ const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffse
|
||||
const arrowProps = toRef(() => props.arrow as ComboboxArrowProps)
|
||||
const searchInputProps = toRef(() => defu(props.searchInput, { placeholder: t('selectMenu.search'), variant: 'none' }) as InputProps)
|
||||
|
||||
const { emitFormBlur, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs } = useFormField<InputProps>(props)
|
||||
const { emitFormBlur, emitFormFocus, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs } = useFormField<InputProps>(props)
|
||||
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps>(props)
|
||||
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(toRef(() => defu(props, { trailingIcon: appConfig.ui.icons.chevronDown })))
|
||||
|
||||
@@ -272,6 +272,7 @@ function onUpdateOpen(value: boolean) {
|
||||
} else {
|
||||
const event = new FocusEvent('focus')
|
||||
emits('focus', event)
|
||||
emitFormFocus()
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ const emits = defineEmits<TextareaEmits>()
|
||||
|
||||
const [modelValue, modelModifiers] = defineModel<string | number>()
|
||||
|
||||
const { emitFormBlur, emitFormInput, emitFormChange, size, color, id, name, highlight, disabled, ariaAttrs } = useFormField<TextareaProps>(props, { deferInputValidation: true })
|
||||
const { emitFormFocus, emitFormBlur, emitFormInput, emitFormChange, size, color, id, name, highlight, disabled, ariaAttrs } = useFormField<TextareaProps>(props, { deferInputValidation: true })
|
||||
|
||||
const ui = computed(() => textarea({
|
||||
color: color.value,
|
||||
@@ -189,6 +189,7 @@ onMounted(() => {
|
||||
@input="onInput"
|
||||
@blur="onBlur"
|
||||
@change="onChange"
|
||||
@focus="emitFormFocus"
|
||||
/>
|
||||
|
||||
<slot />
|
||||
|
||||
Reference in New Issue
Block a user