๐ŸŸข Form group


Basic

NFormGroup - a wrapper component for NInput, NTextarea, Select, and other form components. It provides a label, description, hint, message, status and other features.

Notice that when you click on the label, the input gets focused. By default, we automatically add for attribute to the label and id attribute to the input. If you want to override this behavior, you can define for and id attributes manually.
<template>
<div flex>
  <NFormGroup
    label="Name"
  >
    <NInput />
  </NFormGroup>
</div>
</template>

Required

required - adds * to the label.

<template>
<div flex>
  <NFormGroup
    label="Email"
    required
  >
    <NInput
      placeholder="phojrengel@gmail.com"
      leading="i-heroicons-envelope-20-solid"
    />
  </NFormGroup>
</div>
</template>

Description

description - displays description text.

We'll never share your email with anyone else.
<template>
<div flex>
  <NFormGroup
    label="Email"
    required
    description="We'll never share your email with anyone else."
  >
    <NInput
      placeholder="phojrengel@gmail.com"
      leading="i-heroicons-envelope-20-solid"
    />
  </NFormGroup>
</div>
</template>

Hint

hint - displays hint text.

Optional
<template>
<div flex>
  <NFormGroup
    label="Email"
    hint="Optional"
  >
    <NInput
      placeholder="phojrengel@gmail.com"
      leading="i-heroicons-envelope-20-solid"
    />
  </NFormGroup>
</div>
</template>

Message

message - displays message text. Useful in combination with status prop.

We'll never share your email with anyone else.

<template>
<div flex>
  <NFormGroup
    label="Email"
    message="We'll never share your email with anyone else."
  >
    <NInput
      placeholder="phojrengel@gmail.com"
      leading="i-heroicons-envelope-20-solid"
    />
  </NFormGroup>
</div>
</template>

Status

status - changes the status of the form group. Useful for displaying validation status.

Possible values: info, success, warning, error.

Notice that when you change the status prop, the message prop and the child component status prop are automatically updated.

Your username is available.

This information will be visible to other users.

Your email is invalid

Your password is weak.

<script setup lang="ts">
const form = ref({
username: 'Phojie',
about: '',
email: '',
password: 'Password',
})
</script>

<template>
<div flex="~ col" gap-4>
  <NFormGroup
    label="Username"
    message="Your username is available."
    status="success"
  >
    <NInput
      v-model="form.username"
    />
  </NFormGroup>

  <NFormGroup
    label="About Me"
    status="info"
    message="This information will be visible to other users."
  >
    <!-- TODO: change to NTextArea -->
    <NInput v-model="form.about" />
  </NFormGroup>

  <NFormGroup
    label="Email"
    status="error"
    message="Your email is invalid"
  >
    <NInput
      v-model="form.email"
    />
  </NFormGroup>

  <NFormGroup
    label="Password"
    message="Your password is weak."
    status="warning"
  >
    <NInput
      v-model="form.password"
      type="password"
    />
  </NFormGroup>
</div>
</template>

Counter

counter.value - displays counter text, useful for displaying the number of characters in the input.

counter.max - the maximum number of characters.

Username has no length limit

0
0/10
<script setup lang="ts">
const username = ref('')
</script>

<template>
<div grid="~ cols-1 sm:cols-2" gap-4>
  <NFormGroup
    label="Username"
    :counter="{
      value: username.length,
    }"
    message="Username has no length limit"
  >
    <!-- TODO: update to NTextArea later -->
    <NInput
      v-model="username"
      leading="i-heroicons-user-20-solid"
    />
  </NFormGroup>

  <NFormGroup
    label="Username"
    :counter="{
      value: username.length,
      max: 10,
    }"
    :status="username.length > 10 ? 'error' : undefined"
    :message="username.length > 10 ? 'Username must be less than 10 characters' : undefined"
  >
    <!-- TODO: update to NTextArea later -->
    <NInput
      v-model="username"
      leading="i-heroicons-user-20-solid"
    />
  </NFormGroup>
</div>
</template>

Slots

NameDescription
defaultThe default slot of the form group, refer Basic section.
topThe top section of the form group.
bottomThe bottom section of the form group.
labelThe label slot of the form group.
descriptionThe description slot of the form group.
hintThe hint slot of the form group.
messageThe message slot of the form group.
counterThe counter slot of the form group.

Props

export interface NFormGroupProps {
  /**
   * Update the form group status.
   *
   * @default null
   */
  status?: 'info' | 'success' | 'warning' | 'error'
  /**
   * Add a required indicator to the form group.
   *
   * @default false
   */
  required?: boolean

  /**
   * Manually set the for attribute.
   *
   * By default, the for attribute is synced with the id attribute for accessibility reasons.
   * You can disable this behavior by setting `for` to `false`.
   *
   * @default randomId
   * @example
   * for="email"
   */
  for?: string | boolean
  /**
   * Manually set the id attribute.
   *
   * By default, the id attribute is generated randomly for accessibility reasons.
   *
   * @default randomId
   * @example
   * id="email"
   */
  id?: string
  /**
   * Label for the form group.
   *
   * @example
   * label="Email"
   */
  label?: string
  /**
   * Display `hint` message for the form group.
   *
   * @example
   * hint="Enter your email address"
   */
  hint?: string
  /**
   * Display `Description` message for the form group.
   *
   * @example
   * description="We will never share your email with anyone else."
   */
  description?: string
  /**
   * Display `Message` for the form group.
   * Useful for displaying validation errors.
   *
   * @example
   * message="Email is required"
   */
  message?: string

