Field catalogue

Every value of INPUT_TYPES , grouped by what it stores. The live state under each example shows the exact value shape your API will receive.

Source in the kit: components/Form/Input*/ (each folder has its own types.ts with every prop)

Props every field shares

examples/fields/States.vue

label , description , hint , help , placeholder , required , disabled , readonly and rules come from IFieldProps .

hint

description

help (shown under the input)

Type 1-2 characters

<template>
  <FormFields
    :options="fields"
    class="grid gap-x-4 md:grid-cols-2"
  />
</template>

<script lang="ts" setup>
useForm({
  initialValues: {
    disabled: 'Cannot be edited',
    readonly: 'Can be copied',
  },
})

// These props come from IFieldProps, so every field type accepts them
const fields = createFormFields(() => [
  {
    type: INPUT_TYPES.TEXT,
    props: {
      name: 'full',
      label: 'label',
      required: true,
      hint: 'hint',
      description: 'description',
      help: 'help (shown under the input)',
      placeholder: 'placeholder (defaults to the label)',
    },
  },
  {
    type: INPUT_TYPES.TEXT,
    props: {
      name: 'ruled',
      label: 'rules on one field',
      // A field can carry its own valibot rule, next to or instead of the form schema
      rules: v.pipe(v.optional(v.string(), ''), v.minLength(3)),
      description: 'Type 1-2 characters',
    },
  },
  {
    type: INPUT_TYPES.TEXT,
    props: {
      name: 'disabled',
      label: 'disabled',
      disabled: true,
    },
  },
  {
    type: INPUT_TYPES.TEXT,
    props: {
      name: 'readonly',
      label: 'readonly',
      readonly: true,
    },
  },
])
</script>

Leading and trailing spaces are trimmed

TEXT with type="password" and a show/hide button

Arrow keys + Enter, or click

Emits search on every keystroke, clear button included

Value is string[]; Enter adds a tag

vue
<template>
  <FormFields
    :options="fields"
    class="grid gap-x-4 md:grid-cols-2"
  />
</template>

<script lang="ts" setup>
const form = useForm({
  initialValues: {
    text: '',
    password: '',
    phone: '',
    city: '',
    search: '',
    bio: '',
    tags: ['vue'],
  },
})

const fields = createFormFields(() => [
  {
    type: INPUT_TYPES.TEXT,
    props: {
      name: 'text',
      label: 'TEXT',
      leadingIcon: 'ph:user',
      description: 'Leading and trailing spaces are trimmed',
    },
  },
  {
    type: INPUT_TYPES.PASSWORD,
    props: {
      name: 'password',
      label: 'PASSWORD',
      description: 'TEXT with type="password" and a show/hide button',
    },
  },
  {
    type: INPUT_TYPES.TEXT,
    props: {
      name: 'phone',
      label: 'TEXT with mask',
      // maska tokens: # digit, @ letter, * either
      mask: '###-###-####',
      placeholder: '081-234-5678',
    },
  },
  {
    type: INPUT_TYPES.TEXT,
    props: {
      name: 'city',
      label: 'TEXT with suggestions',
      suggestions: ['Bangkok', 'Chiang Mai', 'Chiang Rai', 'Phuket', 'Khon Kaen'],
      description: 'Arrow keys + Enter, or click',
    },
  },
  {
    type: INPUT_TYPES.SEARCH,
    props: {
      name: 'search',
      label: 'SEARCH',
      description: 'Emits search on every keystroke, clear button included',
    },
    on: {
      search: (query: string) => console.log('search', query),
    },
  },
  {
    type: INPUT_TYPES.TAGS,
    props: {
      name: 'tags',
      label: 'TAGS',
      suggestions: ['vue', 'nuxt', 'vite', 'pinia'],
      description: 'Value is string[]; Enter adds a tag',
    },
  },
  {
    type: INPUT_TYPES.TEXTAREA,
    class: 'md:col-span-2',
    props: {
      name: 'bio',
      label: 'TEXTAREA',
      autoresize: true,
      rows: 2,
      maxrows: 6,
    },
  },
])

defineExpose({
  debug: computed(() => form.values),
})
</script>

Value is an array of option values

Value is a boolean

<template>
  <FormFields
    :options="fields"
    class="grid gap-x-4 md:grid-cols-2"
  />
