Form

Building forms with VeeValidate and Zod.

Examples

Basic

PropDefaultTypeDescription
label-stringAdds a label to the form field.
name-stringAdds a name to the form field.

Building form with vee-validate and zod using FormField component.

Preview
Code

Required

PropDefaultTypeDescription
requiredfalsebooleanAdds * to the label.
Preview
Code

Description

PropDefaultTypeDescription
description-stringAdds a description to the form field.
Preview
Code

We'll never share your email with anyone else.

Hint

PropDefaultTypeDescription
hint-stringAdds a hint to the form field.
Preview
Code
Optional

Message

PropDefaultTypeDescription
message-stringSets the form field's message.
Preview
Code

We'll never share your email with anyone else.

Status

PropDefaultTypeDescription
status-info,success, warning, error, undefinedSets the form field's status.
Preview
Code

Your username is available.

This information will be visible to other users.

Your email is invalid

Your password is weak.

Slots

NamePropsDescription
default-The default slot of the form field, refer Basic section.
top-The top section of the form field.
bottom-The bottom section of the form field.
label-The label slot of the form field.
description-The description slot of the form field.
hint-The hint slot of the form field.
message-The message slot of the form field.

Props

types/form.ts
import type { HTMLAttributes } from 'vue'
import type { NLabelProps } from './label'

export interface NFormFieldProps extends NLabelProps {
  class?: HTMLAttributes['class']
  /**
   * Update the form field status.
   *
   * @default null
   */
  status?: 'info' | 'success' | 'warning' | 'error'
  /**
   * Add a required indicator to the form field.
   *
   * @default false
   */
  required?: boolean
  /**
   * Add a name attribute to the form field.
   *
   */
  name: string
  /**
   * 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 field.
   *
   * @example
   * label="Email"
   */
  label?: string
  /**
   * Display `hint` message for the form field.
   *
   * @example
   * hint="Enter your email address"
   */
  hint?: any
  /**
   * Display `Description` message for the form field.
   *
   * @example
   * description="We will never share your email with anyone else."
   */
  description?: any
  /**
   * Display `Message` for the form field.
   * Useful for displaying validation errors.
   *
   * @example
   * message="Email is required"
   */
  message?: any

  /**
   * `UnaUI` preset configuration
   *
   * @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/form.ts
   */
  una?: NFormUnaProps
}

export interface NFormMessageProps {
  class?: HTMLAttributes['class']

  una?: NFormUnaProps['formMessage']
}

export interface NFormLabelProps extends NLabelProps {
  una?: NFormUnaProps['formLabel']
}

export interface NFormItemProps {
  class?: HTMLAttributes['class']

  una?: NFormUnaProps['formItem']
}

export interface NFormDescriptionProps {
  class?: HTMLAttributes['class']

  una?: NFormUnaProps['formDescription']
}

interface NFormUnaProps {
  formField?: HTMLAttributes['class']

  formFieldTopWrapper?: HTMLAttributes['class']
  formFieldTopWrapperInner?: HTMLAttributes['class']
  formFieldBottomWrapper?: HTMLAttributes['class']
  formFieldCounterWrapper?: HTMLAttributes['class']
  formFieldMessageWrapper?: HTMLAttributes['class']
  formFieldLabelWrapper?: HTMLAttributes['class']

  formFieldHint?: HTMLAttributes['class']
  formMessage?: HTMLAttributes['class']
  formLabel?: HTMLAttributes['class']
  formItem?: HTMLAttributes['class']
  formDescription?: HTMLAttributes['class']

  formFieldLabelRequired?: HTMLAttributes['class']
}

Presets

shortcuts/form.ts
type FormPrefix = 'form'

export const staticForm: Record<`${FormPrefix}-${string}` | FormPrefix, string> = {
  // base
  'form': '',

  'form-field': '',
  'form-field-description': 'text-muted',
  'form-field-hint': 'text-sm leading-none text-muted',
  'form-field-message': '',

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

  // label
  'form-field-label-wrapper': 'flex',
  'form-field-label': 'block',
  'form-field-label-required': 'after:content-[\'*\'] after:ms-0.5 after:text-error',

  'form-message': 'text-0.8rem font-medium transition-all duration-1000 ease-in-out text-error',
  'form-label': '',
  'form-description': 'text-sm text-muted',
  'form-item': 'space-y-2',
}

export const form = [
  staticForm,
]

Components

FormField.vue
FormControl.vue
FormDescription.vue
FormHint.vue
FormLabel.vue
FormMessage.vue
<script setup lang="ts">
import type { NFormFieldProps } from '../../../types'
import { Field } from 'vee-validate'
import { computed } from 'vue'
import { cn } from '../../../utils'
import FormFieldDefaultSlot from '../../slots/FormFieldDefault'
import FormControl from './FormControl.vue'
import FormDescription from './FormDescription.vue'
import FormItem from './FormItem.vue'
import FormLabel from './FormLabel.vue'
import FormMessage from './FormMessage.vue'

defineOptions({
  inheritAttrs: false,
})

const props = defineProps<NFormFieldProps>()

const statusClassVariants = computed(() => {
  const text = {
    info: 'text-info',
    success: 'text-success',
    warning: 'text-warning',
    error: 'text-error',
    default: 'text-muted',
  }

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

<template>
  <Field
    v-slot="{ componentField, errorMessage }"
    :name
  >
    <FormItem
      :class="cn(
        props.class,
      )"
    >
      <slot name="top">
        <div
          :class="cn(
            'form-field-top-wrapper',
            una?.formFieldTopWrapper,
          )"
        >
          <div
            v-if="label || hint || description"
            :class="cn(
              'form-field-top-wrapper',
              una?.formFieldTopWrapper,
            )"
          >
            <div
              v-if="label || hint"
              :class="cn(
                'form-field-top-wrapper-inner',
                una?.formFieldTopWrapperInner,
              )"
            >
              <div
                :class="cn(
                  'form-field-label-wrapper',
                  una?.formFieldLabelWrapper,
                )"
              >
                <slot name="label">
                  <FormLabel
                    :una
                  >
                    <span>
                      {{ label }}
                    </span>

                    <span
                      v-if="required"
                      :class="cn(
                        'form-field-label-required',
                        una?.formFieldLabelRequired,
                      )"
                    />
                  </FormLabel>
                </slot>
              </div>

              <slot name="hint">
                <span
                  v-if="hint"
                  :class="cn(
                    'form-field-hint',
                    una?.formFieldHint,
                  )"
                >
                  {{ hint }}
                </span>
              </slot>
            </div>

            <slot name="description">
              <FormDescription
                v-if="description"
                :una
              >
                {{ description }}
              </FormDescription>
            </slot>
          </div>
        </div>
      </slot>

      <FormControl>
        <FormFieldDefaultSlot
          :status="!errorMessage ? status : 'error'"
          v-bind="componentField"
        >
          <slot />
        </FormFieldDefaultSlot>
      </FormControl>

      <slot name="bottom">
        <div
          :class="cn(
            'form-field-bottom-wrapper',
            una?.formFieldBottomWrapper,
          )"
        >
          <slot name="message">
            <div
              v-if="message && !errorMessage"
              :class="cn(
                'form-field-message-wrapper',
                una?.formFieldMessageWrapper,
              )"
            >
              <FormDescription
                :class="cn(
                  'form-field-message',
                  una?.formMessage,
                  statusClassVariants,
                )"
              >
                {{ message }}
              </FormDescription>
            </div>

            <FormMessage
              v-else
            />
          </slot>
        </div>
      </slot>
    </FormItem>
  </Field>
</template>