  /**
   * Display `counter` for the form group.
   * Useful for displaying character count.
   *
   * @example
   * counter="{ value: 0, max: 100 }"
   */
  counter?: {
    value: number
    max?: number
  }

  /**
   * `UnaUI` preset configuration
   *
   * @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/form-group.ts
   */
  una?: {
    formGroupTopWrapper?: string
    formGroupTopWrapperInner?: string
    formGroupBottomWrapper?: string
    formGroupCounterWrapper?: string
    formGroupMessageWrapper?: string
    formGroupLabelWrapper?: string

    formGroupLabel?: string
    formGroupDescription?: string
    formGroupHint?: string
    formGroupMessage?: string

    formGroupLabelRequired?: string
  }
}

Presets

type FormGroupPrefix = 'form-group'

export const staticFormGroup: Record<`${FormGroupPrefix}-${string}` | FormGroupPrefix, string> = {
  // base
  'form-group': 'space-y-2 flex flex-col',
  'form-group-description': 'text-sm leading-6 text-$c-gray-500',
  'form-group-hint': 'text-sm leading-6 text-$c-gray-500',
  'form-group-message': 'text-sm transition-all duration-1000 ease-in-out',

  // wrappers
  'form-group-top-wrapper': 'flex flex-col',
  'form-group-top-wrapper-inner': 'flex justify-between items-end space-x-1.5',
  'form-group-bottom-wrapper': 'flex space-x-1.5 justify-between items-start',
  'form-group-message-wrapper': '',

  // label
  'form-group-label-wrapper': 'flex',
  'form-group-label': 'block text-sm leading-6 font-medium text-$c-gray-900',
  'form-group-label-required': 'after:content-[\'*\'] after:ms-0.5 after:text-error',

  // counter
  'form-group-counter-wrapper': 'text-sm',
  'form-group-counter-error': 'text-error',
  'form-group-counter-current': 'text-$c-gray-900',
  'form-group-counter-separator': 'text-$c-gray-500',
  'form-group-counter-max': 'text-$c-gray-500',
}

export const formGroup = [
  staticFormGroup,
]

Component

<script setup lang="ts">
import { computed } from 'vue'
import type { NFormGroupProps } from '../../types'
import NFormGroupDefaultSlot from '../slots/FormGroupDefault'
import FormGroupLabel from '../slots/FormGroupLabel'
import { randomId } from '../../utils'

const props = withDefaults(defineProps<NFormGroupProps>(), {
  for: undefined,
})

const id = computed(() => props.id ?? randomId('form-group'))

const statusClassVariants = computed(() => {
  const text = {
    info: 'text-info',
    success: 'text-success',
    warning: 'text-warning',
    error: 'text-error',
    default: 'text-$c-gray-500',
  }

  return text[props.status ?? 'default']
})
</script>

<template>
  <div form-group>
    <slot name="top">
      <div
        form-group="message-wrapper"
        :class="una?.formGroupMessageWrapper"
      >
        <div
          v-if="label || hint || description"
          form-group="top-wrapper"
          :class="una?.formGroupTopWrapper"
        >
          <div
            v-if="label || hint"
            form-group="top-wrapper-inner"
            :class="una?.formGroupTopWrapperInner"
          >
            <slot name="label">
              <FormGroupLabel
                :id="id"
                :for="props.for"
                form-group="label-wrapper"
                :class="una?.formGroupLabelWrapper"
              >
                <span
                  form-group="label"
                  :class="una?.formGroupLabel"
                >
                  {{ label }}
                </span>
                <span
                  v-if="required"
                  form-group="label-required"
                  :class="una?.formGroupLabelRequired"
                />
              </FormGroupLabel>
            </slot>

            <slot name="hint">
              <span
                v-if="hint"
                form-group="hint"
                :class="una?.formGroupHint"
              >
                {{ hint }}
              </span>
            </slot>
          </div>

          <slot name="description">
            <span
              v-if="description"
              form-group="description"
              :class="una?.formGroupDescription"
            >
              {{ description }}
            </span>
          </slot>
        </div>
      </div>
    </slot>

    <NFormGroupDefaultSlot
      :id="id"
      :status="status"
    >
      <slot />
    </NFormGroupDefaultSlot>

    <slot name="bottom">
      <div
        v-if="message || counter"
        form-group="bottom-wrapper"
        :class="[
          { 'justify-end': !message && counter },
          una?.formGroupBottomWrapper,
        ]"
      >
        <slot name="message">
          <div
            v-if="message"
            form-group="message-wrapper"
            :class="una?.formGroupMessageWrapper"
          >
            <p
              form-group="message"
              :class="[
                una?.formGroupMessage,
                statusClassVariants,
              ]"
            >
              {{ message }}
            </p>
          </div>
        </slot>

        <slot name="counter">
          <div
            v-if="counter"
            form-group="counter-wrapper"
            :class="una?.formGroupCounterWrapper"
          >
            <span
              :class="`${counter?.value >= (counter?.max || 0) && counter?.max
                ? 'form-group-counter-error'
                : 'form-group-counter-current'}`"
            >
              {{ counter?.value }}
            </span>
            <span v-if="counter?.max" form-group="counter-separator">/</span>
            <span v-if="counter?.max" form-group="counter-max">{{ counter?.max }}</span>
          </div>
        </slot>
      </div>
    </slot>
  </div>
</template>