mirror of
https://github.com/YunoHost/yunohost-admin.git
synced 2024-09-03 20:06:15 +02:00
feat: add useForm composable
This commit is contained in:
parent
3054c3ec5c
commit
85b4980bee
3 changed files with 114 additions and 3 deletions
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue