Skip to content

Coding Standards for Element Plus Applications

Overview

This guide establishes coding standards and best practices for developing applications with Element Plus. Following these standards ensures code consistency, maintainability, and team collaboration.

Vue.js Coding Standards

Component Structure

vue
<!-- Good: Consistent component structure -->
<template>
  <div class="user-profile">
    <el-card class="profile-card">
      <template #header>
        <div class="card-header">
          <span>{{ title }}</span>
          <el-button 
            type="primary" 
            size="small"
            @click="handleEdit"
          >
            Edit
          </el-button>
        </div>
      </template>
      
      <div class="profile-content">
        <el-form 
          ref="formRef"
          :model="form"
          :rules="rules"
          label-width="120px"
        >
          <el-form-item label="Name" prop="name">
            <el-input 
              v-model="form.name"
              placeholder="Enter your name"
            />
          </el-form-item>
          
          <el-form-item label="Email" prop="email">
            <el-input 
              v-model="form.email"
              type="email"
              placeholder="Enter your email"
            />
          </el-form-item>
        </el-form>
      </div>
    </el-card>
  </div>
</template>

<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'

// Props
const props = defineProps({
  userId: {
    type: [String, Number],
    required: true
  },
  editable: {
    type: Boolean,
    default: true
  }
})

// Emits
const emit = defineEmits(['update', 'delete'])

// Stores
const userStore = useUserStore()

// Refs
const formRef = ref()

// Reactive data
const form = reactive({
  name: '',
  email: ''
})

const rules = {
  name: [
    { required: true, message: 'Please input name', trigger: 'blur' }
  ],
  email: [
    { required: true, message: 'Please input email', trigger: 'blur' },
    { type: 'email', message: 'Please input valid email', trigger: 'blur' }
  ]
}

// Computed
const title = computed(() => {
  return props.editable ? 'Edit Profile' : 'View Profile'
})

// Methods
const handleEdit = async () => {
  try {
    const valid = await formRef.value.validate()
    if (valid) {
      await userStore.updateUser(props.userId, form)
      ElMessage.success('Profile updated successfully')
      emit('update', form)
    }
  } catch (error) {
    ElMessage.error('Failed to update profile')
  }
}

const loadUserData = async () => {
  try {
    const userData = await userStore.getUser(props.userId)
    Object.assign(form, userData)
  } catch (error) {
    ElMessage.error('Failed to load user data')
  }
}

// Lifecycle
onMounted(() => {
  loadUserData()
})
</script>

<style scoped>
.user-profile {
  max-width: 600px;
  margin: 0 auto;
}

.profile-card {
  margin-bottom: 20px;
}

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.profile-content {
  padding: 20px 0;
}
</style>

Naming Conventions

Components

javascript
// Good: PascalCase for component names
UserProfile.vue
DataTable.vue
NavigationMenu.vue

// Bad: Other cases
userProfile.vue
data-table.vue
navigation_menu.vue

Variables and Functions

javascript
// Good: camelCase for variables and functions
const userName = ref('')
const isLoading = ref(false)
const handleSubmit = () => {}
const fetchUserData = async () => {}

// Bad: Other cases
const user_name = ref('')
const IsLoading = ref(false)
const handle_submit = () => {}

Constants

javascript
// Good: SCREAMING_SNAKE_CASE for constants
const API_BASE_URL = 'https://api.example.com'
const MAX_FILE_SIZE = 5 * 1024 * 1024
const DEFAULT_PAGE_SIZE = 20

// Bad: Other cases
const apiBaseUrl = 'https://api.example.com'
const maxFileSize = 5 * 1024 * 1024

CSS Classes

css
/* Good: kebab-case for CSS classes */
.user-profile {}
.data-table-header {}
.navigation-menu-item {}

/* Bad: Other cases */
.userProfile {}
.dataTableHeader {}
.navigation_menu_item {}

Props Definition

