How to use $emit with parameters in Vue
Using $emit with parameters in Vue enables powerful parent-child component communication by passing data, event details, and complex payloads from child components to their parents. As the creator of CoreUI, a widely used open-source UI library, I’ve implemented $emit with parameters in thousands of Vue components for form submissions, user interactions, data updates, and complex event handling across enterprise component libraries. From my expertise, the most effective approach is to use descriptive event names with structured parameter objects. This method provides clear component APIs, type-safe event handling, and maintainable parent-child communication while following Vue’s unidirectional data flow principles.
Use $emit
with descriptive event names and structured parameters to pass data from child to parent components.
<!-- Child Component: CustomButton.vue -->
<template>
<button
:class="buttonClasses"
:disabled="disabled || loading"
@click="handleClick"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
<span v-if="loading" class="spinner"></span>
<slot></slot>
</button>
</template>
<script>
export default {
name: 'CustomButton',
props: {
variant: {
type: String,
default: 'primary'
},
size: {
type: String,
default: 'medium'
},
disabled: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
},
data: {
type: Object,
default: () => ({})
}
},
computed: {
buttonClasses() {
return [
'custom-button',
`custom-button--${this.variant}`,
`custom-button--${this.size}`,
{
'custom-button--disabled': this.disabled,
'custom-button--loading': this.loading
}
]
}
},
methods: {
// 1. Simple event with single parameter
handleClick(event) {
if (this.disabled || this.loading) return
// Emit with single parameter
this.$emit('click', event)
// Emit with multiple parameters
this.$emit('button-clicked', event, this.variant, this.size)
// Emit with structured object parameter
this.$emit('action', {
type: 'click',
variant: this.variant,
size: this.size,
timestamp: new Date().toISOString(),
element: event.target,
data: this.data
})
},
// 2. Mouse events with position data
handleMouseEnter(event) {
this.$emit('hover-start', {
type: 'mouseenter',
position: {
x: event.clientX,
y: event.clientY
},
target: event.target,
variant: this.variant
})
},
handleMouseLeave(event) {
this.$emit('hover-end', {
type: 'mouseleave',
position: {
x: event.clientX,
y: event.clientY
},
duration: this.calculateHoverDuration(),
target: event.target
})
},
calculateHoverDuration() {
// Implementation for calculating hover duration
return Date.now() - (this.hoverStartTime || Date.now())
}
}
}
</script>
<!-- Child Component: UserForm.vue -->
<template>
<form @submit.prevent="handleSubmit" class="user-form">
<div class="form-group">
<label>Name:</label>
<input
v-model="form.name"
type="text"
@input="handleInputChange('name', $event)"
@blur="handleFieldBlur('name')"
>
</div>
<div class="form-group">
<label>Email:</label>
<input
v-model="form.email"
type="email"
@input="handleInputChange('email', $event)"
@blur="handleFieldBlur('email')"
>
</div>
<div class="form-group">
<label>Age:</label>
<input
v-model.number="form.age"
type="number"
@input="handleInputChange('age', $event)"
>
</div>
<div class="form-actions">
<button type="submit" :disabled="!isFormValid">Submit</button>
<button type="button" @click="handleReset">Reset</button>
<button type="button" @click="handleCancel">Cancel</button>
</div>
</form>
</template>
<script>
export default {
name: 'UserForm',
props: {
initialData: {
type: Object,
default: () => ({})
}
},
data() {
return {
form: {
name: '',
email: '',
age: null,
...this.initialData
},
errors: {},
touchedFields: new Set()
}
},
computed: {
isFormValid() {
return Object.keys(this.errors).length === 0 &&
this.form.name.trim() &&
this.form.email.trim()
}
},
methods: {
// 3. Form field change events with validation
handleInputChange(fieldName, event) {
const value = event.target.value
// Update form data
this.form[fieldName] = fieldName === 'age' ? parseInt(value) || null : value
// Validate field
this.validateField(fieldName, value)
// Emit field change event with multiple parameters
this.$emit('field-change', fieldName, value, {
isValid: !this.errors[fieldName],
form: { ...this.form },
event
})
// Emit specific field events
this.$emit(`${fieldName}-changed`, {
value,
isValid: !this.errors[fieldName],
errors: this.errors[fieldName] || null
})
// Emit form state change
this.$emit('form-update', {
field: fieldName,
value,
form: { ...this.form },
errors: { ...this.errors },
isValid: this.isFormValid,
changedFields: Array.from(this.touchedFields)
})
},
// 4. Field blur events with validation
handleFieldBlur(fieldName) {
this.touchedFields.add(fieldName)
this.$emit('field-blur', {
field: fieldName,
value: this.form[fieldName],
isTouched: true,
errors: this.errors[fieldName] || null
})
},
// 5. Form submission with complete data
handleSubmit() {
// Final validation
this.validateAllFields()
if (this.isFormValid) {
this.$emit('submit', {
formData: { ...this.form },
isValid: true,
timestamp: new Date().toISOString(),
touchedFields: Array.from(this.touchedFields)
})
// Also emit success event
this.$emit('form-success', this.form)
} else {
this.$emit('submit', {
formData: { ...this.form },
isValid: false,
errors: { ...this.errors },
timestamp: new Date().toISOString()
})
// Emit validation error event
this.$emit('validation-error', {
errors: this.errors,
form: this.form
})
}
},
// 6. Form reset with previous state
handleReset() {
const previousData = { ...this.form }
this.form = {
name: '',
email: '',
age: null,
...this.initialData
}
this.errors = {}
this.touchedFields.clear()
this.$emit('reset', {
previousData,
newData: { ...this.form },
timestamp: new Date().toISOString()
})
},
// 7. Form cancellation
handleCancel() {
const hasChanges = this.touchedFields.size > 0
this.$emit('cancel', {
hasUnsavedChanges: hasChanges,
form: { ...this.form },
touchedFields: Array.from(this.touchedFields)
})
},
// Validation helper methods
validateField(fieldName, value) {
delete this.errors[fieldName]
switch (fieldName) {
case 'name':
if (!value.trim()) {
this.$set(this.errors, fieldName, 'Name is required')
}
break
case 'email':
if (!value.trim()) {
this.$set(this.errors, fieldName, 'Email is required')
} else if (!this.isValidEmail(value)) {
this.$set(this.errors, fieldName, 'Invalid email format')
}
break
case 'age':
if (value !== null && (value < 0 || value > 150)) {
this.$set(this.errors, fieldName, 'Age must be between 0 and 150')
}
break
}
},
validateAllFields() {
Object.keys(this.form).forEach(field => {
this.validateField(field, this.form[field])
})
},
isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}
}
}
</script>
<!-- Parent Component using the child components -->
<template>
<div class="parent-component">
<h2>Component Communication Examples</h2>
<!-- 1. Button with event handling -->
<div class="section">
<h3>Custom Button Events</h3>
<CustomButton
variant="primary"
size="large"
:data="{ userId: 123, action: 'save' }"
@click="handleButtonClick"
@button-clicked="handleButtonClicked"
@action="handleButtonAction"
@hover-start="handleHoverStart"
@hover-end="handleHoverEnd"
>
Save Changes
</CustomButton>
<div class="event-log">
<h4>Button Events:</h4>
<pre>{{ JSON.stringify(buttonEvents, null, 2) }}</pre>
</div>
</div>
<!-- 2. Form with comprehensive event handling -->
<div class="section">
<h3>User Form Events</h3>
<UserForm
:initial-data="{ name: 'John Doe' }"
@field-change="handleFieldChange"
@name-changed="handleNameChanged"
@email-changed="handleEmailChanged"
@field-blur="handleFieldBlur"
@form-update="handleFormUpdate"
@submit="handleFormSubmit"
@form-success="handleFormSuccess"
@validation-error="handleValidationError"
@reset="handleFormReset"
@cancel="handleFormCancel"
/>
<div class="event-log">
<h4>Form Events:</h4>
<pre>{{ JSON.stringify(formEvents.slice(-5), null, 2) }}</pre>
</div>
</div>
<!-- 3. Dynamic component with parameter passing -->
<div class="section">
<h3>Dynamic Events</h3>
<button @click="triggerCustomEvent">Trigger Custom Event</button>
</div>
</div>
</template>
<script>
import CustomButton from './CustomButton.vue'
import UserForm from './UserForm.vue'
export default {
name: 'ParentComponent',
components: {
CustomButton,
UserForm
},
data() {
return {
buttonEvents: [],
formEvents: []
}
},
methods: {
// Button event handlers
handleButtonClick(event) {
this.buttonEvents.push({
type: 'click',
timestamp: new Date().toISOString(),
event: 'Simple click event'
})
},
handleButtonClicked(event, variant, size) {
this.buttonEvents.push({
type: 'button-clicked',
timestamp: new Date().toISOString(),
data: { variant, size }
})
},
handleButtonAction(actionData) {
this.buttonEvents.push({
type: 'action',
timestamp: new Date().toISOString(),
data: actionData
})
},
handleHoverStart(hoverData) {
console.log('Hover started:', hoverData)
},
handleHoverEnd(hoverData) {
console.log('Hover ended:', hoverData)
},
// Form event handlers
handleFieldChange(fieldName, value, metadata) {
this.formEvents.push({
type: 'field-change',
field: fieldName,
value,
metadata
})
},
handleNameChanged(data) {
console.log('Name changed:', data)
},
handleEmailChanged(data) {
console.log('Email changed:', data)
},
handleFieldBlur(data) {
console.log('Field blur:', data)
},
handleFormUpdate(updateData) {
this.formEvents.push({
type: 'form-update',
data: updateData
})
},
handleFormSubmit(submitData) {
this.formEvents.push({
type: 'submit',
data: submitData
})
if (submitData.isValid) {
// Process valid form submission
console.log('Processing form:', submitData.formData)
} else {
// Handle validation errors
console.log('Form has errors:', submitData.errors)
}
},
handleFormSuccess(formData) {
console.log('Form submitted successfully:', formData)
// Show success message, redirect, etc.
},
handleValidationError(errorData) {
console.log('Validation errors:', errorData)
// Show error message
},
handleFormReset(resetData) {
console.log('Form reset:', resetData)
},
handleFormCancel(cancelData) {
console.log('Form cancelled:', cancelData)
if (cancelData.hasUnsavedChanges) {
// Show confirmation dialog
const confirmed = confirm('You have unsaved changes. Are you sure you want to cancel?')
if (confirmed) {
// Handle cancellation
}
}
},
// 8. Dynamic event emission
triggerCustomEvent() {
// Simulate emitting event with complex parameters
this.$emit('custom-action', {
action: 'complex-operation',
data: {
items: [1, 2, 3],
metadata: {
timestamp: new Date().toISOString(),
user: 'current-user'
}
},
callback: (result) => {
console.log('Operation completed:', result)
}
})
}
}
}
</script>
Use $emit
with multiple parameters by passing additional arguments after the event name. Structure complex data as objects for better maintainability and type safety. Use descriptive event names that clearly indicate the action and context. Pass both the raw event data and processed metadata when helpful. Consider emitting multiple events for different use cases (general and specific). Always document the expected parameters in component documentation. Use Vue DevTools to debug event emissions and verify parameter passing during development.
Best Practice Note:
This is the same approach we use in CoreUI Vue components for comprehensive parent-child communication and event handling. Use structured parameter objects for complex data, emit multiple events for different granularities, include timestamps and metadata for debugging, validate emitted parameters in development, and document event contracts clearly in component APIs.