forked from trent_larson/crowd-funder-for-time-pwa
Add component communication guide with function props preference
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.
This commit is contained in:
314
doc/component-communication-guide.md
Normal file
314
doc/component-communication-guide.md
Normal file
@@ -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.
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
|
import { Component, Vue, Prop } from "vue-facing-decorator";
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -96,6 +96,7 @@ export default class UsageLimitsSection extends Vue {
|
|||||||
@Prop({ required: false }) activeDid?: string;
|
@Prop({ required: false }) activeDid?: string;
|
||||||
@Prop({ required: false }) endorserLimits?: any;
|
@Prop({ required: false }) endorserLimits?: any;
|
||||||
@Prop({ required: false }) imageLimits?: any;
|
@Prop({ required: false }) imageLimits?: any;
|
||||||
|
@Prop({ required: true }) onRecheckLimits!: () => void;
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
// Component mounted
|
// Component mounted
|
||||||
@@ -114,9 +115,8 @@ export default class UsageLimitsSection extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Emit("recheck-limits")
|
|
||||||
recheckLimits() {
|
recheckLimits() {
|
||||||
// Emit recheck-limits event
|
this.onRecheckLimits();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user