javascript
// Good: Detailed prop definitions
const props = defineProps({
  // Required string prop
  title: {
    type: String,
    required: true
  },
  
  // Optional prop with default
  size: {
    type: String,
    default: 'medium',
    validator: (value) => ['small', 'medium', 'large'].includes(value)
  },
  
  // Number prop with validation
  maxLength: {
    type: Number,
    default: 100,
    validator: (value) => value > 0
  },
  
  // Object prop with default factory
  config: {
    type: Object,
    default: () => ({})
  },
  
  // Array prop with default factory
  items: {
    type: Array,
    default: () => []
  }
})

// Bad: Minimal prop definitions
const props = defineProps(['title', 'size', 'maxLength'])

Event Handling

javascript
// Good: Descriptive event names
const emit = defineEmits([
  'update:modelValue',
  'item-selected',
  'form-submitted',
  'data-loaded',
  'error-occurred'
])

// Good: Event payload structure
const handleItemClick = (item) => {
  emit('item-selected', {
    item,
    timestamp: Date.now(),
    source: 'user-interaction'
  })
}

// Bad: Generic event names
const emit = defineEmits(['change', 'click', 'update'])

Element Plus Best Practices

Form Handling

vue
<template>
  <el-form
    ref="formRef"
    :model="form"
    :rules="rules"
    label-width="120px"
    @submit.prevent="handleSubmit"
  >
    <!-- Use proper form validation -->
    <el-form-item label="Username" prop="username">
      <el-input
        v-model="form.username"
        placeholder="Enter username"
        clearable
        :disabled="loading"
      />
    </el-form-item>
    
    <!-- Provide user feedback -->
    <el-form-item label="Password" prop="password">
      <el-input
        v-model="form.password"
        type="password"
        placeholder="Enter password"
        show-password
        :disabled="loading"
      />
    </el-form-item>
    
    <!-- Clear action buttons -->
    <el-form-item>
      <el-button 
        type="primary" 
        :loading="loading"
        @click="handleSubmit"
      >
        {{ loading ? 'Submitting...' : 'Submit' }}
      </el-button>
      <el-button @click="handleReset">
        Reset
      </el-button>
    </el-form-item>
  </el-form>
</template>

<script setup>
// Good: Comprehensive form validation
const rules = {
  username: [
    { required: true, message: 'Please input username', trigger: 'blur' },
    { min: 3, max: 20, message: 'Length should be 3 to 20', trigger: 'blur' },
    { pattern: /^[a-zA-Z0-9_]+$/, message: 'Only letters, numbers and underscore allowed', trigger: 'blur' }
  ],
  password: [
    { required: true, message: 'Please input password', trigger: 'blur' },
    { min: 8, message: 'Password must be at least 8 characters', trigger: 'blur' },
    { pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, message: 'Password must contain uppercase, lowercase and number', trigger: 'blur' }
  ]
}

const handleSubmit = async () => {
  try {
    const valid = await formRef.value.validate()
    if (valid) {
      loading.value = true
      await submitForm(form)
      ElMessage.success('Form submitted successfully')
    }
  } catch (error) {
    ElMessage.error('Validation failed')
  } finally {
    loading.value = false
  }
}
</script>

Table Implementation

