Hone logo
Hone
Problems

Vue Custom Event Emitter

This challenge focuses on implementing a custom event emitter pattern within a Vue.js application using TypeScript. Understanding how to create and manage custom events is crucial for building flexible and decoupled Vue components that can communicate with each other without direct dependencies.

Problem Description

You are tasked with creating a reusable EventEmitter class that can be used throughout your Vue application to facilitate cross-component communication. This EventEmitter should allow components to:

  1. Register listeners for specific event names.
  2. Emit events with optional data.
  3. Remove listeners to prevent memory leaks.

You will then integrate this EventEmitter into a simple Vue component system to demonstrate its functionality.

Key Requirements:

  • EventEmitter Class:
    • A listeners property (e.g., a Map or an object) to store event names and their corresponding callback functions.
    • A on(eventName: string, callback: Function) method to register a listener. If the event already has listeners, the new callback should be added to the existing list.
    • A emit(eventName: string, ...args: any[]) method to trigger all callbacks registered for a given eventName. The arguments passed to emit should be forwarded to the callbacks.
    • A off(eventName: string, callback?: Function) method to remove listeners. If no callback is provided, all listeners for eventName should be removed. If a callback is provided, only that specific callback should be removed.
  • Vue Component Integration:
    • Create a parent component (App.vue or similar) that will manage an instance of the EventEmitter.
    • Create at least two child components (ComponentA.vue and ComponentB.vue).
    • ComponentA should be able to emit an event using the shared EventEmitter instance.
    • ComponentB should listen for this emitted event and update its own state or display a message based on the event data.

Expected Behavior:

  1. When an instance of EventEmitter is created, it should be empty of listeners.
  2. When on is called multiple times for the same event, all callbacks should be executed when the event is emitted.
  3. When emit is called with an event name that has registered listeners, all corresponding callbacks should be invoked with the provided arguments.
  4. When off is called without a callback, all listeners for that event should be cleared.
  5. When off is called with a specific callback, only that callback should be removed, and other callbacks for the same event should remain.
  6. In the Vue integration, ComponentA should trigger an event (e.g., dataUpdated) with some payload. ComponentB should react to this event by displaying the payload or a derived message.

Edge Cases:

  • Emitting an event for which no listeners are registered should not cause errors.
  • Removing a listener that does not exist should not cause errors.
  • Handling null or undefined as arguments passed to emit.

Examples

Example 1: Basic EventEmitter Usage (Conceptual)

// Conceptual Example - Not directly Vue integration yet

const emitter = new EventEmitter();

const handler1 = (message: string) => {
  console.log('Handler 1 received:', message);
};

const handler2 = (count: number) => {
  console.log('Handler 2 received count:', count);
};

emitter.on('greeting', handler1);
emitter.on('greeting', (message: string) => {
  console.log('Another greeting handler:', message.toUpperCase());
});
emitter.on('countUpdate', handler2);

// Output:
// Handler 1 received: Hello!
// Another greeting handler: HELLO!
// Handler 2 received count: 10
emitter.emit('greeting', 'Hello!');
emitter.emit('countUpdate', 10);

emitter.off('greeting', handler1);

// Output:
// Another greeting handler: GOODBYE!
emitter.emit('greeting', 'Goodbye!');

emitter.off('greeting'); // Removes all remaining 'greeting' listeners

emitter.emit('greeting', 'This will not be logged');

Example 2: Vue Component Integration Scenario

Imagine ComponentA fetches data and wants to inform other parts of the application. ComponentB needs to display this data.

  • App.vue:
    • Instantiates EventEmitter.
    • Passes the EventEmitter instance down to ComponentA and ComponentB (e.g., via provide/inject or props).
  • ComponentA.vue:
    • Has a button that, when clicked, fetches some dummy data (e.g., a user's name) and emits a userLoggedIn event with the user's name as payload using the shared EventEmitter.
  • ComponentB.vue:
    • Registers a listener for the userLoggedIn event on the shared EventEmitter.
    • When the userLoggedIn event is received, it updates its local state to store the user's name and displays a welcome message.
// --- App.vue ---
<template>
  <div>
    <ComponentA :emitter="eventBus" />
    <ComponentB :emitter="eventBus" />
  </div>
</template>

<script lang="ts">
import { defineComponent, provide } from 'vue';
import ComponentA from './components/ComponentA.vue';
import ComponentB from './components/ComponentB.vue';
import EventEmitter from './eventEmitter'; // Assuming EventEmitter is in ./eventEmitter.ts

export default defineComponent({
  components: {
    ComponentA,
    ComponentB,
  },
  setup() {
    const eventBus = new EventEmitter();
    // Using provide for simplicity, props could also be used
    provide('eventBus', eventBus);
    return {
      eventBus,
    };
  },
});
</script>

// --- ComponentA.vue ---
<template>
  <div>
    <button @click="loginUser">Login User</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, inject } from 'vue';

export default defineComponent({
  setup() {
    const eventBus = inject('eventBus');

    const loginUser = () => {
      const userName = 'Alice'; // Simulate fetching user data
      if (eventBus) {
        (eventBus as any).emit('userLoggedIn', userName);
      }
    };

    return {
      loginUser,
    };
  },
});
</script>

// --- ComponentB.vue ---
<template>
  <div>
    <p v-if="loggedInUser">Welcome, {{ loggedInUser }}!</p>
    <p v-else>Please log in.</p>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, onMounted, onUnmounted, inject } from 'vue';

export default defineComponent({
  setup() {
    const loggedInUser = ref<string | null>(null);
    const eventBus = inject('eventBus');

    const handleUserLogin = (userName: string) => {
      loggedInUser.value = userName;
    };

    onMounted(() => {
      if (eventBus) {
        (eventBus as any).on('userLoggedIn', handleUserLogin);
      }
    });

    onUnmounted(() => {
      if (eventBus) {
        (eventBus as any).off('userLoggedIn', handleUserLogin);
      }
    });

    return {
      loggedInUser,
    };
  },
});
</script>

Example 3: Multiple Listeners and Removal

Demonstrates more complex listener management.

  • Scenario: A "global notification" event is emitted. Multiple components might listen for it, each reacting differently. A specific component might later decide to stop listening.
// Imagine ComponentC and ComponentD also listen for 'globalNotification'
// ComponentC displays a simple toast.
// ComponentD increments a counter.

// When 'globalNotification' is emitted:
// - ComponentC's toast appears.
// - ComponentD's counter increases.

// If ComponentC is unmounted and cleans up its listener:
// - Emitting 'globalNotification' only affects ComponentD's counter.

Constraints

  • The EventEmitter class must be implemented in TypeScript.
  • The Vue components should be set up using Vue 3's Composition API.
  • The provided EventEmitter instance should be shared between components, and you should choose an appropriate mechanism for this (e.g., provide/inject or passing via props).
  • Memory leaks must be prevented by ensuring listeners are removed when components are unmounted (onUnmounted hook).
  • Do not use any external event emitter libraries. Implement the core logic yourself.

Notes

  • Consider using a Map<string, Function[]> or a similar structure to store listeners, mapping event names to arrays of callback functions.
  • Ensure type safety where possible, especially when defining the EventEmitter class and its methods.
  • The provide/inject mechanism is a good way to share the EventEmitter instance across the component tree in Vue 3.
  • Think about how to gracefully handle situations where eventBus might not be injected (though in this controlled example, it should always be available).
  • Success will be measured by the correct implementation of the EventEmitter class and its seamless integration into the Vue components, resulting in reliable event propagation and cleanup.
Loading editor...
typescript