forked from trent_larson/crowd-funder-for-time-pwa
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.
7.1 KiB
7.1 KiB
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?
- Better TypeScript Support: Full type checking of parameters and return values
- Superior IDE Navigation: Ctrl+click takes you directly to implementation
- Explicit Contracts: Clear declaration of what functions a component needs
- Easier Testing: Simple to mock and test in isolation
- Flexibility: Can pass any function, not just event handlers
When to Use $emit
- DOM-like Events:
@click,@input,@submit,@change - Lifecycle Events:
@mounted,@before-unmount,@updated - Form Validation:
@validation-error,@validation-success - Event Bubbling: When events need to bubble through multiple components
- Vue DevTools Integration: When you want events visible in DevTools timeline
Implementation Patterns
Function Props Pattern
// 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();
}
}
<!-- Parent Template -->
<MyComponent
:on-save="handleSave"
:on-cancel="handleCancel"
:on-validate="validateForm"
/>
$emit Pattern (for DOM-like events)
// Child Component
@Component({
name: "FormComponent"
})
export default class FormComponent extends Vue {
@Emit("submit")
handleSubmit() {
return this.formData;
}
@Emit("input")
handleInput(value: string) {
return value;
}
}
<!-- Parent Template -->
<FormComponent
@submit="handleFormSubmit"
@input="handleInputChange"
/>
Automatic Code Generation Guidelines
Component Template Generation
When generating component templates, follow these patterns:
Function Props Template
<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)
<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:
// 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:
// 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
// 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
// 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
// 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
// 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
- Identify business logic events (not DOM events)
- Add function props to component interface
- Update parent components to pass functions
- Remove $emit decorators and event handlers
- Update tests to use function mocks
Example Migration
Before ($emit):
@Emit("save")
handleSave() {
return this.formData;
}
After (Function Props):
@Prop({ required: true }) onSave!: (data: FormData) => void;
handleSave() {
this.onSave(this.formData);
}
Best Practices Summary
- Use function props for business logic, data operations, and complex interactions
- Use $emit for DOM-like events, lifecycle events, and simple user interactions
- Be consistent within your codebase
- Document your patterns for team alignment
- Consider TypeScript when choosing between approaches
- Test both patterns appropriately
Code Generation Templates
Component Generator Input
interface ComponentSpec {
name: string;
props: Array<{
name: string;
type: string;
required: boolean;
isFunction: boolean;
}>;
events: Array<{
name: string;
payloadType?: string;
}>;
template: string;
}
Generated Output
// 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.