vue
<template>
  <div class="data-table-container">
    <!-- Search and filters -->
    <div class="table-header">
      <el-input
        v-model="searchQuery"
        placeholder="Search..."
        clearable
        class="search-input"
        @input="handleSearch"
      >
        <template #prefix>
          <el-icon><Search /></el-icon>
        </template>
      </el-input>
      
      <el-button type="primary" @click="handleAdd">
        <el-icon><Plus /></el-icon>
        Add New
      </el-button>
    </div>
    
    <!-- Data table -->
    <el-table
      v-loading="loading"
      :data="tableData"
      stripe
      border
      style="width: 100%"
      @selection-change="handleSelectionChange"
      @sort-change="handleSortChange"
    >
      <!-- Selection column -->
      <el-table-column type="selection" width="55" />
      
      <!-- Data columns -->
      <el-table-column
        prop="name"
        label="Name"
        sortable="custom"
        min-width="150"
      >
        <template #default="{ row }">
          <el-link type="primary" @click="handleView(row)">
            {{ row.name }}
          </el-link>
        </template>
      </el-table-column>
      
      <el-table-column
        prop="email"
        label="Email"
        min-width="200"
      />
      
      <el-table-column
        prop="status"
        label="Status"
        width="120"
      >
        <template #default="{ row }">
          <el-tag
            :type="getStatusType(row.status)"
            size="small"
          >
            {{ row.status }}
          </el-tag>
        </template>
      </el-table-column>
      
      <!-- Actions column -->
      <el-table-column label="Actions" width="150" fixed="right">
        <template #default="{ row }">
          <el-button
            type="primary"
            size="small"
            @click="handleEdit(row)"
          >
            Edit
          </el-button>
          <el-button
            type="danger"
            size="small"
            @click="handleDelete(row)"
          >
            Delete
          </el-button>
        </template>
      </el-table-column>
    </el-table>
    
    <!-- Pagination -->
    <el-pagination
      v-model:current-page="currentPage"
      v-model:page-size="pageSize"
      :page-sizes="[10, 20, 50, 100]"
      :total="total"
      layout="total, sizes, prev, pager, next, jumper"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
    />
  </div>
</template>

Dialog and Modal Usage

vue
<template>
  <!-- Good: Proper dialog structure -->
  <el-dialog
    v-model="dialogVisible"
    :title="dialogTitle"
    width="600px"
    :close-on-click-modal="false"
    :close-on-press-escape="false"
    @close="handleDialogClose"
  >
    <div class="dialog-content">
      <!-- Dialog content here -->
    </div>
    
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="handleCancel">
          Cancel
        </el-button>
        <el-button 
          type="primary" 
          :loading="saving"
          @click="handleConfirm"
        >
          {{ saving ? 'Saving...' : 'Confirm' }}
        </el-button>
      </div>
    </template>
  </el-dialog>
</template>

<script setup>
// Good: Proper dialog state management
const dialogVisible = ref(false)
const saving = ref(false)

const openDialog = () => {
  dialogVisible.value = true
  // Reset form or load data
}

const handleDialogClose = () => {
  // Clean up when dialog closes
  resetForm()
}

const handleConfirm = async () => {
  try {
    saving.value = true
    await saveData()
    dialogVisible.value = false
    ElMessage.success('Saved successfully')
  } catch (error) {
    ElMessage.error('Save failed')
  } finally {
    saving.value = false
  }
}
</script>

State Management

Pinia Store Structure

javascript
// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { userApi } from '@/api/user'

export const useUserStore = defineStore('user', () => {
  // State
  const users = ref([])
  const currentUser = ref(null)
  const loading = ref(false)
  const error = ref(null)
  
  // Getters
  const activeUsers = computed(() => 
    users.value.filter(user => user.status === 'active')
  )
  
  const userCount = computed(() => users.value.length)
  
  const isAuthenticated = computed(() => !!currentUser.value)
  
  // Actions
  const fetchUsers = async (params = {}) => {
    try {
      loading.value = true
      error.value = null
      
      const response = await userApi.getUsers(params)
      users.value = response.data
      
      return response
    } catch (err) {
      error.value = err.message
      throw err
    } finally {
      loading.value = false
    }
  }
  
  const createUser = async (userData) => {
    try {
      loading.value = true
      error.value = null
      
      const response = await userApi.createUser(userData)
      users.value.push(response.data)
      
      return response.data
    } catch (err) {
      error.value = err.message
      throw err
    } finally {
      loading.value = false
    }
  }
  
  const updateUser = async (id, userData) => {
    try {
      loading.value = true
      error.value = null
      
      const response = await userApi.updateUser(id, userData)
      const index = users.value.findIndex(user => user.id === id)
      
      if (index !== -1) {
        users.value[index] = response.data
      }
      
      return response.data
    } catch (err) {
      error.value = err.message
      throw err
    } finally {
      loading.value = false
    }
  }
  
  const deleteUser = async (id) => {
    try {
      loading.value = true
      error.value = null
      
      await userApi.deleteUser(id)
      users.value = users.value.filter(user => user.id !== id)
    } catch (err) {
      error.value = err.message
      throw err
    } finally {
      loading.value = false
    }
  }
  
  const clearError = () => {
    error.value = null
  }
  
  return {
    // State
    users,
    currentUser,
    loading,
    error,
    
    // Getters
    activeUsers,
    userCount,
    isAuthenticated,
    
    // Actions
    fetchUsers,
    createUser,
    updateUser,
    deleteUser,
    clearError
  }
})