</template>

<script lang="ts" setup>
const form = useForm({
  initialValues: {
    role: 'editor',
    status: undefined,
    skills: ['vue'],
    plan: 'pro',
    channels: ['email'],
    isActive: true,
    accept: false,
  },
})

// Options are { label, value }. SELECT also reads icon, avatar, chip and `type: 'separator'`.
const roleOptions = [
  {
    label: 'Admin',
    value: 'admin',
    icon: 'ph:crown',
  },
  {
    label: 'Editor',
    value: 'editor',
    icon: 'ph:pencil',
  },
  {
    label: 'Viewer',
    value: 'viewer',
    icon: 'ph:eye',
  },
]

const fields = createFormFields(() => [
  {
    type: INPUT_TYPES.SELECT,
    props: {
      name: 'role',
      label: 'SELECT',
      options: roleOptions,
      clearable: true,
    },
  },
  {
    type: INPUT_TYPES.SELECT,
    props: {
      name: 'status',
      label: 'SELECT with search and chips',
      placeholder: 'Pick a status',
      searchInput: {
        placeholder: 'Filter…',
      },
      options: [
        {
          label: 'Active',
          value: 'active',
          chip: {
            color: 'success',
          },
        },
        {
          label: 'Suspended',
          value: 'suspended',
          chip: {
            color: 'error',
          },
        },
      ],
    },
  },
  {
    type: INPUT_TYPES.SELECT_MULTIPLE,
    props: {
      name: 'skills',
      label: 'SELECT_MULTIPLE',
      description: 'Value is an array of option values',
      options: ['vue', 'nuxt', 'go', 'postgres'].map((skill) => ({
        label: skill,
        value: skill,
      })),
    },
  },
  {
    type: INPUT_TYPES.RADIO,
    props: {
      name: 'plan',
      label: 'RADIO',
      variant: 'card',
      orientation: 'horizontal',
      options: [
        {
          label: 'Free',
          value: 'free',
          description: '1 project',
        },
        {
          label: 'Pro',
          value: 'pro',
          description: 'Unlimited',
        },
      ],
    },
  },
  {
    type: INPUT_TYPES.CHECKBOX_GROUP,
    props: {
      name: 'channels',
      // A checkbox group shows only its options, by design: say what it is about in the option labels
      options: [
        {
          label: 'Notify by email',
          value: 'email',
        },
        {
          label: 'Notify by SMS',
          value: 'sms',
        },
      ],
    },
  },
  {
    type: INPUT_TYPES.TOGGLE,
    props: {
      name: 'isActive',
      label: 'TOGGLE',
      description: 'Value is a boolean',
    },
  },
  {
    type: INPUT_TYPES.CHECKBOX,
    props: {
      name: 'accept',
      label: 'CHECKBOX: I accept the terms',
    },
  },
])

defineExpose({
  debug: computed(() => form.values),
})
</script>

Numbers: NUMBER, CURRENCY, RATING

examples/fields/Number.vue

Value is { value: number, currency: string }

฿

<template>
  <FormFields
    :options="fields"
    class="grid gap-x-4 md:grid-cols-2"
  />
</template>

<script lang="ts" setup>
const form = useForm({
  initialValues: {
    quantity: 1,
    percent: 0.25,
    // CURRENCY needs an object to start from: { value, currency }
    price: {
      value: 1500,
      currency: 'THB',
    },
    rating: 3,
  },
})

const fields = createFormFields(() => [
  {
    type: INPUT_TYPES.NUMBER,
    props: {
      name: 'quantity',
      label: 'NUMBER',
      min: 0,
      max: 10,
      increment: true,
      decrement: true,
      orientation: 'horizontal',
    },
  },
  {
    type: INPUT_TYPES.NUMBER,
    props: {
      name: 'percent',
      label: 'NUMBER with formatOptions',
      step: 0.01,
      // Any Intl.NumberFormat options
      formatOptions: {
        style: 'percent',
      },
    },
  },
  {
    type: INPUT_TYPES.CURRENCY,
    props: {
      name: 'price',
      label: 'CURRENCY',
      description: 'Value is { value: number, currency: string }',
    },
  },
  {
    type: INPUT_TYPES.RATING,
    props: {
      name: 'rating',
      label: 'RATING',
      length: 5,
      clearable: true,
    },
  },
])

