1. Form Handling
<script setup lang="ts">
import { reactive } from 'vue'
const form = reactive({
name: '',
email: '',
role: 'user',
notifications: true,
tags: [] as string[],
})
function handleSubmit() {
console.log(form)
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<input v-model.trim="form.name" placeholder="Tên" />
<input v-model.lazy="form.email" type="email" placeholder="Email" />
<select v-model="form.role">
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
<label>
<input type="checkbox" v-model="form.notifications" />
Nhận thông báo
</label>
<div>
<label v-for="tag in ['vue', 'nuxt', 'ts']" :key="tag">
<input type="checkbox" v-model="form.tags" :value="tag" />
{{ tag }}
</label>
</div>
<button type="submit">Submit</button>
</form>
</template>
2. VeeValidate + Zod
npm install vee-validate @vee-validate/zod zod
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { z } from 'zod'
const schema = toTypedSchema(
z.object({
name: z.string().min(2, 'Tên ít nhất 2 ký tự'),
email: z.string().email('Email không hợp lệ'),
password: z
.string()
.min(8, 'Mật khẩu ít nhất 8 ký tự')
.regex(/[A-Z]/, 'Cần ít nhất 1 chữ hoa'),
})
)
const { defineField, handleSubmit, errors, isSubmitting } = useForm({
validationSchema: schema,
})
const [name, nameAttrs] = defineField('name')
const [email, emailAttrs] = defineField('email')
const [password, passwordAttrs] = defineField('password')
const onSubmit = handleSubmit(async (values) => {
await fetch('/api/register', {
method: 'POST',
body: JSON.stringify(values),
})
})
</script>
<template>
<form @submit="onSubmit">
<div>
<input v-model="name" v-bind="nameAttrs" placeholder="Tên" />
<span v-if="errors.name" class="error">{{ errors.name }}</span>
</div>
<div>
<input v-model="email" v-bind="emailAttrs" type="email" placeholder="Email" />
<span v-if="errors.email" class="error">{{ errors.email }}</span>
</div>
<div>
<input v-model="password" v-bind="passwordAttrs" type="password" placeholder="Mật khẩu" />
<span v-if="errors.password" class="error">{{ errors.password }}</span>
</div>
<button type="submit" :disabled="isSubmitting">Đăng ký</button>
</form>
</template>
3. Tailwind CSS
<template>
<div class="max-w-md mx-auto p-6 bg-white dark:bg-gray-800 rounded-lg shadow-lg">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
{{ title }}
</h2>
<p class="text-gray-600 dark:text-gray-300 leading-relaxed">
{{ description }}
</p>
<button class="mt-4 px-4 py-2 bg-blue-600 text-white rounded-md
hover:bg-blue-700 transition-colors duration-200
focus:outline-none focus:ring-2 focus:ring-blue-500">
Xem thêm
</button>
</div>
</template>
4. CSS Scoped & Modules
<!-- Scoped CSS -->
<style scoped>
.card { padding: 1rem; }
.card :deep(.child-class) { color: red; } /* Affect child components */
.card :slotted(p) { margin: 0; } /* Affect slot content */
:global(.app-title) { font-size: 2rem; } /* Global style */
</style>
<!-- CSS Modules -->
<style module>
.red { color: red; }
.bold { font-weight: bold; }
</style>
<template>
<p :class="$style.red">Red text</p>
<p :class="[$style.red, $style.bold]">Red bold</p>
</template>
5. Component Libraries
| Library | Style | Components | Bundle |
|---|---|---|---|
| PrimeVue | Unstyled / themed | 90+ | Tree-shakeable |
| Vuetify | Material Design | 80+ | Large |
| Naive UI | Custom theme | 90+ | Tree-shakeable |
| Radix Vue | Headless | 40+ | Minimal |
| shadcn-vue | Tailwind | 40+ | Copy-paste |
Bài tiếp theo: Nuxt 3 & File-based Routing — bắt đầu với Nuxt 3 framework.