Files
policy-ui/.claude/skills/nuxt-ui/references/forms.md
HaimKortovich a2eb1f3789 Add nuxt-skills and update auto quotes to use new policy API structure
- Add nuxt-skills (vue, nuxt, nuxt-ui) to .claude/skills/
- Create useCustomerSelection() composable for managing insured/buyer selection
- Create usePolicyApi() composable for policy API operations
- Update auto quote components to use insured/buyer instead of client
- Update vehicle fields: remove valorVehiculo, add market_value, requested_value, rc_limits
- Make chassis_number and engine_number optional
- Update auto quote types and composables to match new API structure
- Update auto quote page to submit to policy API with new structure
2026-04-27 14:56:53 -05:00

8.6 KiB

Forms

Form Components

Component Purpose Key Props Version
UInput Text input type, placeholder, icon, loading
UTextarea Multi-line text rows, autoresize, maxrows
USelect Native select options, placeholder
USelectMenu Custom select options, searchable, multiple
UInputMenu Autocomplete options, searchable
UCheckbox Single checkbox label, description
UCheckboxGroup Multiple checkboxes items, orientation
URadioGroup Radio options items, orientation
USwitch Toggle switch label, description, on-icon, off-icon
USlider Range slider min, max, step
UPinInput Code/OTP input length, type, mask
UInputNumber Number input min, max, step, format-options
UInputDate Date picker mode, range, locale v4.2+
UInputTime Time picker hour-cycle, minute-step v4.2+
UInputTags Tag input max, placeholder
UColorPicker Color selection format, swatches
UFileUpload File upload accept, multiple, max-files

Basic Input Examples

<script setup>
const email = ref('')
const bio = ref('')
const country = ref('')
</script>

<template>
  <!-- Text input -->
  <UInput v-model="email" type="email" placeholder="Email" icon="i-heroicons-envelope" />

  <!-- With validation state -->
  <UInput v-model="email" :color="emailError ? 'error' : undefined" />

  <!-- Textarea -->
  <UTextarea v-model="bio" placeholder="Bio" :rows="3" autoresize />

  <!-- Select -->
  <USelect v-model="country" :options="['USA', 'Canada', 'Mexico']" placeholder="Country" />
</template>

SelectMenu (Custom Dropdown)

<script setup>
const selected = ref()
const options = [
  { label: 'John', value: 'john', avatar: { src: '/john.png' } },
  { label: 'Jane', value: 'jane', avatar: { src: '/jane.png' } }
]
</script>

<template>
  <USelectMenu
    v-model="selected"
    :options="options"
    searchable
    clear
    placeholder="Select user"
  >
    <template #option="{ option }">
      <UAvatar v-bind="option.avatar" size="xs" />
      <span>{{ option.label }}</span>
    </template>
  </USelectMenu>
</template>

SelectMenu/InputMenu Props (v4.4+)

<USelectMenu
  v-model="selected"
  :options="options"
  clear                <!-- Add clear button (v4.4+) -->
  :viewport-ref="ref"  <!-- For infinite scroll (v4.4+) -->
/>

Checkbox & Radio

<script setup>
const agreed = ref(false)
const plan = ref('free')
const features = ref([])
</script>

<template>
  <!-- Single checkbox -->
  <UCheckbox v-model="agreed" label="I agree to terms" description="Required" />

  <!-- Radio group -->
  <URadioGroup
    v-model="plan"
    :items="[
      { label: 'Free', value: 'free', description: '$0/mo' },
      { label: 'Pro', value: 'pro', description: '$10/mo' }
    ]"
  />

  <!-- Checkbox group -->
  <UCheckboxGroup
    v-model="features"
    :items="[
      { label: 'Dark mode', value: 'dark' },
      { label: 'Notifications', value: 'notifications' }
    ]"
  />
</template>

Form Validation

Uses Standard Schema (Zod, Valibot, Yup, Joi, etc.)

With Zod

<script setup lang="ts">
import { z } from 'zod'

const schema = z.object({
  email: z.string().email('Invalid email'),
  password: z.string().min(8, 'Min 8 characters')
})

type Schema = z.output<typeof schema>

const state = reactive<Partial<Schema>>({
  email: '',
  password: ''
})

const form = ref()

