UNPKG

agent-rules-kit

Version:

CLI tool to bootstrap AI agent rules for multiple IDEs and frameworks. Generates optimized rules for Cursor, VS Code, Claude, and 6+ other AI coding assistants.

766 lines (630 loc) 15 kB
--- description: Best practices for Vue applications globs: <root>/src/**/*.vue,<root>/src/**/*.js,<root>/src/**/*.ts alwaysApply: false --- # Vue Best Practices This guide outlines the recommended best practices for Vue development in {projectPath}, covering Vue 3 Composition API and Options API patterns. ## Component Structure ### Composition API (Recommended) Use the Composition API for better TypeScript support and logic reusability: ```vue <template> <div class="user-profile"> <div v-if="loading" class="loading"> <LoadingSpinner /> </div> <div v-else-if="user" class="user-details"> <h2>{{ user.name }}</h2> <p>{{ user.email }}</p> <button @click="updateProfile">Update Profile</button> </div> <div v-else class="error"> User not found </div> </div> </template> <script setup> import { ref, onMounted, computed } from 'vue' import { useUserStore } from '@/stores/user' // Props const props = defineProps({ userId: { type: String, required: true } }) // Emits const emit = defineEmits(['profile-updated']) // Composables const userStore = useUserStore() // Reactive state const loading = ref(true) const user = ref(null) // Computed properties const isCurrentUser = computed(() => { return user.value?.id === userStore.currentUser?.id }) // Methods const fetchUser = async () => { try { loading.value = true user.value = await userStore.fetchUser(props.userId) } catch (error) { console.error('Failed to fetch user:', error) } finally { loading.value = false } } const updateProfile = async () => { try { await userStore.updateUser(user.value.id, user.value) emit('profile-updated', user.value) } catch (error) { console.error('Failed to update profile:', error) } } // Lifecycle onMounted(() => { fetchUser() }) </script> <style scoped> .user-profile { padding: 1rem; } .loading { text-align: center; padding: 2rem; } .user-details h2 { margin-bottom: 0.5rem; color: var(--text-primary); } </style> ``` ### Options API (Legacy) When maintaining existing Options API components: ```vue <template> <div class="product-list"> <SearchInput v-model="searchQuery" placeholder="Search products..." /> <ProductCard v-for="product in filteredProducts" :key="product.id" :product="product" @click="$emit('product-selected', product)" /> </div> </template> <script> import SearchInput from '@/components/SearchInput.vue' import ProductCard from '@/components/ProductCard.vue' export default { name: 'ProductList', components: { SearchInput, ProductCard }, props: { products: { type: Array, default: () => [] } }, emits: ['product-selected'], data() { return { searchQuery: '' } }, computed: { filteredProducts() { if (!this.searchQuery) return this.products return this.products.filter(product => product.name.toLowerCase().includes(this.searchQuery.toLowerCase()) ) } }, methods: { clearSearch() { this.searchQuery = '' } } } </script> ``` ## Composables ### Creating Reusable Composables ```javascript // composables/useApi.js import { ref, reactive } from 'vue' export function useApi() { const loading = ref(false) const error = ref(null) const data = ref(null) const execute = async (apiCall) => { try { loading.value = true error.value = null data.value = await apiCall() } catch (err) { error.value = err.message console.error('API call failed:', err) } finally { loading.value = false } } return { loading: readonly(loading), error: readonly(error), data: readonly(data), execute } } // composables/useLocalStorage.js import { ref, watch } from 'vue' export function useLocalStorage(key, defaultValue) { const storedValue = localStorage.getItem(key) const value = ref(storedValue ? JSON.parse(storedValue) : defaultValue) watch(value, (newValue) => { localStorage.setItem(key, JSON.stringify(newValue)) }, { deep: true }) return value } ``` ### Using Composables ```vue <script setup> import { useApi } from '@/composables/useApi' import { useLocalStorage } from '@/composables/useLocalStorage' // Use composables const { loading, error, data, execute } = useApi() const userPreferences = useLocalStorage('user-preferences', { theme: 'light', language: 'en' }) // Fetch data const fetchUsers = () => { execute(() => fetch('/api/users').then(res => res.json())) } onMounted(fetchUsers) </script> ``` ## State Management with Pinia ### Store Definition ```javascript // stores/user.js import { defineStore } from 'pinia' import { ref, computed } from 'vue' export const useUserStore = defineStore('user', () => { // State const users = ref([]) const currentUser = ref(null) const loading = ref(false) // Getters const isAuthenticated = computed(() => !!currentUser.value) const userCount = computed(() => users.value.length) // Actions const fetchUsers = async () => { try { loading.value = true const response = await fetch('/api/users') users.value = await response.json() } catch (error) { console.error('Failed to fetch users:', error) } finally { loading.value = false } } const login = async (credentials) => { try { const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials) }) if (!response.ok) { throw new Error('Login failed') } currentUser.value = await response.json() } catch (error) { console.error('Login error:', error) throw error } } const logout = () => { currentUser.value = null localStorage.removeItem('auth-token') } return { // State users, currentUser, loading, // Getters isAuthenticated, userCount, // Actions fetchUsers, login, logout } }) ``` ### Using Stores in Components ```vue <script setup> import { useUserStore } from '@/stores/user' const userStore = useUserStore() // Access state and getters const { users, currentUser, isAuthenticated } = storeToRefs(userStore) // Access actions directly const { fetchUsers, login, logout } = userStore onMounted(() => { if (isAuthenticated.value) { fetchUsers() } }) </script> ``` ## Component Communication ### Props and Emits ```vue <!-- Parent Component --> <template> <UserForm :user="selectedUser" :loading="formLoading" @save="handleSave" @cancel="handleCancel" /> </template> <script setup> const selectedUser = ref(null) const formLoading = ref(false) const handleSave = async (userData) => { try { formLoading.value = true await saveUser(userData) // Handle success } catch (error) { console.error('Save failed:', error) } finally { formLoading.value = false } } const handleCancel = () => { selectedUser.value = null } </script> <!-- Child Component --> <template> <form @submit.prevent="handleSubmit"> <input v-model="form.name" type="text" placeholder="Name" required /> <input v-model="form.email" type="email" placeholder="Email" required /> <button type="submit" :disabled="loading"> {{ loading ? 'Saving...' : 'Save' }} </button> <button type="button" @click="$emit('cancel')"> Cancel </button> </form> </template> <script setup> const props = defineProps({ user: { type: Object, default: () => ({}) }, loading: { type: Boolean, default: false } }) const emit = defineEmits(['save', 'cancel']) const form = reactive({ name: props.user.name || '', email: props.user.email || '' }) const handleSubmit = () => { emit('save', { ...form }) } </script> ``` ### Provide/Inject ```vue <!-- Parent Component --> <script setup> import { provide } from 'vue' const theme = ref('light') const toggleTheme = () => { theme.value = theme.value === 'light' ? 'dark' : 'light' } // Provide to all descendants provide('theme', { theme: readonly(theme), toggleTheme }) </script> <!-- Child Component (any level deep) --> <script setup> import { inject } from 'vue' const { theme, toggleTheme } = inject('theme') </script> ``` ## Performance Optimization ### Lazy Loading Components ```javascript // router/index.js import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/', name: 'Home', component: () => import('@/views/Home.vue') }, { path: '/profile', name: 'Profile', component: () => import('@/views/Profile.vue') } ] ``` ### Component Optimization ```vue <template> <div class="product-grid"> <!-- Use v-memo for expensive list items --> <ProductCard v-for="product in products" :key="product.id" v-memo="[product.id, product.price, product.inStock]" :product="product" /> </div> </template> <script setup> // Use computed for expensive operations const expensiveComputation = computed(() => { return products.value.reduce((acc, product) => { // Expensive calculation return acc + calculateComplexValue(product) }, 0) }) // Use watchEffect for side effects watchEffect(() => { if (selectedCategory.value) { analytics.track('category_viewed', { category: selectedCategory.value }) } }) </script> ``` ## Error Handling ### Global Error Handler ```javascript // main.js const app = createApp(App) app.config.errorHandler = (err, instance, info) => { console.error('Global error:', err) console.error('Component instance:', instance) console.error('Error info:', info) // Send to error reporting service errorReporting.captureException(err, { context: info, component: instance?.$options.name }) } ``` ### Component Error Handling ```vue <script setup> import { ref, onErrorCaptured } from 'vue' const error = ref(null) // Capture errors from child components onErrorCaptured((err, instance, info) => { console.error('Error captured:', err) error.value = err.message // Return false to prevent the error from propagating return false }) // Handle async errors const handleAsyncOperation = async () => { try { await riskyOperation() } catch (err) { error.value = err.message } } </script> <template> <div> <div v-if="error" class="error-message"> {{ error }} <button @click="error = null">Dismiss</button> </div> <slot v-else /> </div> </template> ``` ## Testing ### Component Testing ```javascript // UserCard.test.js import { mount } from '@vue/test-utils' import UserCard from '@/components/UserCard.vue' describe('UserCard', () => { const mockUser = { id: '1', name: 'John Doe', email: 'john@example.com' } it('displays user information', () => { const wrapper = mount(UserCard, { props: { user: mockUser } }) expect(wrapper.find('[data-testid="user-name"]').text()).toBe('John Doe') expect(wrapper.find('[data-testid="user-email"]').text()).toBe('john@example.com') }) it('emits edit event when edit button is clicked', async () => { const wrapper = mount(UserCard, { props: { user: mockUser } }) await wrapper.find('[data-testid="edit-button"]').trigger('click') expect(wrapper.emitted('edit')).toBeTruthy() expect(wrapper.emitted('edit')[0]).toEqual([mockUser]) }) }) ``` ### Composable Testing ```javascript // useCounter.test.js import { useCounter } from '@/composables/useCounter' describe('useCounter', () => { it('initializes with default value', () => { const { count } = useCounter() expect(count.value).toBe(0) }) it('initializes with custom value', () => { const { count } = useCounter(10) expect(count.value).toBe(10) }) it('increments count', () => { const { count, increment } = useCounter() increment() expect(count.value).toBe(1) }) }) ``` ## Security ### Input Sanitization ```vue <script setup> import DOMPurify from 'dompurify' const props = defineProps({ htmlContent: String }) // Sanitize HTML content const sanitizedContent = computed(() => { return DOMPurify.sanitize(props.htmlContent) }) // Validate form inputs const form = reactive({ email: '', message: '' }) const errors = reactive({}) const validateForm = () => { errors.email = '' errors.message = '' // Email validation const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ if (!emailRegex.test(form.email)) { errors.email = 'Please enter a valid email address' } // Message validation if (form.message.trim().length < 10) { errors.message = 'Message must be at least 10 characters long' } return !errors.email && !errors.message } </script> <template> <div> <!-- Safe HTML rendering --> <div v-html="sanitizedContent"></div> <!-- Form with validation --> <form @submit.prevent="handleSubmit"> <input v-model="form.email" type="email" :class="{ error: errors.email }" /> <span v-if="errors.email" class="error-text"> {{ errors.email }} </span> <textarea v-model="form.message" :class="{ error: errors.message }" ></textarea> <span v-if="errors.message" class="error-text"> {{ errors.message }} </span> </form> </div> </template> ``` ## Accessibility ### Accessible Components ```vue <template> <button :class="['btn', `btn-${variant}`, { 'btn-disabled': disabled }]" :disabled="disabled" :aria-label="ariaLabel || label" :aria-describedby="describedBy" @click="handleClick" > <span v-if="loading" class="spinner" aria-hidden="true"></span> <span :class="{ 'sr-only': loading }">{{ label }}</span> </button> </template> <script setup> const props = defineProps({ label: { type: String, required: true }, variant: { type: String, default: 'primary', validator: value => ['primary', 'secondary', 'danger'].includes(value) }, disabled: { type: Boolean, default: false }, loading: { type: Boolean, default: false }, ariaLabel: String, describedBy: String }) const emit = defineEmits(['click']) const handleClick = (event) => { if (!props.disabled && !props.loading) { emit('click', event) } } </script> <style scoped> .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } </style> ``` Remember that Vue 3 with the Composition API provides better TypeScript support, improved tree-shaking, and more flexible code organization compared to the Options API.