You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

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?

  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

// 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

  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):

@Emit("save")
handleSave() {
  return this.formData;
}

After (Function Props):

@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

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.