Browse Source
Create comprehensive development guide establishing our preferred patterns for Vue component communication. Document the preference for function props over $emit for business logic while reserving $emit for DOM-like events. Guide covers: - Function props for business logic, data operations, and complex interactions - $emit for DOM-like events, lifecycle events, and simple user interactions - Implementation patterns with TypeScript examples - Testing strategies for both approaches - Migration strategy from $emit to function props - Naming conventions and best practices Establishes consistent, maintainable component communication patterns across the application with focus on type safety and developer experience.web-serve-fix
2 changed files with 317 additions and 3 deletions
@ -0,0 +1,314 @@ |
|||
# Component Communication Guide |
|||
|
|||
## Overview |
|||
|
|||
This guide establishes our preferred patterns for component communication in Vue.js applications, with a focus on maintainability, type safety, and developer experience. |
|||
|
|||
## Core Principle: Function Props Over $emit |
|||
|
|||
**Preference**: Use function props for business logic and data operations, reserve $emit for DOM-like events. |
|||
|
|||
### Why Function Props? |
|||
|
|||
1. **Better TypeScript Support**: Full type checking of parameters and return values |
|||
2. **Superior IDE Navigation**: Ctrl+click takes you directly to implementation |
|||
3. **Explicit Contracts**: Clear declaration of what functions a component needs |
|||
4. **Easier Testing**: Simple to mock and test in isolation |
|||
5. **Flexibility**: Can pass any function, not just event handlers |
|||
|
|||
### When to Use $emit |
|||
|
|||
1. **DOM-like Events**: `@click`, `@input`, `@submit`, `@change` |
|||
2. **Lifecycle Events**: `@mounted`, `@before-unmount`, `@updated` |
|||
3. **Form Validation**: `@validation-error`, `@validation-success` |
|||
4. **Event Bubbling**: When events need to bubble through multiple components |
|||
5. **Vue DevTools Integration**: When you want events visible in DevTools timeline |
|||
|
|||
## Implementation Patterns |
|||
|
|||
### Function Props Pattern |
|||
|
|||
```typescript |
|||
// Child Component |
|||
@Component({ |
|||
name: "MyComponent" |
|||
}) |
|||
export default class MyComponent extends Vue { |
|||
@Prop({ required: true }) onSave!: (data: SaveData) => Promise<void>; |
|||
@Prop({ required: true }) onCancel!: () => void; |
|||
@Prop({ required: false }) onValidate?: (data: FormData) => boolean; |
|||
|
|||
async handleSave() { |
|||
const data = this.collectFormData(); |
|||
await this.onSave(data); |
|||
} |
|||
|
|||
handleCancel() { |
|||
this.onCancel(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
```vue |
|||
<!-- Parent Template --> |
|||
<MyComponent |
|||
:on-save="handleSave" |
|||
:on-cancel="handleCancel" |
|||
:on-validate="validateForm" |
|||
/> |
|||
``` |
|||
|
|||
### $emit Pattern (for DOM-like events) |
|||
|
|||
```typescript |
|||
// Child Component |
|||
@Component({ |
|||
name: "FormComponent" |
|||
}) |
|||
export default class FormComponent extends Vue { |
|||
@Emit("submit") |
|||
handleSubmit() { |
|||
return this.formData; |
|||
} |
|||
|
|||
@Emit("input") |
|||
handleInput(value: string) { |
|||
return value; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
```vue |
|||
<!-- Parent Template --> |
|||
<FormComponent |
|||
@submit="handleFormSubmit" |
|||
@input="handleInputChange" |
|||
/> |
|||
``` |
|||
|
|||
## Automatic Code Generation Guidelines |
|||
|
|||
### Component Template Generation |
|||
|
|||
When generating component templates, follow these patterns: |
|||
|
|||
#### Function Props Template |
|||
```vue |
|||
<template> |
|||
<div class="component-name"> |
|||
<!-- Component content --> |
|||
<button @click="handleAction"> |
|||
{{ buttonText }} |
|||
</button> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { Component, Vue, Prop } from "vue-facing-decorator"; |
|||
|
|||
@Component({ |
|||
name: "ComponentName" |
|||
}) |
|||
export default class ComponentName extends Vue { |
|||
@Prop({ required: true }) onAction!: () => void; |
|||
@Prop({ required: true }) buttonText!: string; |
|||
@Prop({ required: false }) disabled?: boolean; |
|||
|
|||
handleAction() { |
|||
if (!this.disabled) { |
|||
this.onAction(); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
``` |
|||
|
|||
#### $emit Template (for DOM events) |
|||
```vue |
|||
<template> |
|||
<div class="component-name"> |
|||
<!-- Component content --> |
|||
<button @click="handleClick"> |
|||
{{ buttonText }} |
|||
</button> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { Component, Vue, Prop, Emit } from "vue-facing-decorator"; |
|||
|
|||
@Component({ |
|||
name: "ComponentName" |
|||
}) |
|||
export default class ComponentName extends Vue { |
|||
@Prop({ required: true }) buttonText!: string; |
|||
@Prop({ required: false }) disabled?: boolean; |
|||
|
|||
@Emit("click") |
|||
handleClick() { |
|||
return { disabled: this.disabled }; |
|||
} |
|||
} |
|||
</script> |
|||
``` |
|||
|
|||
### Code Generation Rules |
|||
|
|||
#### 1. Function Props for Business Logic |
|||
- **Data operations**: Save, delete, update, validate |
|||
- **Navigation**: Route changes, modal opening/closing |
|||
- **State management**: Store actions, state updates |
|||
- **API calls**: Data fetching, form submissions |
|||
|
|||
#### 2. $emit for User Interactions |
|||
- **Click events**: Button clicks, link navigation |
|||
- **Form events**: Input changes, form submissions |
|||
- **Lifecycle events**: Component mounting, unmounting |
|||
- **UI events**: Focus, blur, scroll, resize |
|||
|
|||
#### 3. Naming Conventions |
|||
|
|||
**Function Props:** |
|||
```typescript |
|||
// Action-oriented names |
|||
onSave: (data: SaveData) => Promise<void> |
|||
onDelete: (id: string) => Promise<void> |
|||
onUpdate: (item: Item) => void |
|||
onValidate: (data: FormData) => boolean |
|||
onNavigate: (route: string) => void |
|||
``` |
|||
|
|||
**$emit Events:** |
|||
```typescript |
|||
// Event-oriented names |
|||
@click: (event: MouseEvent) => void |
|||
@input: (value: string) => void |
|||
@submit: (data: FormData) => void |
|||
@focus: (event: FocusEvent) => void |
|||
@mounted: () => void |
|||
``` |
|||
|
|||
### TypeScript Integration |
|||
|
|||
#### Function Prop Types |
|||
```typescript |
|||
// Define reusable function types |
|||
interface SaveHandler { |
|||
(data: SaveData): Promise<void>; |
|||
} |
|||
|
|||
interface ValidationHandler { |
|||
(data: FormData): boolean; |
|||
} |
|||
|
|||
// Use in components |
|||
@Prop({ required: true }) onSave!: SaveHandler; |
|||
@Prop({ required: true }) onValidate!: ValidationHandler; |
|||
``` |
|||
|
|||
#### Event Types |
|||
```typescript |
|||
// Define event payload types |
|||
interface ClickEvent { |
|||
target: HTMLElement; |
|||
timestamp: number; |
|||
} |
|||
|
|||
@Emit("click") |
|||
handleClick(): ClickEvent { |
|||
return { |
|||
target: this.$el, |
|||
timestamp: Date.now() |
|||
}; |
|||
} |
|||
``` |
|||
|
|||
## Testing Guidelines |
|||
|
|||
### Function Props Testing |
|||
```typescript |
|||
// Easy to mock and test |
|||
const mockOnSave = jest.fn(); |
|||
const wrapper = mount(MyComponent, { |
|||
propsData: { |
|||
onSave: mockOnSave |
|||
} |
|||
}); |
|||
|
|||
await wrapper.vm.handleSave(); |
|||
expect(mockOnSave).toHaveBeenCalledWith(expectedData); |
|||
``` |
|||
|
|||
### $emit Testing |
|||
```typescript |
|||
// Requires event simulation |
|||
const wrapper = mount(MyComponent); |
|||
await wrapper.find('button').trigger('click'); |
|||
expect(wrapper.emitted('click')).toBeTruthy(); |
|||
``` |
|||
|
|||
## Migration Strategy |
|||
|
|||
### From $emit to Function Props |
|||
|
|||
1. **Identify business logic events** (not DOM events) |
|||
2. **Add function props** to component interface |
|||
3. **Update parent components** to pass functions |
|||
4. **Remove $emit decorators** and event handlers |
|||
5. **Update tests** to use function mocks |
|||
|
|||
### Example Migration |
|||
|
|||
**Before ($emit):** |
|||
```typescript |
|||
@Emit("save") |
|||
handleSave() { |
|||
return this.formData; |
|||
} |
|||
``` |
|||
|
|||
**After (Function Props):** |
|||
```typescript |
|||
@Prop({ required: true }) onSave!: (data: FormData) => void; |
|||
|
|||
handleSave() { |
|||
this.onSave(this.formData); |
|||
} |
|||
``` |
|||
|
|||
## Best Practices Summary |
|||
|
|||
1. **Use function props** for business logic, data operations, and complex interactions |
|||
2. **Use $emit** for DOM-like events, lifecycle events, and simple user interactions |
|||
3. **Be consistent** within your codebase |
|||
4. **Document your patterns** for team alignment |
|||
5. **Consider TypeScript** when choosing between approaches |
|||
6. **Test both patterns** appropriately |
|||
|
|||
## Code Generation Templates |
|||
|
|||
### Component Generator Input |
|||
```typescript |
|||
interface ComponentSpec { |
|||
name: string; |
|||
props: Array<{ |
|||
name: string; |
|||
type: string; |
|||
required: boolean; |
|||
isFunction: boolean; |
|||
}>; |
|||
events: Array<{ |
|||
name: string; |
|||
payloadType?: string; |
|||
}>; |
|||
template: string; |
|||
} |
|||
``` |
|||
|
|||
### Generated Output |
|||
```typescript |
|||
// Generator should automatically choose function props vs $emit |
|||
// based on the nature of the interaction (business logic vs DOM event) |
|||
``` |
|||
|
|||
This guide ensures consistent, maintainable component communication patterns across the application. |
Loading…
Reference in new issue