API Integration

HTTP Client Setup

javascript
// api/client.js
import axios from 'axios'
import { ElMessage } from 'element-plus'
import { useAuthStore } from '@/stores/auth'

// Create axios instance
const client = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// Request interceptor
client.interceptors.request.use(
  (config) => {
    const authStore = useAuthStore()
    
    // Add auth token
    if (authStore.token) {
      config.headers.Authorization = `Bearer ${authStore.token}`
    }
    
    // Add request timestamp
    config.metadata = { startTime: Date.now() }
    
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

// Response interceptor
client.interceptors.response.use(
  (response) => {
    // Log response time
    const duration = Date.now() - response.config.metadata.startTime
    console.log(`API call took ${duration}ms: ${response.config.url}`)
    
    return response
  },
  (error) => {
    const { response } = error
    
    // Handle different error types
    if (response) {
      switch (response.status) {
        case 401:
          // Unauthorized - redirect to login
          const authStore = useAuthStore()
          authStore.logout()
          break
          
        case 403:
          ElMessage.error('Access denied')
          break
          
        case 404:
          ElMessage.error('Resource not found')
          break
          
        case 422:
          // Validation errors
          const errors = response.data.errors
          if (errors) {
            Object.values(errors).flat().forEach(error => {
              ElMessage.error(error)
            })
          }
          break
          
        case 500:
          ElMessage.error('Server error. Please try again later.')
          break
          
        default:
          ElMessage.error(response.data.message || 'An error occurred')
      }
    } else if (error.code === 'ECONNABORTED') {
      ElMessage.error('Request timeout. Please check your connection.')
    } else {
      ElMessage.error('Network error. Please check your connection.')
    }
    
    return Promise.reject(error)
  }
)

export default client

API Service Pattern

javascript
// api/user.js
import client from './client'

export const userApi = {
  // Get users with pagination and filters
  getUsers: (params = {}) => {
    return client.get('/users', { params })
  },
  
  // Get single user
  getUser: (id) => {
    return client.get(`/users/${id}`)
  },
  
  // Create user
  createUser: (userData) => {
    return client.post('/users', userData)
  },
  
  // Update user
  updateUser: (id, userData) => {
    return client.put(`/users/${id}`, userData)
  },
  
  // Delete user
  deleteUser: (id) => {
    return client.delete(`/users/${id}`)
  },
  
  // Bulk operations
  bulkUpdate: (userIds, updateData) => {
    return client.patch('/users/bulk', {
      ids: userIds,
      data: updateData
    })
  },
  
  // Upload avatar
  uploadAvatar: (id, file) => {
    const formData = new FormData()
    formData.append('avatar', file)
    
    return client.post(`/users/${id}/avatar`, formData, {
      headers: {
        'Content-Type': 'multipart/form-data'
      }
    })
  }
}

Error Handling

Global Error Handler

javascript
// utils/errorHandler.js
import { ElMessage, ElNotification } from 'element-plus'

export class ErrorHandler {
  static handle(error, context = '') {
    console.error(`Error in ${context}:`, error)
    
    if (error.response) {
      // Server responded with error status
      this.handleServerError(error.response)
    } else if (error.request) {
      // Request was made but no response received
      this.handleNetworkError()
    } else {
      // Something else happened
      this.handleGenericError(error.message)
    }
  }
  
  static handleServerError(response) {
    const { status, data } = response
    
    switch (status) {
      case 400:
        ElMessage.error(data.message || 'Bad request')
        break
      case 401:
        ElMessage.error('Please log in to continue')
        // Redirect to login
        break
      case 403:
        ElMessage.error('You do not have permission to perform this action')
        break
      case 404:
        ElMessage.error('The requested resource was not found')
        break
      case 422:
        this.handleValidationErrors(data.errors)
        break
      case 500:
        ElNotification({
          title: 'Server Error',
          message: 'An internal server error occurred. Please try again later.',
          type: 'error',
          duration: 5000
        })
        break
      default:
        ElMessage.error(data.message || 'An unexpected error occurred')
    }
  }
  
  static handleNetworkError() {
    ElNotification({
      title: 'Network Error',
      message: 'Unable to connect to the server. Please check your internet connection.',
      type: 'error',
      duration: 0 // Don't auto-close
    })
  }
  
  static handleValidationErrors(errors) {
    if (typeof errors === 'object') {
      Object.values(errors).flat().forEach(error => {
        ElMessage.error(error)
      })
    } else {
      ElMessage.error('Validation failed')
    }
  }
  
  static handleGenericError(message) {
    ElMessage.error(message || 'An unexpected error occurred')
  }
}

// Usage in components
export const useErrorHandler = () => {
  const handleError = (error, context = '') => {
    ErrorHandler.handle(error, context)
  }
  
  return { handleError }
}

Performance Optimization

Lazy Loading

javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

// Lazy load route components
const routes = [
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/Dashboard.vue')
  },
  {
    path: '/users',
    name: 'Users',
    component: () => import('@/views/Users.vue')
  },
  {
    path: '/settings',
    name: 'Settings',
    component: () => import('@/views/Settings.vue')
  }
]

// Lazy load with chunk names for better debugging
const routes = [
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import(/* webpackChunkName: "dashboard" */ '@/views/Dashboard.vue')
  }
]

