mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-15 20:48:12 +01:00
Compare commits
3 Commits
pr/3428
...
fix/form-o
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
385cbeec6c | ||
|
|
4d875c03a2 | ||
|
|
5aea866057 |
@@ -34,9 +34,10 @@ const schema = z.object({
|
||||
pin: z.string().regex(/^\d$/).array().length(5)
|
||||
})
|
||||
|
||||
type Schema = z.input<typeof schema>
|
||||
type Input = z.input<typeof schema>
|
||||
type Output = z.output<typeof schema>
|
||||
|
||||
const state = reactive<Partial<Schema>>({})
|
||||
const state = reactive<Partial<Input>>({})
|
||||
|
||||
const form = useTemplateRef('form')
|
||||
|
||||
@@ -47,7 +48,7 @@ const items = [
|
||||
]
|
||||
|
||||
const toast = useToast()
|
||||
async function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
async function onSubmit(event: FormSubmitEvent<Output>) {
|
||||
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
|
||||
console.log(event.data)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ async function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
<UCheckbox v-model="state.news" name="news" label="Register to our newsletter" @update:model-value="state.email = undefined" />
|
||||
</div>
|
||||
|
||||
<UForm v-if="state.news" :state="state" :schema="nestedSchema">
|
||||
<UForm v-if="state.news" :state="state" :schema="nestedSchema" nested>
|
||||
<UFormField label="Email" name="email">
|
||||
<UInput v-model="state.email" placeholder="john@lennon.com" />
|
||||
</UFormField>
|
||||
|
||||
@@ -51,7 +51,14 @@ async function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
<UInput v-model="state.customer" placeholder="Wonka Industries" />
|
||||
</UFormField>
|
||||
|
||||
<UForm v-for="item, count in state.items" :key="count" :state="item" :schema="itemSchema" class="flex gap-2">
|
||||
<UForm
|
||||
v-for="item, count in state.items"
|
||||
:key="count"
|
||||
:state="item"
|
||||
:schema="itemSchema"
|
||||
nested
|
||||
class="flex gap-2"
|
||||
>
|
||||
<UFormField :label="!count ? 'Description' : undefined" name="description">
|
||||
<UInput v-model="item.description" />
|
||||
</UFormField>
|
||||
|
||||
71
playground/app/pages/components/form-sandro.vue
Normal file
71
playground/app/pages/components/form-sandro.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<UContainer>
|
||||
<UForm :schema :state @submit="onSubmit">
|
||||
<UFormField label="A" name="a">
|
||||
<UInput v-model="state.a" />
|
||||
</UFormField>
|
||||
<UFormField label="B" name="b">
|
||||
<UInput v-model="state.b" />
|
||||
</UFormField>
|
||||
|
||||
<UButton type="submit">
|
||||
Submit
|
||||
</UButton>
|
||||
</UForm>
|
||||
|
||||
{{ output }}
|
||||
</UContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||
import * as v from 'valibot'
|
||||
|
||||
const _schemaStringFiltered = v.pipe(v.string(), v.trim())
|
||||
const schema = v.object({
|
||||
a: v.string(),
|
||||
b: v.union([
|
||||
v.pipe(
|
||||
v.array(_schemaStringFiltered),
|
||||
v.filterItems((item, index, array) => (array.indexOf(item) === index || item !== ''))
|
||||
),
|
||||
v.pipe(
|
||||
v.string(),
|
||||
v.trim(),
|
||||
v.transform(
|
||||
(item) => {
|
||||
if (item === '') return undefined
|
||||
|
||||
return item
|
||||
.split(',')
|
||||
.map(val => val.trim())
|
||||
.filter(val => val !== '')
|
||||
}
|
||||
)
|
||||
)
|
||||
])
|
||||
})
|
||||
|
||||
const state = reactive<{
|
||||
a: string
|
||||
b: string
|
||||
}>({
|
||||
a: 'hello, world',
|
||||
b: 'hello, world'
|
||||
})
|
||||
const output = reactive<{
|
||||
a: string
|
||||
b?: string[]
|
||||
}>({
|
||||
a: '',
|
||||
b: []
|
||||
})
|
||||
|
||||
function onSubmit(event: FormSubmitEvent<v.InferOutput<typeof schema>>) {
|
||||
console.log('typeof `a`:', typeof event.data.a) // should be string
|
||||
console.log('typeof `b`:', typeof event.data.b) // should be object (array of strings)
|
||||
|
||||
output.a = event.data.a
|
||||
output.b = event.data.b
|
||||
}
|
||||
</script>
|
||||
@@ -7,18 +7,19 @@ import type { ComponentConfig } from '../types/utils'
|
||||
|
||||
type FormConfig = ComponentConfig<typeof theme, AppConfig, 'form'>
|
||||
|
||||
export interface FormProps<T extends object> {
|
||||
export interface FormProps<I extends object, O extends object = I> {
|
||||
id?: string | number
|
||||
/** Schema to validate the form state. Supports Standard Schema objects, Yup, Joi, and Superstructs. */
|
||||
schema?: FormSchema<T>
|
||||
schema?: FormSchema<I, O>
|
||||
|
||||
/** An object representing the current state of the form. */
|
||||
state: Partial<T>
|
||||
state: Partial<I>
|
||||
/**
|
||||
* Custom validation function to validate the form state.
|
||||
* @param state - The current state of the form.
|
||||
* @returns A promise that resolves to an array of FormError objects, or an array of FormError objects directly.
|
||||
*/
|
||||
validate?: (state: Partial<T>) => Promise<FormError[]> | FormError[]
|
||||
validate?: (state: Partial<I>) => Promise<FormError[]> | FormError[]
|
||||
/**
|
||||
* The list of input events that trigger the form validation.
|
||||
* @defaultValue `['blur', 'change', 'input']`
|
||||
@@ -31,11 +32,11 @@ export interface FormProps<T extends object> {
|
||||
* @defaultValue `300`
|
||||
*/
|
||||
validateOnInputDelay?: number
|
||||
|
||||
/**
|
||||
* If true, schema transformations will be applied to the state on submit.
|
||||
* @defaultValue `true`
|
||||
* If true and nested in another form, this form will attach to its parent and validate at the same time.
|
||||
*/
|
||||
transform?: boolean
|
||||
nested?: boolean
|
||||
/**
|
||||
* When `true`, all form elements will be disabled on `@submit` event.
|
||||
* This will cause any focused input elements to lose their focus state.
|
||||
@@ -43,11 +44,11 @@ export interface FormProps<T extends object> {
|
||||
*/
|
||||
loadingAuto?: boolean
|
||||
class?: any
|
||||
onSubmit?: ((event: FormSubmitEvent<T>) => void | Promise<void>) | (() => void | Promise<void>)
|
||||
onSubmit?: ((event: FormSubmitEvent<O>) => void | Promise<void>) | (() => void | Promise<void>)
|
||||
}
|
||||
|
||||
export interface FormEmits<T extends object> {
|
||||
(e: 'submit', payload: FormSubmitEvent<T>): void
|
||||
export interface FormEmits<I extends object, O extends object = I> {
|
||||
(e: 'submit', payload: FormSubmitEvent<O>): void
|
||||
(e: 'error', payload: FormErrorEvent): void
|
||||
}
|
||||
|
||||
@@ -56,7 +57,7 @@ export interface FormSlots {
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup generic="T extends object">
|
||||
<script lang="ts" setup generic="I extends object, O extends object = I">
|
||||
import { provide, inject, nextTick, ref, onUnmounted, onMounted, computed, useId, readonly } from 'vue'
|
||||
import { useEventBus } from '@vueuse/core'
|
||||
import { useAppConfig } from '#imports'
|
||||
@@ -65,16 +66,15 @@ import { tv } from '../utils/tv'
|
||||
import { validateSchema } from '../utils/form'
|
||||
import { FormValidationException } from '../types/form'
|
||||
|
||||
const props = withDefaults(defineProps<FormProps<T>>(), {
|
||||
const props = withDefaults(defineProps<FormProps<I, O>>(), {
|
||||
validateOn() {
|
||||
return ['input', 'blur', 'change'] as FormInputEvents[]
|
||||
},
|
||||
validateOnInputDelay: 300,
|
||||
transform: true,
|
||||
loadingAuto: true
|
||||
})
|
||||
|
||||
const emits = defineEmits<FormEmits<T>>()
|
||||
const emits = defineEmits<FormEmits<I, O>>()
|
||||
defineSlots<FormSlots>()
|
||||
|
||||
const appConfig = useAppConfig() as FormConfig['AppConfig']
|
||||
@@ -83,7 +83,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.form || {})
|
||||
|
||||
const formId = props.id ?? useId() as string
|
||||
|
||||
const bus = useEventBus<FormEvent<T>>(`form-${formId}`)
|
||||
const bus = useEventBus<FormEvent<I>>(`form-${formId}`)
|
||||
const parentBus = inject(
|
||||
formBusInjectionKey,
|
||||
undefined
|
||||
@@ -126,14 +126,14 @@ onUnmounted(() => {
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (parentBus) {
|
||||
if (props.nested && parentBus) {
|
||||
await nextTick()
|
||||
parentBus.emit({ type: 'attach', validate: _validate, formId })
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (parentBus) {
|
||||
if (props.nested && parentBus) {
|
||||
parentBus.emit({ type: 'detach', formId })
|
||||
}
|
||||
})
|
||||
@@ -141,12 +141,12 @@ onUnmounted(() => {
|
||||
const errors = ref<FormErrorWithId[]>([])
|
||||
provide('form-errors', errors)
|
||||
|
||||
const inputs = ref<{ [P in keyof T]?: { id?: string, pattern?: RegExp } }>({})
|
||||
const inputs = ref<{ [P in keyof I]?: { 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>()
|
||||
const dirtyFields = new Set<keyof I>()
|
||||
const touchedFields = new Set<keyof I>()
|
||||
const blurredFields = new Set<keyof I>()
|
||||
|
||||
function resolveErrorIds(errs: FormError[]): FormErrorWithId[] {
|
||||
return errs.map(err => ({
|
||||
@@ -155,7 +155,7 @@ function resolveErrorIds(errs: FormError[]): FormErrorWithId[] {
|
||||
}))
|
||||
}
|
||||
|
||||
const transformedState = ref<T | null>(null)
|
||||
const transformedState = ref<I | null>(null)
|
||||
|
||||
async function getErrors(): Promise<FormErrorWithId[]> {
|
||||
let errs = props.validate ? (await props.validate(props.state)) ?? [] : []
|
||||
@@ -172,8 +172,8 @@ async function getErrors(): Promise<FormErrorWithId[]> {
|
||||
return resolveErrorIds(errs)
|
||||
}
|
||||
|
||||
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)[]
|
||||
async function _validate(opts: { name?: keyof I | (keyof I)[], silent?: boolean, nested?: boolean } = { silent: false, nested: true }): Promise<O | false> {
|
||||
const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name as (keyof I)[]
|
||||
|
||||
const nestedValidatePromises = !names && opts.nested
|
||||
? Array.from(nestedForms.value.values()).map(
|
||||
@@ -209,11 +209,7 @@ async function _validate(opts: { name?: keyof T | (keyof T)[], silent?: boolean,
|
||||
throw new FormValidationException(formId, errors.value, childErrors)
|
||||
}
|
||||
|
||||
if (opts.transform) {
|
||||
Object.assign(props.state, transformedState.value)
|
||||
}
|
||||
|
||||
return props.state as T
|
||||
return transformedState.value
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -225,7 +221,7 @@ async function onSubmitWrapper(payload: Event) {
|
||||
const event = payload as FormSubmitEvent<any>
|
||||
|
||||
try {
|
||||
event.data = await _validate({ nested: true, transform: props.transform })
|
||||
event.data = await _validate({ nested: true })
|
||||
await props.onSubmit?.(event)
|
||||
dirtyFields.clear()
|
||||
} catch (error) {
|
||||
@@ -251,11 +247,11 @@ provide(formOptionsInjectionKey, computed(() => ({
|
||||
validateOnInputDelay: props.validateOnInputDelay
|
||||
})))
|
||||
|
||||
defineExpose<Form<T>>({
|
||||
defineExpose<Form<I, O>>({
|
||||
validate: _validate,
|
||||
errors,
|
||||
|
||||
setErrors(errs: FormError[], name?: keyof T) {
|
||||
setErrors(errs: FormError[], name?: keyof I) {
|
||||
if (name) {
|
||||
errors.value = errors.value
|
||||
.filter(error => error.name !== name)
|
||||
@@ -269,7 +265,7 @@ defineExpose<Form<T>>({
|
||||
await onSubmitWrapper(new Event('submit'))
|
||||
},
|
||||
|
||||
getErrors(name?: keyof T) {
|
||||
getErrors(name?: keyof I) {
|
||||
if (name) {
|
||||
return errors.value.filter(err => err.name === name)
|
||||
}
|
||||
@@ -288,9 +284,9 @@ defineExpose<Form<T>>({
|
||||
loading,
|
||||
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>>
|
||||
dirtyFields: readonly(dirtyFields) as DeepReadonly<Set<keyof I>>,
|
||||
blurredFields: readonly(blurredFields) as DeepReadonly<Set<keyof I>>,
|
||||
touchedFields: readonly(touchedFields) as DeepReadonly<Set<keyof I>>
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -5,27 +5,27 @@ import type { ObjectSchema as YupObjectSchema } from 'yup'
|
||||
import type { GetObjectField } from './utils'
|
||||
import type { Struct as SuperstructSchema } from 'superstruct'
|
||||
|
||||
export interface Form<T extends object> {
|
||||
validate (opts?: { name?: keyof T | (keyof T)[], silent?: boolean, nested?: boolean, transform?: boolean }): Promise<T | false>
|
||||
export interface Form<I extends object, O extends object = I> {
|
||||
validate (opts?: { name?: keyof I | (keyof I)[], silent?: boolean, nested?: boolean, transform?: boolean }): Promise<O | false>
|
||||
clear (path?: string): void
|
||||
errors: Ref<FormError[]>
|
||||
setErrors (errs: FormError[], name?: keyof T): void
|
||||
getErrors (name?: keyof T): FormError[]
|
||||
setErrors (errs: FormError[], name?: keyof I): void
|
||||
getErrors (name?: keyof I): FormError[]
|
||||
submit (): Promise<void>
|
||||
disabled: ComputedRef<boolean>
|
||||
dirty: ComputedRef<boolean>
|
||||
loading: Ref<boolean>
|
||||
|
||||
dirtyFields: DeepReadonly<Set<keyof T>>
|
||||
touchedFields: DeepReadonly<Set<keyof T>>
|
||||
blurredFields: DeepReadonly<Set<keyof T>>
|
||||
dirtyFields: DeepReadonly<Set<keyof I>>
|
||||
touchedFields: DeepReadonly<Set<keyof I>>
|
||||
blurredFields: DeepReadonly<Set<keyof I>>
|
||||
}
|
||||
|
||||
export type FormSchema<T extends object> =
|
||||
| YupObjectSchema<T>
|
||||
| JoiSchema<T>
|
||||
| SuperstructSchema<any, any>
|
||||
| StandardSchemaV1
|
||||
export type FormSchema<I extends object, O extends object = I> =
|
||||
| YupObjectSchema<I>
|
||||
| JoiSchema<I>
|
||||
| SuperstructSchema<I>
|
||||
| StandardSchemaV1<I, O>
|
||||
|
||||
export type FormInputEvents = 'input' | 'blur' | 'change' | 'focus'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user