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.vuelabel , description , hint , help , placeholder , required , disabled , readonly and rules come from IFieldProps .
description
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>Text: TEXT, PASSWORD, SEARCH, TAGS, TEXTAREA
examples/fields/Text.vueLeading 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
<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>Choice: SELECT, SELECT_MULTIPLE, RADIO, CHECKBOX_GROUP, TOGGLE, CHECKBOX
examples/fields/Choice.vueValue 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.vueValue 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>Date and time: DATE, DATE_TIME, TIME, MONTH, DATE_RANGE, DATE_TIME_RANGE
examples/fields/DateTime.vue 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,
}
}