UNPKG

@volverjs/form-vue

Version:

Vue 3 Forms with @volverjs/ui-vue

334 lines (325 loc) 8.72 kB
import { get, set } from 'ts-dot-prop' import { type Component, type InjectionKey, type PropType, type Ref, type ConcreteComponent, computed, defineAsyncComponent, h, inject, onMounted, provide, readonly, resolveComponent, toRefs, watch, defineComponent, onBeforeUnmount, unref, } from 'vue' import type { z } from 'zod' import { FormFieldType } from './enums' import type { InjectedFormData, InjectedFormWrapperData, InjectedFormFieldData, FormFieldComponentOptions, Path, FormSchema, } from './types' export const defineFormField = <Schema extends FormSchema>( formProvideKey: InjectionKey<InjectedFormData<Schema>>, wrapperProvideKey: InjectionKey<InjectedFormWrapperData<Schema>>, formFieldInjectionKey: InjectionKey<InjectedFormFieldData<Schema>>, options?: FormFieldComponentOptions, ) => { // define component return defineComponent({ name: 'FieldComponent', props: { type: { type: String as PropType<`${FormFieldType}`>, validator: (value: FormFieldType) => { return Object.values(FormFieldType).includes(value) }, default: FormFieldType.custom, }, is: { type: [Object, String] as PropType<Component>, default: undefined, }, name: { type: [String, Number, Boolean, Symbol] as PropType< Path<z.infer<Schema>> >, required: true, }, props: { type: [Object, Function] as PropType< Partial< | z.infer<Schema> | undefined | (( formData?: Ref<ObjectConstructor>, ) => Partial<z.infer<Schema>> | undefined) > >, default: () => ({}), }, showValid: { type: Boolean, default: false, }, defaultValue: { type: [String, Number, Boolean, Array, Object], default: undefined, }, lazyLoad: { type: Boolean, default: false, }, }, emits: ['invalid', 'valid', 'update:formData', 'update:modelValue'], expose: ['invalid', 'invalidLabel', 'errors'], setup(props, { slots, emit }) { // v-model const modelValue = computed({ get() { if (!injectedFormData?.formData) return return get( Object(injectedFormData.formData.value), String(props.name), ) }, set(value) { if (!injectedFormData?.formData) return set( Object(injectedFormData.formData.value), String(props.name), value, ) emit('update:modelValue', { newValue: modelValue.value, formData: injectedFormData?.formData, }) }, }) onMounted(() => { if ( modelValue.value === undefined && props.defaultValue !== undefined ) { modelValue.value = props.defaultValue } }) onBeforeUnmount(() => { unwatchInvalid() unwatchInjectedFormData() }) // inject data from parent form wrapper const injectedWrapperData = inject(wrapperProvideKey, undefined) if (injectedWrapperData) { injectedWrapperData.fields.value.add(props.name as string) } // inject data from parent form const injectedFormData = inject(formProvideKey) const { props: fieldProps, name: fieldName } = toRefs(props) const errors = computed(() => { if (!injectedFormData?.errors.value) { return undefined } return get(injectedFormData.errors.value, String(props.name)) }) const invalidLabel = computed(() => { return errors.value?._errors }) const invalid = computed(() => { return errors.value !== undefined }) const unwatchInvalid = watch(invalid, () => { if (invalid.value) { emit('invalid', invalidLabel.value) if (injectedWrapperData) { injectedWrapperData.errors.value.set( props.name as string, { _errors: invalidLabel.value, } as z.inferFormattedError<Schema>, ) } } else { emit('valid', modelValue.value) if (injectedWrapperData) { injectedWrapperData.errors.value.delete( props.name as string, ) } } }) const unwatchInjectedFormData = watch( () => injectedFormData?.formData, () => { emit('update:formData', injectedFormData?.formData) }, { deep: true }, ) const onUpdate = (value: unknown) => { modelValue.value = value } const hasFieldProps = computed(() => { let toReturn = fieldProps.value if (typeof toReturn === 'function') { toReturn = toReturn(injectedFormData?.formData) } return Object.keys(toReturn).reduce<Record<string, unknown>>( (acc, key) => { acc[key] = unref(toReturn[key]) return acc }, {}, ) }) const hasProps = computed(() => ({ ...hasFieldProps.value, name: hasFieldProps.value.name ?? props.name, invalid: invalid.value, valid: props.showValid ? Boolean(!invalid.value && modelValue.value) : undefined, type: ((type: FormFieldType) => { if ( [ FormFieldType.text, FormFieldType.number, FormFieldType.email, FormFieldType.password, FormFieldType.tel, FormFieldType.url, FormFieldType.search, FormFieldType.date, FormFieldType.time, FormFieldType.datetimeLocal, FormFieldType.month, FormFieldType.week, FormFieldType.color, ].includes(type) ) { return type } return undefined })(props.type as FormFieldType), invalidLabel: invalidLabel.value, modelValue: modelValue.value, 'onUpdate:modelValue': onUpdate, })) provide(formFieldInjectionKey, { name: readonly(fieldName as Ref<string>), errors: readonly(errors), }) const component = computed(() => { if (props.type === FormFieldType.custom) { return { render() { return ( slots.default?.({ modelValue: modelValue.value, onUpdate, submit: injectedFormData?.submit, validate: injectedFormData?.validate, invalid: invalid.value, invalidLabel: invalidLabel.value, formData: injectedFormData?.formData.value, formErrors: injectedFormData?.errors.value, errors: errors.value, }) ?? slots.defalut ) }, } } if (!(options?.lazyLoad ?? props.lazyLoad)) { let component: string | ConcreteComponent switch (props.type) { case FormFieldType.select: component = resolveComponent('VvSelect') break case FormFieldType.checkbox: component = resolveComponent('VvCheckbox') break case FormFieldType.radio: component = resolveComponent('VvRadio') break case FormFieldType.textarea: component = resolveComponent('VvTextarea') break case FormFieldType.radioGroup: component = resolveComponent('VvRadioGroup') break case FormFieldType.checkboxGroup: component = resolveComponent('VvCheckboxGroup') break case FormFieldType.combobox: component = resolveComponent('VvCombobox') break default: component = resolveComponent('VvInputText') } if (typeof component !== 'string') { return component } else { console.warn( `[form-vue warn]: ${component} not found, the component will be loaded asynchronously. To avoid this warning, please set "lazyLoad" option.`, ) } } return defineAsyncComponent(async () => { if (options?.sideEffects) { await Promise.resolve(options.sideEffects(props.type)) } switch (props.type) { case FormFieldType.textarea: return import( '@volverjs/ui-vue/vv-textarea' ) as Component case FormFieldType.radio: return import( '@volverjs/ui-vue/vv-radio' ) as Component case FormFieldType.radioGroup: return import( '@volverjs/ui-vue/vv-radio-group' ) as Component case FormFieldType.checkbox: return import( '@volverjs/ui-vue/vv-checkbox' ) as Component case FormFieldType.checkboxGroup: return import( '@volverjs/ui-vue/vv-checkbox-group' ) as Component case FormFieldType.select: return import( '@volverjs/ui-vue/vv-select' ) as Component case FormFieldType.combobox: return import( '@volverjs/ui-vue/vv-combobox' ) as Component } return import('@volverjs/ui-vue/vv-input-text') as Component }) }) return { component, hasProps, invalid } }, render() { if (this.is) { return h(this.is, this.hasProps, this.$slots) } if (this.type === FormFieldType.custom) { return h(this.component as Component, null, this.$slots) } return h(this.component as Component, this.hasProps, this.$slots) }, }) }