UNPKG

@volverjs/form-vue

Version:

Vue 3 Forms with @volverjs/ui-vue

210 lines (200 loc) 7.46 kB
import type { DeepReadonly, InjectionKey, Ref, SlotsType } from 'vue' import type { FormSchema, InjectedFormData, InjectedFormWrapperData, Path, InferFormattedError, InferSchema, } from './types' import { computed, defineComponent, h, inject, onBeforeUnmount, onMounted, provide, readonly, ref, toRefs, watch, } from 'vue' export function defineFormWrapper<Schema extends FormSchema, Type = undefined>(formProvideKey: InjectionKey<InjectedFormData<Schema, Type>>, wrapperProvideKey: InjectionKey<InjectedFormWrapperData<Schema>>) { return defineComponent({ name: 'VvFormWrapper', props: { name: { type: String, required: true, }, tag: { type: String, default: undefined, }, readonly: { type: Boolean, default: false, }, }, emits: ['invalid', 'valid'], expose: [ 'clear', 'errors', 'fields', 'fieldsErrors', 'formData', 'invalid', 'readonly', 'reset', 'submit', 'tag', 'validate', 'validateWrapper', ], slots: Object as SlotsType<{ default: { errors?: DeepReadonly<InferFormattedError<Schema>> fieldsErrors: Map<string, InferFormattedError<Schema>> formData?: undefined extends Type ? Partial<InferSchema<Schema>> : Type formErrors?: DeepReadonly<InferFormattedError<Schema>> invalid: boolean readonly: boolean clear?: InjectedFormData<Schema, Type>['clear'] reset?: InjectedFormData<Schema, Type>['reset'] submit?: InjectedFormData<Schema, Type>['submit'] validate?: InjectedFormData<Schema, Type>['validate'] validateWrapper?: () => Promise<boolean> } }>, setup(props, { emit }) { // inject data from parent form const injectedFormData = inject(formProvideKey) // inject data from parent form wrapper const injectedWrapperData = inject(wrapperProvideKey, undefined) const fields: Ref<Map<string, Path<InferSchema<Schema>>>> = ref(new Map()) const fieldsErrors: Ref< Map<string, InferFormattedError<Schema>> > = ref(new Map()) const { name } = toRefs(props) // invalid const isInvalid = computed(() => { if (!injectedFormData?.invalid.value) { return false } return fieldsErrors.value.size > 0 }) watch(isInvalid, (newValue) => { if (newValue) { emit('invalid') return } emit('valid') }) // readonly const isReadonly = computed(() => injectedFormData?.readonly.value || props.readonly) // provide data to child fields const providedData = { name: readonly(name), errors: fieldsErrors, invalid: readonly(isInvalid), readonly: readonly(isReadonly), fields, } provide(wrapperProvideKey, providedData) // add fields to parent wrapper const computedFields = computed(() => new Map(fields.value)) watch( computedFields, (newValue, oldValue) => { if (injectedWrapperData?.fields) { oldValue.forEach((_field, key) => { if (!newValue.has(key)) { injectedWrapperData?.fields.value.delete(key) } }) newValue.forEach((field, key) => { if (!injectedWrapperData?.fields.value.has(key)) { injectedWrapperData?.fields.value.set(key, field) } }) } }, { deep: true }, ) // add fields errors to parent wrapper watch( fieldsErrors, (newValue) => { if (injectedWrapperData?.errors) { fields.value.forEach((field) => { if (!newValue.has(field)) { injectedWrapperData.errors.value.delete(field) } if (newValue.has(field)) { const value = newValue.get(field) if (value) { injectedWrapperData.errors.value.set(field, value) } } }) } }, { deep: true }, ) onMounted(() => { if (!injectedFormData?.wrappers || !name.value) { console.warn('[@volverjs/form-vue]: Invalid wrapper registration state') return } if (injectedFormData.wrappers.has(name.value)) { console.warn(`[@volverjs/form-vue]: wrapper name "${name.value}" is already used`) return } injectedFormData.wrappers.set(name.value, providedData) }) onBeforeUnmount(() => { if (injectedFormData?.wrappers && name.value) { injectedFormData.wrappers.delete(name.value) } }) const validateWrapper = () => { return injectedFormData?.validate(undefined, { fields: new Set(fields.value.values()) }) ?? Promise.resolve(true) } return { errors: injectedFormData?.errors, fields, fieldsErrors, formData: injectedFormData?.formData, invalid: isInvalid, readonly: isReadonly, clear: injectedFormData?.clear, reset: injectedFormData?.reset, submit: injectedFormData?.submit, validate: injectedFormData?.validate, validateWrapper, } }, render() { const defaultSlot = () => this.$slots.default?.({ errors: this.errors, fieldsErrors: this.fieldsErrors, formData: this.formData, invalid: this.invalid, readonly: this.readonly, clear: this.clear, reset: this.reset, submit: this.submit, validate: this.validate, validateWrapper: this.validateWrapper, }) if (this.tag) { return h(this.tag, null, { default: defaultSlot, }) } return defaultSlot() }, }) }