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">
import type { BaseValidation } from '@vuelidate/core'
import { computed } from 'vue'
<script
setup
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 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(
defineProps<{
id?: string
modelValue?: MV
fields?: FFD
validations?: V
submitText?: string
validation?: BaseValidation
serverError?: string
inline?: boolean
formClasses?: VueClass
noFooter?: boolean
hr?: boolean
}>(),
{
id: 'ynh-form',
modelValue: undefined,
fields: undefined,
validations: undefined,
submitText: undefined,
validation: undefined,
serverError: '',
inline: false,
formClasses: undefined,
noFooter: false,
hr: false,
},
)
const emit = defineEmits<{
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 errorFeedback = computed(() => {
const v = props.validation
return (
props.serverError ||
(v && v.$errors.length ? t('form_errors.invalid_form') : '')
)
const globalErrorFeedback = computed(() => {
const v = props.validations
if (!v) return ''
const externalResults = toValue(v.global.$externalResults[0]?.$message)
return externalResults ?? (v.form.$error ? t('form_errors.invalid_form') : '')
})
function onSubmit(e: Event) {
const v = props.validation
if (v) {
v.$touch()
if (v.$pending || v.$errors.length) return
}
emit('submit', e as SubmitEvent)
const fields = computed(() => (props.fields ? toEntries(props.fields) : []))
function onModelUpdate(key: keyof MV, value: MV[keyof MV]) {
emit('update:modelValue', {
...props.modelValue!,
[key]: value,
})
}
</script>
<template>
<!-- FIXME inheritAttrs false? probably remove vbind instead -->
<YCard v-bind="$attrs" class="card-form">
<YCard class="card-form">
<template #default>
<slot name="disclaimer" />
{{ serverError }}
<BForm
:id="id"
:inline="inline"
:class="formClasses"
@submit.prevent.stop="onSubmit"
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">
<BAlert
variant="danger"
class="my-3"
icon="ban"
:modelValue="errorFeedback !== ''"
:model-value="globalErrorFeedback !== ''"
>
<div v-html="errorFeedback" />
<div v-html="globalErrorFeedback" />
</BAlert>
</slot>
</BForm>
@ -82,7 +164,7 @@ function onSubmit(e: Event) {
<template v-if="!noFooter" #buttons>
<slot name="buttons">
<BButton type="submit" variant="success" :form="id">
{{ submitText ? submitText : $t('save') }}
{{ submitText ?? $t('save') }}
</BButton>
</slot>
</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)[]>(
obj: T,
keys: K,

View file

@ -5,7 +5,7 @@ import type {
} from '@vuelidate/core'
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 StateVariant = 'success' | 'info' | 'warning' | 'danger'
@ -264,3 +264,14 @@ export type FormFieldReadonlyProps<
> = Omit<FormFieldReadonly<C>, 'hr' | 'visible' | 'readonly'> & {
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>
}