@volverjs/form-vue
Version:
Vue 3 Forms with @volverjs/ui-vue
172 lines (164 loc) • 3.7 kB
text/typescript
import {
type InjectionKey,
type Ref,
computed,
defineComponent,
inject,
provide,
readonly,
ref,
toRefs,
watch,
h,
type DeepReadonly,
} from 'vue'
import type { TypeOf, z } from 'zod'
import type {
FormSchema,
InjectedFormData,
InjectedFormWrapperData,
} from './types'
export const defineFormWrapper = <Schema extends FormSchema>(
formProvideKey: InjectionKey<InjectedFormData<Schema>>,
wrapperProvideKey: InjectionKey<InjectedFormWrapperData<Schema>>,
) => {
const VvFormWrapper = defineComponent({
name: 'WrapperComponent',
props: {
name: {
type: String,
required: true,
},
tag: {
type: String,
default: undefined,
},
},
emits: ['invalid', 'valid'],
expose: ['fields', 'invalid'],
setup(props, { emit }) {
const injectedFormData = inject(formProvideKey)
const wrapperProvided = inject(wrapperProvideKey, undefined)
const fields = ref(new Set<string>())
const fieldsErrors: Ref<
Map<string, z.inferFormattedError<Schema>>
> = ref(new Map())
const { name } = toRefs(props)
// provide data to child fields
provide(wrapperProvideKey, {
name: readonly(name),
errors: fieldsErrors,
fields,
})
// add fields to parent wrapper
watch(
fields,
(newValue) => {
if (wrapperProvided?.fields) {
newValue.forEach((field) => {
wrapperProvided?.fields.value.add(field)
})
}
},
{ deep: true },
)
// add fields to parent wrapper
watch(
() => new Map(fieldsErrors.value),
(newValue, oldValue) => {
if (wrapperProvided?.errors) {
Array.from(oldValue.keys()).forEach((key) => {
wrapperProvided.errors.value.delete(key)
})
Array.from(newValue.keys()).forEach((key) => {
const value = newValue.get(key)
if (value) {
wrapperProvided.errors.value.set(key, value)
}
})
}
},
{ deep: true },
)
const invalid = computed(() => {
if (!injectedFormData?.invalid.value) {
return false
}
return fieldsErrors.value.size > 0
})
watch(invalid, () => {
if (invalid.value) {
emit('invalid')
} else {
emit('valid')
}
})
return {
formData: injectedFormData?.formData,
errors: injectedFormData?.errors,
submit: injectedFormData?.submit,
validate: injectedFormData?.validate,
invalid,
fields,
fieldsErrors,
}
},
render() {
if (this.tag) {
return h(this.tag, null, {
default: () =>
this.$slots.default?.({
invalid: this.invalid,
formData: this.formData,
submit: this.submit,
validate: this.validate,
errors: this.errors,
fieldsErrors: this.fieldsErrors,
}) ?? this.$slots.defalut,
})
}
return (
this.$slots.default?.({
invalid: this.invalid,
formData: this.formData,
submit: this.submit,
validate: this.validate,
errors: this.errors,
fieldsErrors: this.fieldsErrors,
}) ?? this.$slots.defalut
)
},
})
/**
* An hack to add types to the default slot
*/
return VvFormWrapper as typeof VvFormWrapper & {
new (): {
$slots: {
default: (_: {
invalid: boolean
formData: unknown extends
| Partial<TypeOf<Schema>>
| undefined
? undefined
: Partial<TypeOf<Schema>> | undefined
submit: () => boolean
validate: () => boolean
errors: Readonly<
Ref<DeepReadonly<z.inferFormattedError<Schema>>>
>
fieldsErrors: Map<
string,
Record<
string,
{
_errors: string[]
}
>
>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) => any
}
}
}
}