Skip to content

Component Design Pattern Analysis ​

Overview ​

This document provides an in-depth analysis of Element Plus component design patterns, exploring the architectural principles, design philosophies, and implementation strategies that make Element Plus a robust and maintainable UI library.

Core Design Principles ​

1. Composition over Inheritance ​

Element Plus follows the composition pattern extensively, allowing components to be built by combining smaller, focused pieces rather than inheriting from large base classes.

typescript
// Example: Button component composition
import { computed, inject } from 'vue'
import { useNamespace } from '@element-plus/hooks'
import { buttonGroupContextKey } from './constants'

export const useButton = (props: ButtonProps) => {
  const ns = useNamespace('button')
  const buttonGroupContext = inject(buttonGroupContextKey, undefined)
  
  const buttonSize = computed(() => {
    return props.size || buttonGroupContext?.size || 'default'
  })
  
  const buttonType = computed(() => {
    return props.type || buttonGroupContext?.type || 'default'
  })
  
  return {
    ns,
    buttonSize,
    buttonType
  }
}

2. Single Responsibility Principle ​

Each component has a clear, single responsibility, making them easier to understand, test, and maintain.

typescript
// Example: Separate concerns in Form components

// FormItem - handles validation and layout
export const FormItem = defineComponent({
  name: 'ElFormItem',
  setup(props) {
    const { validateField, clearValidate } = useFormItem(props)
    return { validateField, clearValidate }
  }
})

// Input - handles user input
export const Input = defineComponent({
  name: 'ElInput',
  setup(props) {
    const { handleInput, handleFocus, handleBlur } = useInput(props)
    return { handleInput, handleFocus, handleBlur }
  }
})

3. Dependency Inversion ​

Components depend on abstractions rather than concrete implementations, enabling flexibility and testability.

typescript
// Abstract validation interface
interface ValidationRule {
  required?: boolean
  message?: string
  validator?: (rule: any, value: any, callback: any) => void
}

// Form component depends on abstraction
export const useFormValidation = (rules: ValidationRule[]) => {
  const validate = async (value: any): Promise<boolean> => {
    for (const rule of rules) {
      if (rule.validator) {
        const isValid = await new Promise(resolve => {
          rule.validator!(rule, value, (error?: Error) => {
            resolve(!error)
          })
        })
        if (!isValid) return false
      }
    }
    return true
  }
  
  return { validate }
}

Component Architecture Patterns ​

1. Provider-Consumer Pattern ​

Used extensively for component communication and context sharing.

typescript
// Provider: Form component
export const Form = defineComponent({
  name: 'ElForm',
  setup(props, { slots }) {
    const formContext = {
      model: props.model,
      rules: props.rules,
      validateField: (prop: string) => { /* validation logic */ },
      clearValidate: (props?: string[]) => { /* clear logic */ }
    }
    
    provide(formContextKey, formContext)
    
    return () => slots.default?.()
  }
})

// Consumer: FormItem component
export const FormItem = defineComponent({
  name: 'ElFormItem',
  setup(props) {
    const formContext = inject(formContextKey)
    
    const validate = () => {
      return formContext?.validateField(props.prop)
    }
    
    return { validate }
  }
})

2. Compound Component Pattern ​

Components that work together to form a cohesive unit.

typescript
// Menu compound components
export const Menu = defineComponent({
  name: 'ElMenu',
  setup(props, { slots }) {
    const menuContext = {
      activeIndex: ref(props.defaultActive),
      openedMenus: ref([]),
      handleMenuItemClick: (index: string) => { /* handle click */ }
    }
    
    provide(menuContextKey, menuContext)
    
    return () => (
      <ul class="el-menu">
        {slots.default?.()}
      </ul>
    )
  }
})

export const MenuItem = defineComponent({
  name: 'ElMenuItem',
  setup(props) {
    const menuContext = inject(menuContextKey)
    
    const handleClick = () => {
      menuContext?.handleMenuItemClick(props.index)
    }
    
    return () => (
      <li class="el-menu-item" onClick={handleClick}>
        {slots.default?.()}
      </li>
    )
  }
})

3. Render Props Pattern (Vue Slots) ​

Providing flexibility through customizable rendering.

