UNPKG

@volverjs/form-vue

Version:

Vue 3 Forms with @volverjs/ui-vue

226 lines (217 loc) 5.19 kB
import { type Component, type InjectionKey, type DeepReadonly, type Ref, type PropType, withModifiers, defineComponent, ref, provide, readonly, watch, h, toRaw, isProxy, computed, } from 'vue' import { watchThrottled } from '@vueuse/core' import type { z, ZodFormattedError, TypeOf } from 'zod' import type { FormComponentOptions, FormSchema, FormTemplate, InjectedFormData, } from './types' import { FormStatus } from './enums' import { defaultObjectBySchema } from './utils' export const defineForm = <Schema extends FormSchema>( schema: Schema, provideKey: InjectionKey<InjectedFormData<Schema>>, options?: FormComponentOptions<Schema>, VvFormTemplate?: Component, ) => { const errors = ref<z.inferFormattedError<Schema> | undefined>() const status = ref<FormStatus | undefined>() const formData = ref<Partial<z.infer<Schema> | undefined>>() const component = defineComponent({ name: 'FormComponent', props: { modelValue: { type: Object, default: () => ({}), }, updateThrottle: { type: Number, default: 500, }, continuosValidation: { type: Boolean, default: false, }, template: { type: [Array, Function] as PropType<FormTemplate<Schema>>, default: undefined, }, }, emits: ['invalid', 'valid', 'submit', 'update:modelValue'], expose: ['submit', 'validate', 'errors', 'status', 'valid', 'invalid'], setup(props, { emit }) { formData.value = defaultObjectBySchema( schema, toRaw(props.modelValue), ) // clone modelValue and update formData watch( () => props.modelValue, (newValue) => { if (newValue) { const original = isProxy(newValue) ? toRaw(newValue) : newValue formData.value = typeof original?.clone === 'function' ? original.clone() : JSON.parse(JSON.stringify(original)) } }, { deep: true }, ) // emit update:modelValue on formData change watchThrottled( formData, (newValue) => { if ( (errors.value || options?.continuosValidation) ?? props.continuosValidation ) { validate() } if ( !newValue || !props.modelValue || JSON.stringify(newValue) !== JSON.stringify(props.modelValue) ) { emit('update:modelValue', newValue) options?.onUpdate?.(toRaw(newValue)) } }, { deep: true, throttle: options?.updateThrottle ?? props.updateThrottle, }, ) // validate formData with safeParse const validate = (value = formData.value) => { const parseResult = schema.safeParse(value) if (!parseResult.success) { errors.value = parseResult.error.format() as ZodFormattedError< z.infer<Schema> > status.value = FormStatus.invalid emit('invalid', errors.value) options?.onInvalid?.(toRaw(errors.value)) return false } errors.value = undefined status.value = FormStatus.valid formData.value = parseResult.data emit('update:modelValue', formData.value) options?.onUpdate?.(toRaw(formData.value)) emit('valid', parseResult.data) options?.onValid?.(toRaw(formData.value)) return true } // emit submit event if form is valid const submit = () => { if (!validate()) { return false } emit('submit', formData.value as z.infer<Schema>) options?.onSubmit?.(toRaw(formData.value) as z.infer<Schema>) return true } const invalid = computed(() => status.value === FormStatus.invalid) // provide data to children provide(provideKey, { formData, submit, validate, errors: readonly(errors), status: readonly(status), invalid, }) return { formData, submit, validate, errors: readonly(errors), status: readonly(status), invalid, } }, render() { const defaultSlot = () => this.$slots?.default?.({ formData: this.formData, submit: this.submit, validate: this.validate, errors: this.errors, status: this.status, invalid: this.invalid, }) ?? this.$slots.default return h( 'form', { onSubmit: withModifiers(this.submit, ['prevent']), }, (this.template ?? options?.template) && VvFormTemplate ? [ h( VvFormTemplate, { schema: this.template ?? options?.template, }, { default: defaultSlot, }, ), ] : { default: defaultSlot, }, ) }, }) return { errors, status, formData, /** * An hack to add types to the default slot */ VvForm: component as typeof component & { new (): { $slots: { default: (_: { formData: unknown extends | Partial<TypeOf<Schema>> | undefined ? undefined : Partial<TypeOf<Schema>> | undefined submit: () => boolean validate: () => boolean errors: Readonly< Ref<DeepReadonly<z.inferFormattedError<Schema>>> > status: Ref<DeepReadonly<`${FormStatus}` | undefined>> invalid: Ref<DeepReadonly<boolean>> // eslint-disable-next-line @typescript-eslint/no-explicit-any }) => any } } }, } }