defineExpose({
  debug: computed(() => form.values),
})
</script>

Display follows core.date_format , core.locale and core.is_thai_year from app.config.ts ; stored values do not.

Stored as a yyyy-MM-dd string; shown in core.date_format

Stored as a Date

Stored as "HH:mm"

Stored as { month, year } (month is 0-based)

Stored as { start, end }

<template>
  <FormFields
    :options="fields"
    class="grid gap-x-4 md:grid-cols-2"
  />
</template>

<script lang="ts" setup>
const form = useForm({
  initialValues: {
    // DATE reads and writes core.date_format_system: 'yyyy-MM-dd'
    birthDate: '1990-05-17',
  },
})

const fields = createFormFields(() => [
  {
    type: INPUT_TYPES.DATE,
    props: {
      name: 'birthDate',
      label: 'DATE',
      maxDate: new Date(),
      description: 'Stored as a yyyy-MM-dd string; shown in core.date_format',
    },
  },
  {
    type: INPUT_TYPES.DATE_TIME,
    props: {
      name: 'meetingAt',
      label: 'DATE_TIME',
      description: 'Stored as a Date',
    },
  },
  {
    type: INPUT_TYPES.TIME,
    props: {
      name: 'openAt',
      label: 'TIME',
      description: 'Stored as "HH:mm"',
    },
  },
  {
    type: INPUT_TYPES.MONTH,
    props: {
      name: 'period',
      label: 'MONTH',
      description: 'Stored as { month, year } (month is 0-based)',
    },
  },
  {
    type: INPUT_TYPES.DATE_RANGE,
    props: {
      name: 'holiday',
      label: 'DATE_RANGE',
      description: 'Stored as { start, end }',
    },
  },
  {
    type: INPUT_TYPES.DATE_TIME_RANGE,
    props: {
      name: 'booking',
      label: 'DATE_TIME_RANGE',
    },
  },
])

defineExpose({
  debug: computed(() => form.values),
})
</script>

Rich text: WYSIWYG

examples/fields/Editor.vue
<template>
  <FormFields :options="fields" />
</template>

<script lang="ts" setup>
const form = useForm({
  initialValues: {
    content: '<h2>Hello</h2><p>The value is an <strong>HTML string</strong>.</p>',
  },
})

const requestOptions = useRequestOptions()

const fields = createFormFields(() => [
  {
    type: INPUT_TYPES.WYSIWYG,
    props: {
      name: 'content',
      label: 'WYSIWYG',
      // Pasted or inserted images are uploaded with these options, like the upload fields
      image: {
        requestOptions: requestOptions.getFile(),
        uploadPathURL: '/uploads',
        maxSize: 5120,
      },
    },
  },
])

defineExpose({
  debug: computed(() => form.values),
})
</script>

composables/useRequestOptions.ts

import type { AxiosRequestConfig } from 'axios'

export const useRequestOptions = () => {
  const config = useRuntimeConfig()

  // The mock API is served by this same app, so its address is wherever the page came from.
  // A fixed http://localhost:3000 only works on the machine running the dev server.
  const origin = useRequestURL().origin

  const getMock = (): Omit<AxiosRequestConfig, 'baseURL'> & {
    baseURL: string
  } => {
    return {
      baseURL: String(config.public.baseAPIMock || `${origin}/api/mock`),
    }
  }

  const getFile = (): Omit<AxiosRequestConfig, 'baseURL'> & {
    baseURL: string
  } => {
    // Set NUXT_PUBLIC_UPLOAD_API and NUXT_PUBLIC_UPLOAD_TOKEN to upload to a real API; falls back to the local mock
    const uploadAPI = config.public.uploadAPI as string
    const uploadToken = config.public.uploadToken as string

    if (!uploadAPI) return getMock()

    return {
      baseURL: uploadAPI,
      headers: uploadToken
        ? {
            Authorization: `Bearer ${uploadToken}`,
          }
        : {},
    }
  }

  const getDefault = (): Omit<AxiosRequestConfig, 'baseURL'> & {
    baseURL: string
  } => {
    return {
      baseURL: String(config.public.baseAPI),
    }
  }

  return {
    getDefault,
    getMock,
    getFile,
  }
}