Day 73: Complex Component Communication Patterns
Learning Objectives
- Master advanced component communication patterns in Vue 3
- Learn complex interaction mechanisms between Element Plus components
- Practice cross-level component communication solutions
- Build scalable component communication architecture
1. Advanced Component Communication Patterns
1.1 Enhanced Event Bus Pattern
typescript
// Type-safe event bus
interface EventMap {
'user:login': { userId: string; username: string }
'user:logout': { userId: string }
'data:update': { type: string; payload: any }
'ui:theme-change': { theme: string }
'form:validate': { formId: string; isValid: boolean }
'table:selection-change': { selectedRows: any[] }
}
type EventKey = keyof EventMap
type EventHandler<K extends EventKey> = (data: EventMap[K]) => void
class TypedEventBus {
private events: Map<string, Set<Function>> = new Map()
private onceEvents: Map<string, Set<Function>> = new Map()
private middlewares: Array<(event: string, data: any) => boolean> = []
// Add middleware
use(middleware: (event: string, data: any) => boolean): void {
this.middlewares.push(middleware)
}
// Listen to events
on<K extends EventKey>(event: K, handler: EventHandler<K>): () => void {
if (!this.events.has(event)) {
this.events.set(event, new Set())
}
this.events.get(event)!.add(handler)
// Return unsubscribe function
return () => this.off(event, handler)
}
// Listen once
once<K extends EventKey>(event: K, handler: EventHandler<K>): void {
if (!this.onceEvents.has(event)) {
this.onceEvents.set(event, new Set())
}
this.onceEvents.get(event)!.add(handler)
}
// Unsubscribe
off<K extends EventKey>(event: K, handler: EventHandler<K>): void {
const handlers = this.events.get(event)
if (handlers) {
handlers.delete(handler)
}
const onceHandlers = this.onceEvents.get(event)
if (onceHandlers) {
onceHandlers.delete(handler)
}
}
// Emit event
emit<K extends EventKey>(event: K, data: EventMap[K]): void {
// Execute middleware
for (const middleware of this.middlewares) {
if (!middleware(event, data)) {
return // Middleware prevents event propagation
}
}
// Execute regular listeners
const handlers = this.events.get(event)
if (handlers) {
handlers.forEach(handler => {
try {
handler(data)
} catch (error) {
console.error(`Error in event handler for ${event}:`, error)
}
})
}
// Execute one-time listeners
const onceHandlers = this.onceEvents.get(event)
if (onceHandlers) {
onceHandlers.forEach(handler => {
try {
handler(data)
} catch (error) {
console.error(`Error in once event handler for ${event}:`, error)
}
})
onceHandlers.clear()
}
}
// Clear all listeners
clear(): void {
this.events.clear()
this.onceEvents.clear()
}
// Get event statistics
getStats(): Record<string, number> {
const stats: Record<string, number> = {}
for (const [event, handlers] of this.events) {
stats[event] = handlers.size
}
return stats
}
}
// Global event bus instance
export const eventBus = new TypedEventBus()
// Event bus middleware example
eventBus.use((event, data) => {
// Logging middleware
console.log(`Event emitted: ${event}`, data)
return true
})
eventBus.use((event, data) => {
// Permission check middleware
if (event.startsWith('admin:') && !checkAdminPermission()) {
console.warn(`Permission denied for event: ${event}`)
return false
}
return true
})
1.2 Dependency Injection Communication Pattern
typescript
// Dependency injection system
interface InjectionKey<T> extends Symbol {}
function createInjectionKey<T>(description: string): InjectionKey<T> {
return Symbol(description) as InjectionKey<T>
}
// Service interface definitions
interface UserService {
getCurrentUser(): Promise<User>
updateUser(user: Partial<User>): Promise<void>
logout(): Promise<void>
}
interface NotificationService {
success(message: string): void
error(message: string): void
warning(message: string): void
info(message: string): void
}
interface DataService {
fetch<T>(url: string): Promise<T>
post<T>(url: string, data: any): Promise<T>
put<T>(url: string, data: any): Promise<T>
delete(url: string): Promise<void>
}
// Injection key definitions
const USER_SERVICE_KEY = createInjectionKey<UserService>('userService')
const NOTIFICATION_SERVICE_KEY = createInjectionKey<NotificationService>('notificationService')
const DATA_SERVICE_KEY = createInjectionKey<DataService>('dataService')
// Service implementations
class DefaultUserService implements UserService {
private currentUser: Ref<User | null> = ref(null)
async getCurrentUser(): Promise<User> {
if (!this.currentUser.value) {
// Get user info from API
const user = await fetch('/api/user/current').then(res => res.json())
this.currentUser.value = user
}
return this.currentUser.value!
}
async updateUser(user: Partial<User>): Promise<void> {
await fetch('/api/user/update', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user)
})
if (this.currentUser.value) {
Object.assign(this.currentUser.value, user)
}
}
async logout(): Promise<void> {
await fetch('/api/auth/logout', { method: 'POST' })
this.currentUser.value = null
}
}
class ElementPlusNotificationService implements NotificationService {
success(message: string): void {
ElNotification({
type: 'success',
title: 'Success',
message
})
}
error(message: string): void {
ElNotification({
type: 'error',
title: 'Error',
message
})
}
warning(message: string): void {
ElNotification({
type: 'warning',
title: 'Warning',
message
})
}
info(message: string): void {
ElNotification({
type: 'info',
title: 'Info',
message
})
}
}
// Service provider composable
function useServiceProvider() {
const userService = new DefaultUserService()
const notificationService = new ElementPlusNotificationService()
const dataService = new DefaultDataService()
return {
provide: (app: App) => {
app.provide(USER_SERVICE_KEY, userService)
app.provide(NOTIFICATION_SERVICE_KEY, notificationService)
app.provide(DATA_SERVICE_KEY, dataService)
},
userService,
notificationService,
dataService
}
}
// Service consumer composables
function useUserService(): UserService {
const service = inject(USER_SERVICE_KEY)
if (!service) {
throw new Error('UserService not provided')
}
return service
}
function useNotification(): NotificationService {
const service = inject(NOTIFICATION_SERVICE_KEY)
if (!service) {
throw new Error('NotificationService not provided')
}
return service
}
function useDataService(): DataService {
const service = inject(DATA_SERVICE_KEY)
if (!service) {
throw new Error('DataService not provided')
}
return service
}
2. Cross-Level Component Communication
2.1 Context Pattern Implementation
typescript
// Context creation tool
function createContext<T>(defaultValue?: T) {
const key = Symbol('context')
function useProvideContext(value: T) {
provide(key, value)
return value
}
function useInjectContext(fallback?: T): T {
const value = inject(key, fallback ?? defaultValue)
if (value === undefined) {
throw new Error('Context not provided')
}
return value
}
return {
useProvideContext,
useInjectContext
}
}
// Form context
interface FormContextValue {
formData: Ref<Record<string, any>>
errors: Ref<Record<string, string>>
isSubmitting: Ref<boolean>
validateField: (field: string) => Promise<boolean>
setFieldValue: (field: string, value: any) => void
setFieldError: (field: string, error: string) => void
clearFieldError: (field: string) => void
submitForm: () => Promise<boolean>
resetForm: () => void
}
const { useProvideContext: useProvideFormContext, useInjectContext: useInjectFormContext } =
createContext<FormContextValue>()
// Form provider component
const FormProvider = defineComponent({
name: 'FormProvider',
props: {
initialData: {
type: Object,
default: () => ({})
},
validationRules: {
type: Object,
default: () => ({})
},
onSubmit: {
type: Function,
required: true
}
},
setup(props, { slots }) {
const formData = ref({ ...props.initialData })
const errors = ref<Record<string, string>>({})
const isSubmitting = ref(false)
const validateField = async (field: string): Promise<boolean> => {
const rule = props.validationRules[field]
if (!rule) return true
try {
const value = formData.value[field]
const result = await rule(value)
if (result === true) {
clearFieldError(field)
return true
} else {
setFieldError(field, result || 'Validation failed')
return false
}
} catch (error) {
setFieldError(field, error.message)
return false
}
}
const setFieldValue = (field: string, value: any) => {
formData.value[field] = value
// Automatically clear errors
if (errors.value[field]) {
clearFieldError(field)
}
}
const setFieldError = (field: string, error: string) => {
errors.value[field] = error
}
const clearFieldError = (field: string) => {
delete errors.value[field]
}
const submitForm = async (): Promise<boolean> => {
isSubmitting.value = true
try {
// Validate all fields
const fieldNames = Object.keys(props.validationRules)
const validationResults = await Promise.all(
fieldNames.map(field => validateField(field))
)
const isValid = validationResults.every(result => result)
if (isValid) {
await props.onSubmit(formData.value)
return true
}
return false
} finally {
isSubmitting.value = false
}
}
const resetForm = () => {
formData.value = { ...props.initialData }
errors.value = {}
}
const contextValue: FormContextValue = {
formData,
errors,
isSubmitting,
validateField,
setFieldValue,
setFieldError,
clearFieldError,
submitForm,
resetForm
}
useProvideFormContext(contextValue)
return () => slots.default?.()
}
})
// Form field component
const FormField = defineComponent({
name: 'FormField',
props: {
name: {
type: String,
required: true
},
label: String,
required: Boolean
},
setup(props, { slots }) {
const formContext = useInjectFormContext()
const fieldValue = computed({
get: () => formContext.formData.value[props.name],
set: (value) => formContext.setFieldValue(props.name, value)
})
const fieldError = computed(() => formContext.errors.value[props.name])
const validate = () => formContext.validateField(props.name)
return () => (
<div class="form-field">
{props.label && (
<label class={['form-field__label', { 'is-required': props.required }]}>
{props.label}
</label>
)}
<div class="form-field__content">
{slots.default?.({
value: fieldValue.value,
onChange: (value: any) => fieldValue.value = value,
onBlur: validate,
error: fieldError.value
})}
</div>
{fieldError.value && (
<div class="form-field__error">{fieldError.value}</div>
)}
</div>
)
}
})
2.2 State Management Communication Pattern
typescript
// Component state management
interface ComponentState {
id: string
type: string
props: Record<string, any>
state: Record<string, any>
children: string[]
parent?: string
}
class ComponentStateManager {
private components: Map<string, ComponentState> = new Map()
private subscribers: Map<string, Set<Function>> = new Map()
private globalSubscribers: Set<Function> = new Set()
// Register component
register(component: ComponentState): void {
this.components.set(component.id, component)
this.notifySubscribers(component.id, 'register', component)
}
// Unregister component
unregister(componentId: string): void {
const component = this.components.get(componentId)
if (component) {
this.components.delete(componentId)
this.subscribers.delete(componentId)
this.notifySubscribers(componentId, 'unregister', component)
}
}
// Update component state
updateState(componentId: string, updates: Record<string, any>): void {
const component = this.components.get(componentId)
if (component) {
Object.assign(component.state, updates)
this.notifySubscribers(componentId, 'state-update', { componentId, updates })
}
}
// Update component props
updateProps(componentId: string, updates: Record<string, any>): void {
const component = this.components.get(componentId)
if (component) {
Object.assign(component.props, updates)
this.notifySubscribers(componentId, 'props-update', { componentId, updates })
}
}
// Get component state
getComponent(componentId: string): ComponentState | undefined {
return this.components.get(componentId)
}
// Get child components
getChildren(componentId: string): ComponentState[] {
const component = this.components.get(componentId)
if (!component) return []
return component.children
.map(childId => this.components.get(childId))
.filter(Boolean) as ComponentState[]
}
// Get parent component
getParent(componentId: string): ComponentState | undefined {
const component = this.components.get(componentId)
if (!component || !component.parent) return undefined
return this.components.get(component.parent)
}
// Get sibling components
getSiblings(componentId: string): ComponentState[] {
const component = this.components.get(componentId)
if (!component || !component.parent) return []
const parent = this.components.get(component.parent)
if (!parent) return []
return parent.children
.filter(childId => childId !== componentId)
.map(childId => this.components.get(childId))
.filter(Boolean) as ComponentState[]
}
// Find components
findComponents(predicate: (component: ComponentState) => boolean): ComponentState[] {
return Array.from(this.components.values()).filter(predicate)
}
// Subscribe to component changes
subscribe(componentId: string, callback: Function): () => void {
if (!this.subscribers.has(componentId)) {
this.subscribers.set(componentId, new Set())
}
this.subscribers.get(componentId)!.add(callback)
return () => {
const subs = this.subscribers.get(componentId)
if (subs) {
subs.delete(callback)
}
}
}
// Subscribe to global changes
subscribeGlobal(callback: Function): () => void {
this.globalSubscribers.add(callback)
return () => {
this.globalSubscribers.delete(callback)
}
}
// Notify subscribers
private notifySubscribers(componentId: string, event: string, data: any): void {
// Notify component-specific subscribers
const componentSubs = this.subscribers.get(componentId)
if (componentSubs) {
componentSubs.forEach(callback => {
try {
callback(event, data)
} catch (error) {
console.error('Error in component subscriber:', error)
}
})
}
// Notify global subscribers
this.globalSubscribers.forEach(callback => {
try {
callback(event, data)
} catch (error) {
console.error('Error in global subscriber:', error)
}
})
}
// Component communication
sendMessage(fromId: string, toId: string, message: any): void {
const fromComponent = this.components.get(fromId)
const toComponent = this.components.get(toId)
if (fromComponent && toComponent) {
this.notifySubscribers(toId, 'message', {
from: fromId,
to: toId,
message,
timestamp: Date.now()
})
}
}
// Broadcast message
broadcast(fromId: string, message: any, filter?: (component: ComponentState) => boolean): void {
const fromComponent = this.components.get(fromId)
if (!fromComponent) return
for (const [componentId, component] of this.components) {
if (componentId !== fromId && (!filter || filter(component))) {
this.sendMessage(fromId, componentId, message)
}
}
}
}
// Global component state manager
export const componentStateManager = new ComponentStateManager()
// Component state management composable
function useComponentState(componentId: string, initialState: Record<string, any> = {}) {
const state = reactive(initialState)
onMounted(() => {
componentStateManager.register({
id: componentId,
type: getCurrentInstance()?.type.name || 'Unknown',
props: getCurrentInstance()?.props || {},
state,
children: [],
parent: undefined
})
})
onUnmounted(() => {
componentStateManager.unregister(componentId)
})
const updateState = (updates: Record<string, any>) => {
Object.assign(state, updates)
componentStateManager.updateState(componentId, updates)
}
const sendMessage = (toId: string, message: any) => {
componentStateManager.sendMessage(componentId, toId, message)
}
const broadcast = (message: any, filter?: (component: ComponentState) => boolean) => {
componentStateManager.broadcast(componentId, message, filter)
}
const subscribe = (callback: Function) => {
return componentStateManager.subscribe(componentId, callback)
}
return {
state: readonly(state),
updateState,
sendMessage,
broadcast,
subscribe
}
}
3. Element Plus Component Communication Enhancement
3.1 Table and Form Linkage
typescript
// Table-form linkage manager
class TableFormLinker {
private tableRef: Ref<any>
private formRef: Ref<any>
private linkConfig: TableFormLinkConfig
constructor(tableRef: Ref<any>, formRef: Ref<any>, config: TableFormLinkConfig) {
this.tableRef = tableRef
this.formRef = formRef
this.linkConfig = config
this.setupLinking()
}
private setupLinking(): void {
// Monitor table selection changes
watch(
() => this.tableRef.value?.getSelectionRows?.(),
(selectedRows) => {
if (selectedRows && selectedRows.length > 0) {
this.handleTableSelection(selectedRows)
}
},
{ deep: true }
)
// Monitor form data changes
if (this.linkConfig.formToTable) {
watch(
() => this.formRef.value?.model,
(formData) => {
if (formData) {
this.handleFormChange(formData)
}
},
{ deep: true }
)
}
}
private handleTableSelection(selectedRows: any[]): void {
const { tableToForm } = this.linkConfig
if (tableToForm && this.formRef.value) {
if (selectedRows.length === 1) {
// Single selection: fill form
const selectedRow = selectedRows[0]
const formData = this.mapTableRowToForm(selectedRow, tableToForm.mapping)
Object.keys(formData).forEach(key => {
this.formRef.value.model[key] = formData[key]
})
if (tableToForm.onSelection) {
tableToForm.onSelection(selectedRow, formData)
}
} else if (selectedRows.length > 1) {
// Multiple selection: batch operation mode
if (tableToForm.onMultiSelection) {
tableToForm.onMultiSelection(selectedRows)
}
}
}
}
private handleFormChange(formData: any): void {
const { formToTable } = this.linkConfig
if (formToTable && this.tableRef.value) {
// Filter table based on form data
if (formToTable.filterTable) {
const filterConditions = this.mapFormToTableFilter(formData, formToTable.mapping)
this.applyTableFilter(filterConditions)
}
if (formToTable.onChange) {
formToTable.onChange(formData)
}
}
}
private mapTableRowToForm(row: any, mapping: Record<string, string>): any {
const formData: any = {}
Object.entries(mapping).forEach(([formField, tableField]) => {
if (row[tableField] !== undefined) {
formData[formField] = row[tableField]
}
})
return formData
}
private mapFormToTableFilter(formData: any, mapping: Record<string, string>): any {
const filterConditions: any = {}
Object.entries(mapping).forEach(([formField, tableField]) => {
if (formData[formField] !== undefined && formData[formField] !== '') {
filterConditions[tableField] = formData[formField]
}
})
return filterConditions
}
private applyTableFilter(conditions: any): void {
// Apply table filter conditions
if (this.tableRef.value && this.tableRef.value.clearFilter) {
this.tableRef.value.clearFilter()
Object.entries(conditions).forEach(([column, value]) => {
if (this.tableRef.value.filter) {
this.tableRef.value.filter(column, value)
}
})
}
}
// Manually sync data
syncTableToForm(): void {
const selectedRows = this.tableRef.value?.getSelectionRows?.()
if (selectedRows && selectedRows.length > 0) {
this.handleTableSelection(selectedRows)
}
}
syncFormToTable(): void {
const formData = this.formRef.value?.model
if (formData) {
this.handleFormChange(formData)
}
}
}
interface TableFormLinkConfig {
tableToForm?: {
mapping: Record<string, string> // formField -> tableField
onSelection?: (row: any, formData: any) => void
onMultiSelection?: (rows: any[]) => void
}
formToTable?: {
mapping: Record<string, string> // formField -> tableField
filterTable?: boolean
onChange?: (formData: any) => void
}
}
// Using composable function
function useTableFormLink(config: TableFormLinkConfig) {
const tableRef = ref()
const formRef = ref()
let linker: TableFormLinker | null = null
onMounted(() => {
if (tableRef.value && formRef.value) {
linker = new TableFormLinker(tableRef, formRef, config)
}
})
onUnmounted(() => {
linker = null
})
return {
tableRef,
formRef,
syncTableToForm: () => linker?.syncTableToForm(),
syncFormToTable: () => linker?.syncFormToTable()
}
}
3.2 Dialog and Page Communication
typescript
// Dialog communication manager
class DialogCommunicationManager {
private dialogs: Map<string, DialogInstance> = new Map()
private messageQueue: Map<string, any[]> = new Map()
// Register dialog
registerDialog(id: string, instance: DialogInstance): void {
this.dialogs.set(id, instance)
// Process queued messages
const queuedMessages = this.messageQueue.get(id) || []
queuedMessages.forEach(message => {
instance.receiveMessage(message)
})
this.messageQueue.delete(id)
}
// Unregister dialog
unregisterDialog(id: string): void {
this.dialogs.delete(id)
}
// Send message to dialog
sendToDialog(dialogId: string, message: any): void {
const dialog = this.dialogs.get(dialogId)
if (dialog) {
dialog.receiveMessage(message)
} else {
// Dialog not registered, add to queue
if (!this.messageQueue.has(dialogId)) {
this.messageQueue.set(dialogId, [])
}
this.messageQueue.get(dialogId)!.push(message)
}
}
// Send message from dialog to page
sendFromDialog(dialogId: string, message: any): void {
eventBus.emit('dialog:message', {
dialogId,
message,
timestamp: Date.now()
})
}
// Broadcast message to all dialogs
broadcast(message: any): void {
this.dialogs.forEach((dialog, id) => {
dialog.receiveMessage({
...message,
broadcast: true,
targetId: id
})
})
}
// Get active dialog list
getActiveDialogs(): string[] {
return Array.from(this.dialogs.keys())
}
}
interface DialogInstance {
receiveMessage(message: any): void
}
// Global dialog communication manager
export const dialogManager = new DialogCommunicationManager()
// Dialog communication composable
function useDialogCommunication(dialogId: string) {
const messageHandlers: Map<string, Function[]> = new Map()
const dialogInstance: DialogInstance = {
receiveMessage(message: any) {
const handlers = messageHandlers.get(message.type) || []
handlers.forEach(handler => {
try {
handler(message)
} catch (error) {
console.error('Error in dialog message handler:', error)
}
})
}
}
onMounted(() => {
dialogManager.registerDialog(dialogId, dialogInstance)
})
onUnmounted(() => {
dialogManager.unregisterDialog(dialogId)
})
const onMessage = (type: string, handler: Function) => {
if (!messageHandlers.has(type)) {
messageHandlers.set(type, [])
}
messageHandlers.get(type)!.push(handler)
}
const sendMessage = (message: any) => {
dialogManager.sendFromDialog(dialogId, message)
}
const sendToDialog = (targetDialogId: string, message: any) => {
dialogManager.sendToDialog(targetDialogId, message)
}
return {
onMessage,
sendMessage,
sendToDialog
}
}
// Page dialog communication composable
function usePageDialogCommunication() {
const sendToDialog = (dialogId: string, message: any) => {
dialogManager.sendToDialog(dialogId, message)
}
const broadcast = (message: any) => {
dialogManager.broadcast(message)
}
const onDialogMessage = (handler: (data: any) => void) => {
return eventBus.on('dialog:message', handler)
}
return {
sendToDialog,
broadcast,
onDialogMessage
}
}
4. Practical Exercises
Event Bus Implementation:
- Implement a type-safe event bus
- Add event middleware mechanism
- Create event debugging tools
Dependency Injection System:
- Design service injection architecture
- Implement service lifecycle management
- Create service testing framework
Complex Component Communication:
- Implement table-form linkage
- Create dialog communication system
- Design cross-page component communication
5. Learning Resources
- Vue 3 Provide/Inject
- Vue 3 Composition API
- Element Plus Component Communication
- Advanced Vue.js Patterns
6. Assignments
- Implement a complete component communication framework
- Develop a table-form linkage system
- Create a dialog communication manager
- Design component communication best practices documentation
Summary
Through Day 73's learning, we have mastered:
- Advanced Communication Patterns: Understanding event bus, dependency injection, and other advanced communication patterns
- Cross-Level Communication: Learning Context pattern and state management communication
- Element Plus Integration: Mastering complex communication implementation in Element Plus components
- Practical Applications: Practicing table-form linkage, dialog communication, and other real-world scenarios
These skills will help us build more flexible and maintainable Element Plus application architectures.