Component Optimization

vue
<template>
  <div class="optimized-component">
    <!-- Use v-memo for expensive renders -->
    <div v-memo="[expensiveData]" class="expensive-content">
      {{ processExpensiveData(expensiveData) }}
    </div>
    
    <!-- Use v-show for frequently toggled elements -->
    <div v-show="isVisible" class="toggle-content">
      Content that toggles frequently
    </div>
    
    <!-- Use v-if for conditionally rendered elements -->
    <div v-if="shouldRender" class="conditional-content">
      Content that rarely changes
    </div>
  </div>
</template>

<script setup>
import { ref, computed, shallowRef } from 'vue'

// Use shallowRef for large objects that don't need deep reactivity
const largeDataSet = shallowRef([])

// Memoize expensive computations
const expensiveComputation = computed(() => {
  return largeDataSet.value.reduce((acc, item) => {
    // Expensive operation
    return acc + item.value
  }, 0)
})

// Debounce user input
const searchQuery = ref('')
const debouncedSearch = useDebounceFn(() => {
  // Perform search
}, 300)

watch(searchQuery, debouncedSearch)
</script>

Testing Standards

Unit Test Structure

javascript
// tests/components/UserProfile.test.js
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { ElButton, ElForm } from 'element-plus'
import UserProfile from '@/components/UserProfile.vue'

describe('UserProfile', () => {
  let wrapper
  
  const defaultProps = {
    userId: '123',
    editable: true
  }
  
  const createWrapper = (props = {}) => {
    return mount(UserProfile, {
      props: { ...defaultProps, ...props },
      global: {
        components: {
          ElButton,
          ElForm
        }
      }
    })
  }
  
  beforeEach(() => {
    wrapper = createWrapper()
  })
  
  describe('rendering', () => {
    it('renders correctly with default props', () => {
      expect(wrapper.find('.user-profile').exists()).toBe(true)
      expect(wrapper.find('.profile-card').exists()).toBe(true)
    })
    
    it('shows edit button when editable is true', () => {
      expect(wrapper.find('[data-testid="edit-button"]').exists()).toBe(true)
    })
    
    it('hides edit button when editable is false', () => {
      wrapper = createWrapper({ editable: false })
      expect(wrapper.find('[data-testid="edit-button"]').exists()).toBe(false)
    })
  })
  
  describe('interactions', () => {
    it('emits update event when form is submitted', async () => {
      await wrapper.find('[data-testid="name-input"]').setValue('John Doe')
      await wrapper.find('[data-testid="email-input"]').setValue('john@example.com')
      await wrapper.find('[data-testid="submit-button"]').trigger('click')
      
      expect(wrapper.emitted('update')).toBeTruthy()
      expect(wrapper.emitted('update')[0][0]).toEqual({
        name: 'John Doe',
        email: 'john@example.com'
      })
    })
    
    it('validates form before submission', async () => {
      await wrapper.find('[data-testid="submit-button"]').trigger('click')
      
      expect(wrapper.find('.el-form-item__error').exists()).toBe(true)
      expect(wrapper.emitted('update')).toBeFalsy()
    })
  })
})

