Documentation Index
Fetch the complete documentation index at: https://mintlify.com/zrclouddev-oss/saas-starter-vue/llms.txt
Use this file to discover all available pages before exploring further.
Overview
SaaS Starter Vue provides a complete form system with validation support. Forms are built using vee-validate for validation logic and zod for schema definition, integrated with Inertia.js form handling.
- vee-validate - Form validation library
- @vee-validate/zod - Zod integration for vee-validate
- zod - TypeScript-first schema validation
- Inertia.js useForm - Server-side validation and form state
Here’s a simple login form using Inertia.js form handling:
<script setup lang="ts">
import { Form } from '@inertiajs/vue3'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import InputError from '@/components/InputError.vue'
defineProps<{
canResetPassword: boolean
}>()
</script>
<template>
<Form
v-bind="store()"
:reset-on-success="['password']"
v-slot="{ errors, processing }"
class="flex flex-col gap-6"
>
<div class="grid gap-2">
<Label for="email">Email address</Label>
<Input
id="email"
type="email"
name="email"
required
autofocus
autocomplete="email"
placeholder="email@example.com"
/>
<InputError :message="errors.email" />
</div>
<div class="grid gap-2">
<Label for="password">Password</Label>
<Input
id="password"
type="password"
name="password"
required
autocomplete="current-password"
placeholder="Password"
/>
<InputError :message="errors.password" />
</div>
<Button type="submit" :disabled="processing">
Log in
</Button>
</Form>
</template>
The Input component supports all standard HTML input attributes:
<script setup lang="ts">
import { Input } from '@/components/ui/input'
</script>
<template>
<Input
v-model="value"
type="text"
placeholder="Enter text..."
:disabled="false"
/>
</template>
modelValue - v-model binding
type - Input type (text, email, password, etc.)
placeholder - Placeholder text
disabled - Disabled state
class - Additional CSS classes
<template>
<!-- Normal state -->
<Input placeholder="Normal input" />
<!-- Disabled state -->
<Input placeholder="Disabled" disabled />
<!-- Invalid state (via aria-invalid) -->
<Input aria-invalid="true" placeholder="Invalid input" />
</template>
Textarea Component
For multi-line text input:
<script setup lang="ts">
import { Textarea } from '@/components/ui/textarea'
import { Label } from '@/components/ui/label'
</script>
<template>
<div class="grid gap-2">
<Label for="description">Description</Label>
<Textarea
id="description"
v-model="description"
placeholder="Enter description..."
rows="4"
/>
</div>
</template>
Select Component
Dropdown selection using reka-ui:
<script setup lang="ts">
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
</script>
<template>
<Select v-model="selectedValue">
<SelectTrigger>
<SelectValue placeholder="Select an option" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Options</SelectLabel>
<SelectItem value="option1">Option 1</SelectItem>
<SelectItem value="option2">Option 2</SelectItem>
<SelectItem value="option3">Option 3</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</template>
Checkbox Component
For boolean selections:
<script setup lang="ts">
import { Checkbox } from '@/components/ui/checkbox'
import { Label } from '@/components/ui/label'
</script>
<template>
<Label class="flex items-center space-x-3">
<Checkbox id="remember" name="remember" />
<span>Remember me</span>
</Label>
</template>
From resources/js/pages/system/auth/Login.vue:82-87:
<Label for="remember" class="flex items-center space-x-3">
<Checkbox id="remember" name="remember" :tabindex="3" />
<span>Remember me</span>
</Label>
Switch Component
Toggle switches for on/off states:
<script setup lang="ts">
import { Switch } from '@/components/ui/switch'
import { Label } from '@/components/ui/label'
import { ref } from 'vue'
const isEnabled = ref(false)
</script>
<template>
<div class="flex items-center space-x-2">
<Switch id="notifications" v-model:checked="isEnabled" />
<Label for="notifications">Enable notifications</Label>
</div>
</template>
Radio Group
For selecting one option from multiple choices:
<script setup lang="ts">
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Label } from '@/components/ui/label'
</script>
<template>
<RadioGroup v-model="selectedOption">
<div class="flex items-center space-x-2">
<RadioGroupItem id="option1" value="option1" />
<Label for="option1">Option 1</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroupItem id="option2" value="option2" />
<Label for="option2">Option 2</Label>
</div>
</RadioGroup>
</template>
Using vee-validate with zod schemas:
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
const schema = toTypedSchema(
z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
})
)
const { errors, handleSubmit } = useForm({
validationSchema: schema,
})
const onSubmit = handleSubmit((values) => {
console.log('Form submitted:', values)
})
</script>
<template>
<form @submit="onSubmit" class="space-y-4">
<div>
<Label for="email">Email</Label>
<Input id="email" name="email" type="email" />
<p v-if="errors.email" class="text-sm text-destructive mt-1">
{{ errors.email }}
</p>
</div>
<div>
<Label for="password">Password</Label>
<Input id="password" name="password" type="password" />
<p v-if="errors.password" class="text-sm text-destructive mt-1">
{{ errors.password }}
</p>
</div>
<Button type="submit">Submit</Button>
</form>
</template>
Combining Inertia.js form handling with components:
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import InputError from '@/components/InputError.vue'
const form = useForm({
company_name: '',
owner_name: '',
owner_email: '',
password: '',
})
const submit = () => {
form.post('/guest-register', {
onFinish: () => form.reset('password'),
})
}
</script>
<template>
<form @submit.prevent="submit" class="flex flex-col gap-4">
<div class="grid gap-2">
<Label for="company_name">Company Name</Label>
<Input
id="company_name"
v-model="form.company_name"
type="text"
required
autofocus
/>
<InputError :message="form.errors.company_name" />
</div>
<div class="grid gap-2">
<Label for="owner_email">Email</Label>
<Input
id="owner_email"
v-model="form.owner_email"
type="email"
required
/>
<InputError :message="form.errors.owner_email" />
</div>
<Button type="submit" :disabled="form.processing">
{{ form.processing ? 'Creating...' : 'Create Account' }}
</Button>
</form>
</template>
Label Component
Accessible form labels with proper associations:
<script setup lang="ts">
import { Label } from '@/components/ui/label'
</script>
<template>
<Label for="input-id">Field Label</Label>
Labels automatically handle:
- Click-to-focus behavior
- ARIA associations
- Disabled state styling
- Screen reader support
For organized form layouts:
<script setup lang="ts">
import {
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
</script>
<template>
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="Enter username" />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
</template>
The InputError component displays validation errors:
<script setup lang="ts">
import InputError from '@/components/InputError.vue'
</script>
<template>
<InputError :message="errors.fieldName" />
</template>
File Upload
Handling file uploads with preview:
<script setup lang="ts">
import { ref } from 'vue'
import { useForm } from '@inertiajs/vue3'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
const form = useForm({
file: null as File | null,
})
const fileInput = ref<HTMLInputElement | null>(null)
const preview = ref<string | null>(null)
const handleFileChange = (event: Event) => {
const target = event.target as HTMLInputElement
const file = target.files?.[0]
if (!file) return
form.file = file
const reader = new FileReader()
reader.onload = (e) => {
preview.value = e.target?.result as string
}
reader.readAsDataURL(file)
}
</script>
<template>
<div class="space-y-2">
<Label for="file">Upload File</Label>
<Input
id="file"
ref="fileInput"
type="file"
@change="handleFileChange"
/>
<img v-if="preview" :src="preview" alt="Preview" class="mt-2 max-w-xs" />
</div>
</template>
Best Practices
- Always use labels - Provide accessible labels for all form fields
- Show validation errors - Display clear error messages near the relevant field
- Disable on submit - Prevent double submissions by disabling the submit button
- Reset on success - Clear sensitive fields after successful submission
- Use appropriate input types - email, password, tel, etc. for better UX
- Add autocomplete - Help users fill forms faster
- Provide placeholder text - Give users context about expected input
Accessibility
All form components include:
- Proper ARIA attributes
- Keyboard navigation
- Focus indicators
- Error announcements
- Label associations