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