Documentation Standards

Component Documentation

vue
<template>
  <!-- Component template -->
</template>

<script setup>
/**
 * UserProfile Component
 * 
 * A reusable component for displaying and editing user profile information.
 * Supports both view and edit modes with form validation.
 * 
 * @example
 * <UserProfile 
 *   :user-id="123" 
 *   :editable="true" 
 *   @update="handleUpdate"
 *   @delete="handleDelete"
 * />
 */

/**
 * Component props
 * @typedef {Object} Props
 * @property {string|number} userId - The ID of the user to display
 * @property {boolean} [editable=true] - Whether the profile can be edited
 * @property {string} [size='medium'] - Size of the component (small, medium, large)
 */
const props = defineProps({
  userId: {
    type: [String, Number],
    required: true,
    description: 'The ID of the user to display'
  },
  editable: {
    type: Boolean,
    default: true,
    description: 'Whether the profile can be edited'
  }
})

/**
 * Component events
 * @typedef {Object} Events
 * @property {Function} update - Fired when user profile is updated
 * @property {Function} delete - Fired when user profile is deleted
 */
const emit = defineEmits({
  /**
   * Fired when user profile is updated
   * @param {Object} userData - The updated user data
   */
  update: (userData) => {
    return userData && typeof userData === 'object'
  },
  
  /**
   * Fired when user profile is deleted
   * @param {string|number} userId - The ID of the deleted user
   */
  delete: (userId) => {
    return userId != null
  }
})
</script>

Code Quality Tools

ESLint Configuration

javascript
// .eslintrc.js
module.exports = {
  root: true,
  env: {
    node: true,
    browser: true,
    es2022: true
  },
  extends: [
    'eslint:recommended',
    '@vue/eslint-config-typescript',
    '@vue/eslint-config-prettier'
  ],
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module'
  },
  rules: {
    // Vue specific rules
    'vue/component-name-in-template-casing': ['error', 'PascalCase'],
    'vue/component-definition-name-casing': ['error', 'PascalCase'],
    'vue/multi-word-component-names': 'error',
    'vue/no-unused-vars': 'error',
    'vue/require-default-prop': 'error',
    'vue/require-prop-types': 'error',
    
    // General rules
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-unused-vars': 'error',
    'prefer-const': 'error',
    'no-var': 'error',
    
    // Code style
    'indent': ['error', 2],
    'quotes': ['error', 'single'],
    'semi': ['error', 'never'],
    'comma-dangle': ['error', 'never']
  }
}

Prettier Configuration

javascript
// .prettierrc.js
module.exports = {
  semi: false,
  singleQuote: true,
  tabWidth: 2,
  trailingComma: 'none',
  printWidth: 100,
  bracketSpacing: true,
  arrowParens: 'avoid',
  vueIndentScriptAndStyle: false
}

Summary

Following these coding standards ensures:

  1. Consistency - Code looks and behaves uniformly across the project
  2. Maintainability - Code is easy to understand and modify
  3. Performance - Optimized patterns for better application performance
  4. Quality - Reduced bugs and improved reliability
  5. Collaboration - Team members can easily work on each other's code

Regularly review and update these standards as the project evolves and new best practices emerge.

Element Plus Study Guide