typescript
// Table component with flexible rendering
export const Table = defineComponent({
  name: 'ElTable',
  setup(props, { slots }) {
    const renderCell = (row: any, column: TableColumn) => {
      // Check for custom slot
      if (slots[column.prop]) {
        return slots[column.prop]!({ row, column, $index: row.index })
      }
      
      // Check for column render function
      if (column.render) {
        return column.render(row, column)
      }
      
      // Default rendering
      return row[column.prop]
    }
    
    return { renderCell }
  }
})

State Management Patterns ​

1. Local State with Composables ​

typescript
// useToggle composable for managing boolean state
export const useToggle = (initialValue = false) => {
  const state = ref(initialValue)
  
  const toggle = () => {
    state.value = !state.value
  }
  
  const setTrue = () => {
    state.value = true
  }
  
  const setFalse = () => {
    state.value = false
  }
  
  return {
    state: readonly(state),
    toggle,
    setTrue,
    setFalse
  }
}

// Usage in Dialog component
export const Dialog = defineComponent({
  setup(props) {
    const { state: visible, setTrue: show, setFalse: hide } = useToggle(props.modelValue)
    
    return { visible, show, hide }
  }
})

2. Reactive State Management ​

typescript
// useFormState for complex form management
export const useFormState = (initialData: Record<string, any>) => {
  const formData = reactive({ ...initialData })
  const errors = reactive<Record<string, string>>({})
  const touched = reactive<Record<string, boolean>>({})
  
  const setFieldValue = (field: string, value: any) => {
    formData[field] = value
    touched[field] = true
    
    // Clear error when field is modified
    if (errors[field]) {
      delete errors[field]
    }
  }
  
  const setFieldError = (field: string, error: string) => {
    errors[field] = error
  }
  
  const reset = () => {
    Object.assign(formData, initialData)
    Object.keys(errors).forEach(key => delete errors[key])
    Object.keys(touched).forEach(key => delete touched[key])
  }
  
  return {
    formData: readonly(formData),
    errors: readonly(errors),
    touched: readonly(touched),
    setFieldValue,
    setFieldError,
    reset
  }
}

Event Handling Patterns ​

1. Event Delegation ​

typescript
// Table component using event delegation
export const Table = defineComponent({
  setup(props) {
    const handleTableClick = (event: MouseEvent) => {
      const target = event.target as HTMLElement
      
      // Handle row click
      const row = target.closest('.el-table__row')
      if (row) {
        const rowIndex = parseInt(row.dataset.index || '0')
        emit('row-click', props.data[rowIndex], rowIndex)
        return
      }
      
      // Handle cell click
      const cell = target.closest('.el-table__cell')
      if (cell) {
        const cellIndex = parseInt(cell.dataset.index || '0')
        emit('cell-click', target, cellIndex)
        return
      }
    }
    
    return { handleTableClick }
  }
})

2. Event Bus Pattern ​

typescript
// Global event bus for component communication
class EventBus {
  private events: Map<string, Function[]> = new Map()
  
  on(event: string, callback: Function) {
    if (!this.events.has(event)) {
      this.events.set(event, [])
    }
    this.events.get(event)!.push(callback)
  }
  
  emit(event: string, ...args: any[]) {
    const callbacks = this.events.get(event)
    if (callbacks) {
      callbacks.forEach(callback => callback(...args))
    }
  }
  
  off(event: string, callback?: Function) {
    if (!callback) {
      this.events.delete(event)
      return
    }
    
    const callbacks = this.events.get(event)
    if (callbacks) {
      const index = callbacks.indexOf(callback)
      if (index > -1) {
        callbacks.splice(index, 1)
      }
    }
  }
}

export const eventBus = new EventBus()

// Usage in Message component
export const useMessage = () => {
  const show = (options: MessageOptions) => {
    eventBus.emit('message:show', options)
  }
  
  const hide = (id: string) => {
    eventBus.emit('message:hide', id)
  }
  
  return { show, hide }
}

Performance Optimization Patterns ​

1. Lazy Loading Pattern ​

typescript
// Lazy loading for large components
export const useLazyComponent = (loader: () => Promise<any>) => {
  const component = ref(null)
  const loading = ref(false)
  const error = ref(null)
  
  const load = async () => {
    if (component.value) return component.value
    
    loading.value = true
    error.value = null
    
    try {
      const loaded = await loader()
      component.value = loaded.default || loaded
      return component.value
    } catch (err) {
      error.value = err
      throw err
    } finally {
      loading.value = false
    }
  }
  
  return {
    component: readonly(component),
    loading: readonly(loading),
    error: readonly(error),
    load
  }
}

