refactor: CardForm ts + composition

This commit is contained in:
axolotle 2024-07-06 12:48:15 +02:00
parent f8b7f89488
commit 3c3f50ca60
3 changed files with 130 additions and 30 deletions

View file

@ -1,79 +1,161 @@
<script setup lang="ts"> <script
import type { BaseValidation } from '@vuelidate/core' setup
import { computed } from 'vue' lang="ts"
generic="
MV extends Obj,
FFD extends FormFieldDict<MV>,
V extends Validation<
ValidationArgs<unknown>,
{ form: Ref<MV>; global: null }
>
"
>
import type { Validation, ValidationArgs } from '@vuelidate/core'
import { computed, toValue } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import type { VueClass } from '@/types/commons' import { toEntries } from '@/helpers/commons'
import type { Obj, VueClass } from '@/types/commons'
import type {
AnyDisplayComponents,
AnyWritableComponents,
BaseItemComputedProps,
FormFieldDict,
} from '@/types/form'
import { isDisplayComponent, isWritableComponent } from '@/types/form'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
id?: string id?: string
modelValue?: MV
fields?: FFD
validations?: V
submitText?: string submitText?: string
validation?: BaseValidation
serverError?: string
inline?: boolean inline?: boolean
formClasses?: VueClass formClasses?: VueClass
noFooter?: boolean noFooter?: boolean
hr?: boolean
}>(), }>(),
{ {
id: 'ynh-form', id: 'ynh-form',
modelValue: undefined,
fields: undefined,
validations: undefined,
submitText: undefined, submitText: undefined,
validation: undefined,
serverError: '',
inline: false, inline: false,
formClasses: undefined, formClasses: undefined,
noFooter: false, noFooter: false,
hr: false,
}, },
) )
const emit = defineEmits<{ const emit = defineEmits<{
submit: [e: SubmitEvent] submit: [e: SubmitEvent]
'update:modelValue': [modelValue: MV]
}>() }>()
const slots = defineSlots<
{
disclaimer?: any
default?: any
'server-error'?: any
buttons: any
} & {
[K in Extract<keyof MV, string> as `field:${K}`]?: (_: FFD[K]) => any
} & {
[K in Extract<keyof FFD, string> as `component:${K}`]?: (
_: FFD[K]['component'] extends AnyWritableComponents
? FFD[K]['props'] & BaseItemComputedProps<MV[K]>
: FFD[K]['component'] extends AnyDisplayComponents
? FFD[K]['props']
: never,
) => any
}
>()
const { t } = useI18n() const { t } = useI18n()
const errorFeedback = computed(() => { const globalErrorFeedback = computed(() => {
const v = props.validation const v = props.validations
return ( if (!v) return ''
props.serverError || const externalResults = toValue(v.global.$externalResults[0]?.$message)
(v && v.$errors.length ? t('form_errors.invalid_form') : '') return externalResults ?? (v.form.$error ? t('form_errors.invalid_form') : '')
)
}) })
function onSubmit(e: Event) { const fields = computed(() => (props.fields ? toEntries(props.fields) : []))
const v = props.validation
if (v) { function onModelUpdate(key: keyof MV, value: MV[keyof MV]) {
v.$touch() emit('update:modelValue', {
if (v.$pending || v.$errors.length) return ...props.modelValue!,
} [key]: value,
emit('submit', e as SubmitEvent) })
} }
</script> </script>
<template> <template>
<!-- FIXME inheritAttrs false? probably remove vbind instead --> <YCard class="card-form">
<YCard v-bind="$attrs" class="card-form">
<template #default> <template #default>
<slot name="disclaimer" /> <slot name="disclaimer" />
{{ serverError }}
<BForm <BForm
:id="id" :id="id"
:inline="inline" :inline="inline"
:class="formClasses" :class="formClasses"
@submit.prevent.stop="onSubmit"
novalidate novalidate
@submit.prevent.stop="emit('submit', $event as SubmitEvent)"
> >
<slot name="default" /> <slot name="default">
<template v-for="[key, fieldProps] in fields" :key="key">
<template v-if="fieldProps.visible ?? true">
<slot
v-if="isWritableComponent<MV[typeof key]>(fieldProps)"
:name="`field:${key}`"
v-bind="fieldProps"
>
<FormField
v-if="!fieldProps.readonly"
v-bind="fieldProps"
:model-value="props.modelValue![key]"
:validation="props.validations?.form[key]"
@update:model-value="onModelUpdate(key, $event)"
>
<template
v-if="slots[`component:${key}`]"
#default="childProps"
>
<slot :name="`component:${key}`" v-bind="childProps" />
</template>
</FormField>
<FormFieldReadonly
v-else
v-bind="fieldProps"
:model-value="props.modelValue![key]"
/>
</slot>
<slot
v-else-if="isDisplayComponent(fieldProps)"
:name="`component:${key}`"
v-bind="fieldProps.props"
>
<Component
:is="fieldProps.component"
v-bind="fieldProps.props"
/>
</slot>
<hr v-if="fieldProps.hr ?? hr" />
</template>
</template>
</slot>
<slot name="server-error"> <slot name="server-error">
<BAlert <BAlert
variant="danger" variant="danger"
class="my-3" class="my-3"
icon="ban" icon="ban"
:modelValue="errorFeedback !== ''" :model-value="globalErrorFeedback !== ''"
> >
<div v-html="errorFeedback" /> <div v-html="globalErrorFeedback" />
</BAlert> </BAlert>
</slot> </slot>
</BForm> </BForm>
@ -82,7 +164,7 @@ function onSubmit(e: Event) {
<template v-if="!noFooter" #buttons> <template v-if="!noFooter" #buttons>
<slot name="buttons"> <slot name="buttons">
<BButton type="submit" variant="success" :form="id"> <BButton type="submit" variant="success" :form="id">
{{ submitText ? submitText : $t('save') }} {{ submitText ?? $t('save') }}
</BButton> </BButton>
</slot> </slot>
</template> </template>

View file

@ -145,6 +145,13 @@ export function getFileContent(
}) })
} }
export function toEntries<T extends Record<PropertyKey, unknown>>(
obj: T,
): { [K in keyof T]: [K, T[K]] }[keyof T][] {
return Object.entries(obj) as { [K in keyof T]: [K, T[K]] }[keyof T][]
}
}
export function omit<T extends Obj, K extends (keyof T)[]>( export function omit<T extends Obj, K extends (keyof T)[]>(
obj: T, obj: T,
keys: K, keys: K,

View file

@ -5,7 +5,7 @@ import type {
} from '@vuelidate/core' } from '@vuelidate/core'
import type { RouteLocationRaw } from 'vue-router' import type { RouteLocationRaw } from 'vue-router'
import type { ArrInnerType, Cols } from '@/types/commons' import type { ArrInnerType, Cols, Obj } from '@/types/commons'
type StateValidation = false | null type StateValidation = false | null
type StateVariant = 'success' | 'info' | 'warning' | 'danger' type StateVariant = 'success' | 'info' | 'warning' | 'danger'
@ -264,3 +264,14 @@ export type FormFieldReadonlyProps<
> = Omit<FormFieldReadonly<C>, 'hr' | 'visible' | 'readonly'> & { > = Omit<FormFieldReadonly<C>, 'hr' | 'visible' | 'readonly'> & {
modelValue?: MV modelValue?: MV
} }
export type FormFieldDict<T extends Obj = Obj> = {
[k in keyof T | string]: k extends keyof T
?
| FormField<AnyWritableComponents, T[k]>
| FormFieldReadonly<AnyWritableComponents>
:
| FormField<AnyWritableComponents>
| FormFieldReadonly<AnyWritableComponents>
| FormFieldDisplay<AnyDisplayComponents>
}