async function onSubmit() {
  await form.value.validate()
  // Submit logic
}
</script>

<template>
  <UForm ref="form" :schema="schema" :state="state" @submit="onSubmit">
    <UFormField name="email" label="Email">
      <UInput v-model="state.email" type="email" />
    </UFormField>

    <UFormField name="password" label="Password">
      <UInput v-model="state.password" type="password" />
    </UFormField>

    <UButton type="submit">Submit</UButton>
  </UForm>
</template>

With Valibot

<script setup lang="ts">
import * as v from 'valibot'

const schema = v.object({
  email: v.pipe(v.string(), v.email('Invalid email')),
  password: v.pipe(v.string(), v.minLength(8, 'Min 8 characters'))
})

type Schema = v.InferOutput<typeof schema>

const state = reactive<Partial<Schema>>({
  email: '',
  password: ''
})
</script>

<template>
  <UForm :schema="schema" :state="state" @submit="onSubmit">
    <!-- Same as above -->
  </UForm>
</template>

UFormField Props

<UFormField
  name="email"              <!-- Field name (matches state key) -->
  label="Email"             <!-- Label text -->
  description="Your email"  <!-- Help text -->
  hint="Optional"           <!-- Right-aligned hint -->
  required                  <!-- Shows asterisk -->
  :help="error?.message"    <!-- Error message -->
>
  <UInput v-model="state.email" />
</UFormField>

UFieldGroup (Group Fields)

<UFieldGroup label="Name">
  <UInput v-model="firstName" placeholder="First" />
  <UInput v-model="lastName" placeholder="Last" />
</UFieldGroup>

Input States

<!-- Disabled -->
<UInput disabled placeholder="Disabled" />

<!-- Loading -->
<UInput :loading="true" placeholder="Loading..." />

<!-- With icon -->
<UInput icon="i-heroicons-magnifying-glass" placeholder="Search" />

<!-- Trailing icon -->
<UInput trailing-icon="i-heroicons-x-mark" placeholder="Clearable" />

File Upload

<script setup>
const { files, open, reset } = useFileUpload()
</script>

<template>
  <UFileUpload v-model="files" accept="image/*" multiple :max-files="5">
    <UButton @click="open">Upload</UButton>
  </UFileUpload>
</template>

Note (v4.4): FileUpload now emits null when clearing files (previously emitted empty array).

Date & Time Pickers (v4.2+)

Date Picker

<script setup>
const date = ref(new Date())
const range = ref({ start: new Date(), end: new Date() })
</script>

<template>
  <!-- Single date -->
  <UInputDate v-model="date" />

  <!-- Date range -->
  <UInputDate v-model="range" mode="range" />

  <!-- With locale -->
  <UInputDate v-model="date" locale="es" />
</template>

Time Picker

<script setup>
import { Time } from '@internationalized/date'

const time = ref(new Time(12, 0))
</script>

<template>
  <!-- Basic time picker -->
  <UInputTime v-model="time" />

  <!-- 24-hour format -->
  <UInputTime v-model="time" hour-cycle="24" />

  <!-- Custom step (minutes) -->
  <UInputTime v-model="time" :minute-step="15" />
</template>

Common Patterns

Login Form

<UForm :schema="loginSchema" :state="state" @submit="login">
  <UFormField name="email" label="Email" required>
    <UInput v-model="state.email" type="email" icon="i-heroicons-envelope" />
  </UFormField>
  <UFormField name="password" label="Password" required>
    <UInput v-model="state.password" type="password" icon="i-heroicons-lock-closed" />
  </UFormField>
  <UButton type="submit" block :loading="loading">Sign in</UButton>
</UForm>

Settings Form

<UForm :state="settings" @submit="save">
  <UFormField name="notifications" label="Notifications">
    <USwitch v-model="settings.notifications" label="Enable notifications" />
  </UFormField>
  <UFormField name="theme" label="Theme">
    <URadioGroup v-model="settings.theme" :items="themeOptions" />
  </UFormField>
  <UButton type="submit">Save</UButton>
</UForm>

Best Practices

Do Don't
Use UForm + UFormField Build custom form wrappers
Use Standard Schema (Zod/Valibot) Write custom validation
Use v-model on form inputs Manually sync form state
Use required prop for asterisks Add asterisks manually
Use description for help text Use separate help elements