// Usage in Select component for large option lists
export const Select = defineComponent({
  setup(props) {
    const { component: OptionList, load: loadOptions } = useLazyComponent(
      () => import('./OptionList.vue')
    )
    
    const handleDropdownShow = async () => {
      if (props.options.length > 1000) {
        await loadOptions()
      }
    }
    
    return { OptionList, handleDropdownShow }
  }
})

2. Virtual Scrolling Pattern ​

typescript
// Virtual scrolling for large lists
export const useVirtualScroll = (options: {
  items: Ref<any[]>
  itemHeight: number
  containerHeight: number
}) => {
  const scrollTop = ref(0)
  const startIndex = computed(() => {
    return Math.floor(scrollTop.value / options.itemHeight)
  })
  
  const endIndex = computed(() => {
    const visibleCount = Math.ceil(options.containerHeight / options.itemHeight)
    return Math.min(startIndex.value + visibleCount + 1, options.items.value.length)
  })
  
  const visibleItems = computed(() => {
    return options.items.value.slice(startIndex.value, endIndex.value)
  })
  
  const totalHeight = computed(() => {
    return options.items.value.length * options.itemHeight
  })
  
  const offsetY = computed(() => {
    return startIndex.value * options.itemHeight
  })
  
  const handleScroll = (event: Event) => {
    const target = event.target as HTMLElement
    scrollTop.value = target.scrollTop
  }
  
  return {
    visibleItems,
    totalHeight,
    offsetY,
    handleScroll
  }
}

Testing Patterns ​

1. Component Testing with Composition API ​

typescript
// Testing composables in isolation
import { describe, it, expect } from 'vitest'
import { useToggle } from '../composables/useToggle'

describe('useToggle', () => {
  it('should initialize with default value', () => {
    const { state } = useToggle()
    expect(state.value).toBe(false)
  })
  
  it('should toggle state', () => {
    const { state, toggle } = useToggle()
    toggle()
    expect(state.value).toBe(true)
    toggle()
    expect(state.value).toBe(false)
  })
})

// Testing components with context
import { mount } from '@vue/test-utils'
import { provide } from 'vue'
import Form from '../Form.vue'
import FormItem from '../FormItem.vue'

describe('Form integration', () => {
  it('should provide context to form items', () => {
    const wrapper = mount({
      template: `
        <Form :model="model" :rules="rules">
          <FormItem prop="name">
            <input v-model="model.name" />
          </FormItem>
        </Form>
      `,
      components: { Form, FormItem },
      data() {
        return {
          model: { name: '' },
          rules: { name: [{ required: true, message: 'Name is required' }] }
        }
      }
    })
    
    expect(wrapper.findComponent(FormItem).exists()).toBe(true)
  })
})

Best Practices ​

1. Component API Design ​

  • Consistent naming: Use clear, descriptive names for props, events, and slots
  • Prop validation: Always provide proper TypeScript types and runtime validation
  • Event naming: Use kebab-case for events and include descriptive prefixes
  • Slot design: Provide meaningful slot names and scope data

2. Performance Considerations ​

  • Computed properties: Use computed properties for derived state
  • Event handlers: Avoid creating new functions in templates
  • Watchers: Use watchers sparingly and clean them up properly
  • Memory leaks: Always clean up event listeners and timers

3. Accessibility ​

  • ARIA attributes: Include proper ARIA labels and roles
  • Keyboard navigation: Implement keyboard support for interactive elements
  • Focus management: Handle focus states appropriately
  • Screen readers: Ensure compatibility with assistive technologies

Conclusion ​

Element Plus demonstrates excellent component design patterns that promote:

  • Maintainability: Clear separation of concerns and modular architecture
  • Reusability: Composable functions and flexible component APIs
  • Performance: Optimized rendering and efficient state management
  • Developer Experience: Intuitive APIs and comprehensive TypeScript support
  • Accessibility: Built-in support for assistive technologies

These patterns serve as excellent examples for building robust, scalable Vue.js applications and component libraries.

Element Plus Study Guide