Skip to content
Snippets Groups Projects
FormGroup.vue 3.46 KiB
Newer Older
  • Learn to ignore specific revisions
  • <template>
    
      <div class="form-group tw-block last:tw-mb-0" :class="formGroupClass">
    
        <div
          class="form-group-description tw-flex tw-gap-x-2 tw-gap-y-1 tw-mb-2 empty:tw-hidden"
          :class="center ? 'tw-items-center tw-self-center' : 'tw-items-baseline'"
        >
    
            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-flex tw-flex-col">
    
          <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 { useI18n } from '@/i18n'
    
    import SaveIndicator from '@/components/generic/SaveIndicator.vue'
    
    defineOptions({ compatConfig: { MODE: 3 } })
    
    
    type Error = {
    
    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 { t } = useI18n()
    
    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()
    })