Secondary Development Best Practices
Secondary Development Best Practices
Section titled βSecondary Development Best PracticesβA comprehensive guide for efficiently extending and customizing the Vue admin template. This guide focuses on practical patterns and strategies that help you build upon the template foundation while maintaining code quality and development velocity.
ποΈ Project Structure Optimization
Section titled βποΈ Project Structure OptimizationβRecommended Directory Organization
Section titled βRecommended Directory OrganizationβOrganize your code by features rather than file types for better maintainability:
src/βββ components/ # Shared componentsβ βββ ui/ # Base UI components (buttons, inputs, etc.)β βββ business/ # Business logic componentsβ βββ layout/ # Layout-specific componentsβββ features/ # Feature modulesβ βββ user-management/β β βββ components/ # Feature-specific componentsβ β β βββ UserTable.vueβ β β βββ UserForm.vueβ β β βββ UserFilters.vueβ β βββ composables/ # Feature-specific composablesβ β β βββ useUserData.tsβ β β βββ useUserValidation.tsβ β βββ services/ # API servicesβ β β βββ userService.tsβ β βββ types/ # TypeScript typesβ β β βββ user.types.tsβ β βββ views/ # Feature pagesβ β βββ UserListView.vueβ β βββ UserDetailView.vueβ βββ product-management/β βββ dashboard/βββ shared/ # Shared utilities and servicesβ βββ services/ # Global servicesβ βββ composables/ # Shared composablesβ βββ utils/ # Utility functionsβ βββ constants/ # Application constantsβββ stores/ # Pinia stores βββ auth.ts βββ app.ts βββ modules/ # Feature-specific stores βββ user.ts βββ product.ts
Benefits:
- Easier Navigation: Related files are grouped together
- Better Encapsulation: Feature-specific code is self-contained
- Improved Scalability: New features can be added without affecting existing structure
- Team Collaboration: Multiple developers can work on different features independently
Use index files to create clean import paths and better API surfaces:
// features/user-management/index.ts// Export all public componentsexport { default as UserTable } from './components/UserTable.vue'export { default as UserForm } from './components/UserForm.vue'export { default as UserFilters } from './components/UserFilters.vue'
// Export composablesexport { useUserData } from './composables/useUserData'export { useUserValidation } from './composables/useUserValidation'
// Export servicesexport { userService } from './services/userService'
// Export typesexport type * from './types/user.types'
// Export views for routerexport { default as UserListView } from './views/UserListView.vue'export { default as UserDetailView } from './views/UserDetailView.vue'
// Usage in other parts of the application:// import { UserTable, useUserData, userService } from '@/features/user-management'
Create barrel exports for clean imports:
// shared/composables/index.tsexport { useAsyncData } from './useAsyncData'export { useLocalStorage } from './useLocalStorage'export { useDebounce } from './useDebounce'export { useMediaQuery } from './useMediaQuery'
// shared/utils/index.tsexport { formatDate } from './date'export { formatCurrency } from './currency'export { validateEmail } from './validation'export { debounce } from './performance'
// Usage becomes much cleaner:import { useAsyncData, useDebounce } from '@/shared/composables'import { formatDate, validateEmail } from '@/shared/utils'
Configuration Management
Section titled βConfiguration ManagementβOrganize configuration based on environments and features:
// config/index.tsimport { z } from 'zod'
// Configuration schema for validationconst ConfigSchema = z.object({ app: z.object({ name: z.string(), version: z.string(), environment: z.enum(['development', 'staging', 'production']), }), api: z.object({ baseUrl: z.string().url(), timeout: z.number().default(10000), retries: z.number().default(3), }), features: z.object({ enableNotifications: z.boolean().default(true), enableAnalytics: z.boolean().default(false), maxFileUploadSize: z.number().default(10485760), // 10MB }), ui: z.object({ theme: z.enum(['light', 'dark', 'auto']).default('auto'), language: z.string().default('en'), pageSize: z.number().default(20), })})
// Type-safe configurationtype AppConfig = z.infer<typeof ConfigSchema>
// Environment-specific configurationsconst baseConfig: AppConfig = { app: { name: 'Vue Admin Template', version: '1.0.0', environment: (import.meta.env.MODE as any) || 'development', }, api: { baseUrl: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api', timeout: 10000, retries: 3, }, features: { enableNotifications: true, enableAnalytics: import.meta.env.PROD, maxFileUploadSize: 10485760, }, ui: { theme: 'auto', language: 'en', pageSize: 20, }}
// Development overridesconst developmentConfig: Partial<AppConfig> = { api: { ...baseConfig.api, timeout: 30000, // Longer timeout for development }, features: { ...baseConfig.features, enableAnalytics: false, }}
// Production overridesconst productionConfig: Partial<AppConfig> = { api: { ...baseConfig.api, baseUrl: import.meta.env.VITE_API_BASE_URL || 'https://api.yourdomain.com', }, features: { ...baseConfig.features, enableAnalytics: true, }}
// Merge configurationsconst envConfigs = { development: developmentConfig, staging: baseConfig, production: productionConfig,}
const mergedConfig = { ...baseConfig, ...envConfigs[baseConfig.app.environment],}
// Validate and exportexport const config = ConfigSchema.parse(mergedConfig)
// Type-safe config accessexport type { AppConfig }
Implement feature flags for gradual rollouts and A/B testing:
// shared/services/featureFlags.tsinterface FeatureFlag { key: string enabled: boolean rolloutPercentage?: number userGroups?: string[] environment?: string[]}
class FeatureFlagService { private flags: Map<string, FeatureFlag> = new Map() private userId?: string
constructor() { this.loadFlags() }
async loadFlags() { try { // Load from API or configuration const response = await fetch('/api/feature-flags') const flags: FeatureFlag[] = await response.json()
flags.forEach(flag => { this.flags.set(flag.key, flag) }) } catch (error) { console.warn('Failed to load feature flags:', error) } }
setUserId(userId: string) { this.userId = userId }
isEnabled(flagKey: string): boolean { const flag = this.flags.get(flagKey) if (!flag) { return false }
// Check environment if (flag.environment && !flag.environment.includes(config.app.environment)) { return false }
// Check user groups if (flag.userGroups && this.userId) { const userStore = useUserStore() const userGroups = userStore.user?.groups || [] if (!flag.userGroups.some(group => userGroups.includes(group))) { return false } }
// Check rollout percentage if (flag.rolloutPercentage !== undefined && this.userId) { const hash = this.hashUserId(this.userId) if (hash % 100 >= flag.rolloutPercentage) { return false } }
return flag.enabled }
private hashUserId(userId: string): number { let hash = 0 for (let i = 0; i < userId.length; i++) { const char = userId.charCodeAt(i) hash = ((hash << 5) - hash) + char hash = hash & hash // Convert to 32-bit integer } return Math.abs(hash) }}
export const featureFlags = new FeatureFlagService()
// Composable for easy use in componentsexport function useFeatureFlag(flagKey: string) { return computed(() => featureFlags.isEnabled(flagKey))}
// Usage in components:// const showNewDashboard = useFeatureFlag('new-dashboard')// const enableAdvancedSearch = useFeatureFlag('advanced-search')
π Component Reuse Strategies
Section titled βπ Component Reuse StrategiesβHigher-Order Components and Composition
Section titled βHigher-Order Components and CompositionβCreate reusable data providers for common operations:
// composables/useDataProvider.tsexport function useDataProvider<T>( fetchFn: () => Promise<T[]>, options: { immediate?: boolean refetchOnWindowFocus?: boolean staleTime?: number cacheKey?: string } = {}) { const { immediate = true, refetchOnWindowFocus = false, staleTime = 5 * 60 * 1000, // 5 minutes cacheKey } = options
const data = ref<T[]>([]) const loading = ref(false) const error = ref<Error | null>(null) const lastFetch = ref<number>(0)
// Cache management const cache = new Map<string, { data: T[], timestamp: number }>()
const fetch = async (force = false) => { // Check cache first if (cacheKey && !force) { const cached = cache.get(cacheKey) if (cached && Date.now() - cached.timestamp < staleTime) { data.value = cached.data return } }
try { loading.value = true error.value = null
const result = await fetchFn() data.value = result lastFetch.value = Date.now()
// Update cache if (cacheKey) { cache.set(cacheKey, { data: result, timestamp: Date.now() }) } } catch (err) { error.value = err instanceof Error ? err : new Error('Unknown error') console.error('Data fetch error:', err) } finally { loading.value = false } }
const refetch = () => fetch(true)
// Auto-fetch on mount if (immediate) { onMounted(() => fetch()) }
// Refetch on window focus if (refetchOnWindowFocus) { useEventListener('visibilitychange', () => { if (!document.hidden && Date.now() - lastFetch.value > staleTime) { fetch() } }) }
return { data: readonly(data), loading: readonly(loading), error: readonly(error), fetch, refetch }}
// Usage in components:const { data: users, loading, error, refetch } = useDataProvider( () => userService.getUsers(), { cacheKey: 'users', refetchOnWindowFocus: true })
Create a flexible form builder for consistent form handling:
// composables/useFormBuilder.tsimport { z } from 'zod'import type { FormFieldConfig } from '@/components/HiForm/types'
export function useFormBuilder<T extends Record<string, any>>( schema: z.ZodSchema<T>, options: { defaultValues?: Partial<T> onSubmit?: (data: T) => Promise<void> | void onError?: (error: Error) => void } = {}) { const { defaultValues = {}, onSubmit, onError } = options
// Generate form fields from schema const fields = computed<FormFieldConfig[]>(() => { return generateFieldsFromSchema(schema) })
// Form state const formData = reactive<Partial<T>>({ ...defaultValues }) const errors = ref<Record<string, string>>({}) const isSubmitting = ref(false) const isDirty = ref(false)
// Track form changes watch( () => formData, () => { isDirty.value = true }, { deep: true } )
// Validation const validate = async (): Promise<boolean> => { try { await schema.parseAsync(formData) errors.value = {} return true } catch (error) { if (error instanceof z.ZodError) { errors.value = Object.fromEntries( error.errors.map(err => [err.path[0], err.message]) ) } return false } }
// Submit handler const handleSubmit = async () => { if (!onSubmit) return
const isValid = await validate() if (!isValid) return
try { isSubmitting.value = true await onSubmit(formData as T) isDirty.value = false } catch (error) { const errorInstance = error instanceof Error ? error : new Error('Submission failed') if (onError) { onError(errorInstance) } else { console.error('Form submission error:', errorInstance) } } finally { isSubmitting.value = false } }
// Reset form const reset = () => { Object.assign(formData, defaultValues) errors.value = {} isDirty.value = false }
// Set form values const setValues = (values: Partial<T>) => { Object.assign(formData, values) }
return { fields, formData, errors: readonly(errors), isSubmitting: readonly(isSubmitting), isDirty: readonly(isDirty), validate, handleSubmit, reset, setValues }}
// Usage example:const UserSchema = z.object({ name: z.string().min(2), email: z.string().email(), role: z.enum(['admin', 'user'])})
const { fields, formData, errors, isSubmitting, handleSubmit, reset} = useFormBuilder(UserSchema, { defaultValues: { role: 'user' }, onSubmit: async (data) => { await userService.createUser(data) toast.success('User created successfully') }, onError: (error) => { toast.error(error.message) }})
Create consistent table configurations across your application:
// utils/tableFactory.tsimport type { ColumnDef } from '@tanstack/vue-table'import type { TableConfig } from '@/components/HiTable/types'
interface TableFactoryOptions<T> { searchableColumns?: (keyof T)[] sortableColumns?: (keyof T)[] defaultSort?: { column: keyof T; direction: 'asc' | 'desc' } pageSize?: number enableSelection?: boolean enableExport?: boolean actions?: Array<{ label: string onClick: (row: T) => void condition?: (row: T) => boolean variant?: string }>}
export function createTableConfig<T>( columns: ColumnDef<T>[], options: TableFactoryOptions<T> = {}): { columns: ColumnDef<T>[], config: TableConfig } { const { searchableColumns = [], sortableColumns = [], defaultSort, pageSize = 20, enableSelection = false, enableExport = false, actions = [] } = options
// Enhanced columns with sorting configuration const enhancedColumns: ColumnDef<T>[] = [ // Selection column ...(enableSelection ? [createSelectionColumn<T>()] : []),
// Data columns ...columns.map(column => ({ ...column, enableSorting: sortableColumns.includes(column.accessorKey as keyof T), })),
// Actions column ...(actions.length > 0 ? [createActionColumn(actions)] : []) ]
// Table configuration const config: TableConfig = { pagination: { enabled: true, pageSize, showPageInfo: true, }, sorting: { enabled: sortableColumns.length > 0, multiSort: false, ...(defaultSort && { defaultSort: [{ id: defaultSort.column as string, desc: defaultSort.direction === 'desc' }] }) }, filtering: { enabled: searchableColumns.length > 0, searchable: true, searchPlaceholder: `Search ${searchableColumns.join(', ')}...` }, rowSelection: { enabled: enableSelection, }, columnVisibility: { enabled: true, }, styling: { compact: false, headerClass: 'font-semibold text-sm', }, emptyState: { message: 'No data available', } }
return { columns: enhancedColumns, config }}
// Usage example:const { columns, config } = createTableConfig<User>( [ { accessorKey: 'name', header: 'Name' }, { accessorKey: 'email', header: 'Email' }, { accessorKey: 'role', header: 'Role' }, { accessorKey: 'createdAt', header: 'Created' }, ], { searchableColumns: ['name', 'email'], sortableColumns: ['name', 'createdAt'], defaultSort: { column: 'createdAt', direction: 'desc' }, enableSelection: true, enableExport: true, actions: [ { label: 'Edit', onClick: (user) => editUser(user), variant: 'outline' }, { label: 'Delete', onClick: (user) => deleteUser(user), condition: (user) => user.role !== 'admin', variant: 'destructive' } ] })
π¨ Styling Customization Methods
Section titled βπ¨ Styling Customization MethodsβTheme System Extension
Section titled βTheme System ExtensionβExtend the theme system using CSS custom properties for consistent styling:
/* styles/theme.css */:root { /* Brand Colors */ --brand-primary: #3b82f6; --brand-secondary: #6366f1; --brand-accent: #f59e0b;
/* Extended Color Palette */ --color-success-50: #f0fdf4; --color-success-500: #22c55e; --color-success-900: #14532d;
--color-warning-50: #fffbeb; --color-warning-500: #f59e0b; --color-warning-900: #78350f;
--color-error-50: #fef2f2; --color-error-500: #ef4444; --color-error-900: #7f1d1d;
/* Component-specific variables */ --table-header-bg: var(--color-gray-50); --table-row-hover: var(--color-gray-25); --form-field-border: var(--color-gray-300); --form-field-focus: var(--brand-primary);
/* Layout variables */ --sidebar-width: 280px; --header-height: 64px; --content-max-width: 1400px;
/* Animation variables */ --animation-fast: 150ms; --animation-normal: 300ms; --animation-slow: 500ms;
/* Shadow system */ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1); --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);}
/* Dark theme overrides */[data-theme="dark"] { --table-header-bg: var(--color-gray-800); --table-row-hover: var(--color-gray-700); --form-field-border: var(--color-gray-600);}
/* Component styling using custom properties */.custom-table { --table-border-color: var(--form-field-border);}
.custom-table th { background-color: var(--table-header-bg); border-bottom: 1px solid var(--table-border-color);}
.custom-table tr:hover { background-color: var(--table-row-hover);}
/* Utility classes for custom styling */.bg-brand-primary { background-color: var(--brand-primary);}
.text-brand-primary { color: var(--brand-primary);}
.border-brand-primary { border-color: var(--brand-primary);}
Implement consistent component styling without breaking the original design:
<!-- Component-level styling strategies --><template> <!-- Method 1: CSS Modules for scoped styling --> <div :class="[$style.customWrapper, 'original-class']"> <HiTable :class="$style.customTable" :data="data" :columns="columns" :config="tableConfig" /> </div>
<!-- Method 2: Dynamic classes with computed properties --> <HiForm :class="formClasses" :fields="fields" :schema="schema" />
<!-- Method 3: CSS-in-JS with style objects --> <BaseCard :style="cardStyles"> <BaseCardContent> <!-- Content --> </BaseCardContent> </BaseCard></template>
<script setup lang="ts">// Method 2: Computed classesconst formClasses = computed(() => ({ 'form-compact': useCompactMode.value, 'form-bordered': showBorders.value, 'form-elevated': elevation.value > 0, 'form-dark': isDarkTheme.value}))
// Method 3: Dynamic stylesconst cardStyles = computed(() => ({ '--card-padding': `${padding.value}px`, '--card-radius': `${borderRadius.value}px`, '--card-shadow': shadows[elevation.value], transition: 'all var(--animation-normal) ease-in-out'}))</script>
<style module>/* Method 1: CSS Modules */.customWrapper { @apply p-6 bg-white dark:bg-gray-900 rounded-lg shadow-sm;
/* Custom animations */ animation: slideIn var(--animation-normal) ease-out;}
.customTable { @apply border border-gray-200 dark:border-gray-700;
/* Custom hover effects */ --table-row-hover: theme('colors.blue.50');}
.customTable[data-theme="dark"] { --table-row-hover: theme('colors.blue.900/20');}
@keyframes slideIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); }}</style>
<!-- Global styles for component variants --><style>.form-compact .form-field { @apply mb-3;}
.form-compact .form-label { @apply text-sm;}
.form-bordered { @apply border border-gray-200 dark:border-gray-700 rounded-lg p-4;}
.form-elevated { @apply shadow-md;}
.form-dark { @apply bg-gray-800 text-white;}</style>
Implement responsive design patterns that work with the existing breakpoint system:
<!-- Responsive layout components --><template> <div class="responsive-container"> <!-- Mobile-first navigation --> <nav class="mobile-nav lg:hidden"> <MobileMenuButton @click="toggleMobileMenu" /> </nav>
<!-- Desktop navigation --> <nav class="desktop-nav hidden lg:block"> <DesktopMenu /> </nav>
<!-- Responsive grid layout --> <div class="responsive-grid"> <aside class="sidebar"> <!-- Sidebar content --> </aside> <main class="main-content"> <!-- Responsive table --> <HiTable :data="data" :columns="responsiveColumns" :config="responsiveTableConfig" /> </main> </div> </div></template>
<script setup lang="ts">import { useMediaQuery } from '@/shared/composables'
// Responsive breakpointsconst isMobile = useMediaQuery('(max-width: 768px)')const isTablet = useMediaQuery('(max-width: 1024px)')const isDesktop = useMediaQuery('(min-width: 1025px)')
// Responsive column configurationconst responsiveColumns = computed(() => { const baseColumns = [ { accessorKey: 'name', header: 'Name' }, { accessorKey: 'email', header: 'Email' }, { accessorKey: 'role', header: 'Role' }, ]
// Add additional columns on larger screens if (isTablet.value) { baseColumns.push( { accessorKey: 'department', header: 'Department' }, { accessorKey: 'lastLogin', header: 'Last Login' } ) }
if (isDesktop.value) { baseColumns.push( { accessorKey: 'createdAt', header: 'Created' }, { accessorKey: 'status', header: 'Status' } ) }
return baseColumns})
// Responsive table configurationconst responsiveTableConfig = computed(() => ({ pagination: { enabled: true, pageSize: isMobile.value ? 10 : 20, showPageInfo: !isMobile.value, }, styling: { compact: isMobile.value, containerClass: isMobile.value ? 'mobile-table' : 'desktop-table' }}))</script>
<style scoped>.responsive-container { @apply min-h-screen bg-gray-50 dark:bg-gray-900;}
.responsive-grid { @apply grid grid-cols-1 lg:grid-cols-[280px_1fr] gap-6 p-4 lg:p-6;}
.sidebar { @apply bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm;
/* Mobile: full width */ @apply w-full lg:w-auto;
/* Desktop: fixed width */ @screen lg { @apply sticky top-6 self-start; }}
.main-content { @apply space-y-6;
/* Prevent horizontal overflow on mobile */ @apply min-w-0;}
/* Mobile table optimizations */:deep(.mobile-table) { @apply text-sm;
/* Hide less important columns on mobile */ .table-cell:nth-child(n+4) { @apply hidden; }
/* Compact row spacing */ .table-row { @apply py-2; }}
/* Desktop table enhancements */:deep(.desktop-table) { /* Enhanced hover effects */ .table-row:hover { @apply bg-blue-50 dark:bg-blue-900/20 transform scale-[1.001]; transition: all var(--animation-fast) ease-in-out; }}
/* Responsive form layouts */.responsive-form { @apply grid gap-4;
/* Mobile: 1 column */ @apply grid-cols-1;
/* Tablet: 2 columns */ @screen md { @apply grid-cols-2; }
/* Desktop: 3 columns for simple fields */ @screen lg { @apply grid-cols-3; }}
/* Full-width fields on all screens */.responsive-form .field-full { @apply col-span-full;}
/* Half-width fields on larger screens */.responsive-form .field-half { @apply col-span-1 md:col-span-1 lg:col-span-1;}</style>