@volverjs/form-vue
Version:
Vue 3 Forms with @volverjs/ui-vue
210 lines (200 loc) • 7.46 kB
text/typescript
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()
},
})
}