Newer
Older
<div
class="form-group last:tw-mb-0"
:class="[
formGroupClass,
{
'tw-block': !inline,
'tw-flex tw-gap-2 tw-items-center': inline,
},
]"
>
class="form-group-description tw-flex tw-gap-x-2 tw-gap-y-1 empty:tw-hidden"
:class="[
center ? 'tw-items-center tw-self-center' : 'tw-items-baseline',
{ 'tw-mb-2': !inline },
]"
<label
v-if="label"
:for="id"
class="tw-text-gray-500 tw-font-medium tw-flex tw-gap-2 tw-items-center tw-grow tw-m-0"
>
<span>{{ label }}</span>
<button v-if="withEditButton" type="button" class="btn btn-sm tw-p-0" @click="emit('edit')">
<icon-system-uicons-pen />
</button>
</label>
<SaveIndicator
v-if="typeof isSaving === 'boolean'"
class="tw-transition-opacity tw-ease-in-out tw-duration-150"
:class="{ 'tw-invisible tw-opacity-0': !isSavingDebounced }"
/>
<div class="tw-grid tw-order-first">
<slot v-bind="controlAttributes" />
<slot name="iconLeft" :class="[iconClasses, 'tw-ml-2']" role="presentation" />
<slot name="iconRight" :class="[iconClasses, 'tw-mr-2']" role="presentation" />
</div>
<div
v-if="hasErrors"
:id="errorsId"
class="invalid-feedback"
:class="{ 'tw-block': hasErrors }"
>
<template v-for="(error, index) in errorList" :key="index">
<p class="last:tw-mb-0">
{{
error.code
? t(`error.${error.code}`)
: error.message
? error.message
: t('error.unknown')
}}
</p>
</template>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, inject, useSlots, watchEffect } from 'vue'
import { computedDebounced, useId } from '@/util'
import SaveIndicator from '@/components/generic/SaveIndicator.vue'
defineOptions({ compatConfig: { MODE: 3 } })
message?: string
const props = withDefaults(
defineProps<{
label?: string
customControl?: boolean
errors?: undefined | (Error | undefined | null)[]
withEditButton?: boolean
isSaving?: boolean | undefined
}>(),
{
isSaving: undefined,
label: '',
errors: undefined,
const emit = defineEmits<{
edit: []
const slots = useSlots()
const formGroupClass = inject('formGroupClass', undefined)
const id = useId('form-group-control')
const errorsId = useId('form-group-errors')
const errorList = computed<Error[]>(() => (props.errors ?? []).filter((e): e is Error => !!e))
const hasErrors = computed(() => errorList.value.length > 0)
const iconClasses = 'tw-pointer-events-none tw-grid-area-cover tw-self-center'
const controlAttributes = computed(() => ({
class: [
'tw-grid-area-cover',
{
'is-invalid': hasErrors.value,
'form-control': !props.customControl,
'tw-pl-8': slots.iconLeft,
'tw-pr-8': slots.iconRight,
},
],
'aria-describedby': hasErrors.value ? errorsId.value : '',
id: id.value,
}))
let isSavingStart = new Date()
const isSavingDebounced = computedDebounced(
() => props.isSaving,
(isSaving) => {
if (isSaving) return 0
const now = new Date()
const timePassed = (now.getTime() - isSavingStart.getTime()) / 1000
return timePassed < 1 ? 1 - timePassed : 0
},
)
watchEffect(() => {
if (props.isSaving) isSavingStart = new Date()
})