feat: add useForm composable

This commit is contained in:
axolotle 2024-07-06 16:13:45 +02:00
parent 3054c3ec5c
commit 85b4980bee
3 changed files with 114 additions and 3 deletions

View file

@ -1,6 +1,17 @@
import type { BaseValidation } from '@vuelidate/core' import type {
import type { InjectionKey, MaybeRefOrGetter } from 'vue' BaseValidation,
import { inject, provide, toValue } from 'vue' ServerErrors,
ValidationArgs,
ValidationRuleCollection,
} from '@vuelidate/core'
import useVuelidate from '@vuelidate/core'
import type { InjectionKey, MaybeRefOrGetter, Ref } from 'vue'
import { inject, provide, reactive, toValue } from 'vue'
import { computedWithControl } from '@vueuse/core'
import { APIBadRequestError, type APIError } from '@/api/errors'
import type { Obj } from '@/types/commons'
import type { FormField, FormFieldDict } from '@/types/form'
export const clearServerErrorsSymbol = Symbol() as InjectionKey< export const clearServerErrorsSymbol = Symbol() as InjectionKey<
(key?: string) => void (key?: string) => void
@ -30,3 +41,90 @@ export function useTouch(
return touch return touch
} }
export function useForm<
MV extends Obj,
FFD extends FormFieldDict<MV> = FormFieldDict<MV>,
>(form: Ref<MV>, fields: MaybeRefOrGetter<FFD>) {
const serverErrors = reactive<ServerErrors>({})
const validByDefault: ValidationRuleCollection = { true: () => true }
const rules = computedWithControl(
() => toValue(fields),
() => {
const fs = toValue(fields)
const validations = Object.keys(form.value).map((key: keyof MV) => [
key,
(fs[key] as FormField).rules ?? validByDefault,
])
const rules: ValidationArgs<MV> = Object.fromEntries(validations)
return {
// create a fake validation rule for global state to be able to add $externalResult errors to it
global: { true: () => true },
form: rules,
}
},
)
const v = useVuelidate(
rules,
{ form, global: null },
{ $externalResults: serverErrors },
)
function onErrorFn(err: APIError, errorMessage?: string) {
if (!(err instanceof APIBadRequestError)) throw err
if (errorMessage || !err.data.name) {
serverErrors.global = [errorMessage || err.message]
} else {
deepSetErrors(
serverErrors,
[err.message],
'form',
...err.data.name.split('.'),
)
}
}
function onSubmit(
fn: (onError: typeof onErrorFn, serverErrors: ServerErrors) => void,
) {
// FIXME add option to ask confirmation (with param text confirm)
return async (e: SubmitEvent) => {
e.preventDefault()
if (!(await v.value.$validate())) return
fn(onErrorFn, serverErrors)
}
}
provide(clearServerErrorsSymbol, (key?: string) => {
const keys = key?.split('.')
if (keys?.length) {
deepSetErrors(serverErrors, [], ...keys)
}
})
return {
v,
serverErrors,
onSubmit,
}
}
export function deepSetErrors(
serverErrors: ServerErrors,
value: string[],
...keys: string[]
) {
const [k, ...ks] = keys
if (ks.length) {
if (!(k in serverErrors) && !value.length) {
serverErrors[k] = {}
deepSetErrors(serverErrors[k] as ServerErrors, value, ...ks)
} else if (k in serverErrors) {
deepSetErrors(serverErrors[k] as ServerErrors, value, ...ks)
}
} else {
if (!(k in serverErrors) && !value.length) return
serverErrors[k] = value
}
}

View file

@ -162,3 +162,7 @@ export function omit<T extends Obj, K extends (keyof T)[]>(
.map((key) => [key, obj[key]]), .map((key) => [key, obj[key]]),
) as Omit<T, K[number]> ) as Omit<T, K[number]>
} }
export function asUnreffed<T>(value: T): UnwrapRef<T> {
return value as UnwrapRef<T>
}

View file

@ -275,3 +275,12 @@ export type FormFieldDict<T extends Obj = Obj> = {
| FormFieldReadonly<AnyWritableComponents> | FormFieldReadonly<AnyWritableComponents>
| FormFieldDisplay<AnyDisplayComponents> | FormFieldDisplay<AnyDisplayComponents>
} }
export type FieldProps<
C extends AnyItemComponents = 'InputItem',
MV extends any = never,
> = C extends AnyWritableComponents
? FormField<C, MV> | FormFieldReadonly<C>
: C extends AnyDisplayComponents
? FormFieldDisplay<C>
: never