|
| 1 | +--- |
| 2 | +title: Dispatcher |
| 3 | +description: A simple event dispatcher for Nuxt. |
| 4 | +seo: |
| 5 | + title: Dispatcher file |
| 6 | + description: A type-safe, channel-based event dispatcher system for Nuxt with automatic cleanup and flexible event handling. |
| 7 | +tags: [] |
| 8 | +date: 2026-01-14 |
| 9 | +progress: release |
| 10 | +repository: |
| 11 | + repoUsername: CTRL-Neo-Studios |
| 12 | + repoName: dispatcher |
| 13 | + showIssues: true |
| 14 | + showWiki: false |
| 15 | +navigation: |
| 16 | + icon: i-lucide-git-branch |
| 17 | +--- |
| 18 | + |
| 19 | +A type-safe, channel-based event dispatcher system for Nuxt with automatic cleanup and flexible event handling. |
| 20 | + |
| 21 | +✨ [Release Notes](https://github.com/CTRL-Neo-Studios/dispatcher/blob/dev/CHANGELOG.md) |
| 22 | + |
| 23 | +## Features |
| 24 | + |
| 25 | +- Simple event dispatcher system wrapping [mitt](https://github.com/developit/mitt) |
| 26 | +- Create event buses with strong types quickly |
| 27 | +- Create custom event types |
| 28 | +- TypeScript-safe and type-safe |
| 29 | + |
| 30 | +## Installation |
| 31 | + |
| 32 | +```bash |
| 33 | +bun add @type32/dispatcher |
| 34 | +``` |
| 35 | + |
| 36 | +## Quick Start |
| 37 | + |
| 38 | +### 1. Define Your Events |
| 39 | + |
| 40 | +Create a typed event schema using the `DispatcherEvent` wrapper: |
| 41 | + |
| 42 | +```ts [types/events.ts] |
| 43 | +import type { DispatcherEvent } from '@type32/dispatcher' |
| 44 | + |
| 45 | +export interface AppEvents { |
| 46 | + user: { |
| 47 | + login: DispatcherEvent<{ username: string; id: number }> |
| 48 | + logout: DispatcherEvent // No payload |
| 49 | + } |
| 50 | + notification: { |
| 51 | + show: DispatcherEvent<{ message: string; type: 'success' | 'error' | 'info' }> |
| 52 | + hide: DispatcherEvent |
| 53 | + } |
| 54 | + modal: { |
| 55 | + open: DispatcherEvent<{ modalId: string; props?: Record<string, any> }> |
| 56 | + close: DispatcherEvent<{ modalId: string }> |
| 57 | + } |
| 58 | +} |
| 59 | +``` |
| 60 | + |
| 61 | +### 2. Use the Event Dispatcher |
| 62 | + |
| 63 | +The `useEventDispatcher` composable is auto-imported in Nuxt: |
| 64 | + |
| 65 | +```vue |
| 66 | +<script setup> |
| 67 | +import type { AppEvents } from '~/types/events' |
| 68 | +
|
| 69 | +// Create a dispatcher with a channel key (recommended) |
| 70 | +const events = useEventDispatcher<AppEvents>('app') |
| 71 | +
|
| 72 | +// Listen to events (auto-cleanup on unmount) |
| 73 | +events.on('user.login', (data) => { |
| 74 | + console.log('User logged in:', data. username, data.id) |
| 75 | +}) |
| 76 | +
|
| 77 | +events.on('notification.show', (data) => { |
| 78 | + // Show notification UI |
| 79 | + console.log(data.message, data.type) |
| 80 | +}) |
| 81 | +
|
| 82 | +// Emit events |
| 83 | +const handleLogin = () => { |
| 84 | + events.emit('user.login', { username: 'john', id: 123 }) |
| 85 | +} |
| 86 | +
|
| 87 | +const handleLogout = () => { |
| 88 | + events.emit('user.logout') // No payload required |
| 89 | +} |
| 90 | +</script> |
| 91 | +``` |
| 92 | + |
| 93 | +## Channel Keys |
| 94 | + |
| 95 | +**Channel keys are highly recommended**, especially when using the dispatcher within composables. |
| 96 | + |
| 97 | +### ✅ With Channel Key (Recommended) |
| 98 | + |
| 99 | +```ts [composables/useNotifications.ts] |
| 100 | +export function useNotifications() { |
| 101 | + // Same channel key = same event bus across all usages |
| 102 | + const events = useEventDispatcher<AppEvents>('app') |
| 103 | + |
| 104 | + const showNotification = (message: string, type: 'success' | 'error') => { |
| 105 | + events.emit('notification.show', { message, type }) |
| 106 | + } |
| 107 | + |
| 108 | + return { showNotification } |
| 109 | +} |
| 110 | +``` |
| 111 | + |
| 112 | +```vue [pages/index.vue] |
| 113 | +// Same key - will receive events |
| 114 | +const events = useEventDispatcher<AppEvents>('app') |
| 115 | +events.on('notification.show', (data) => { |
| 116 | + // This works! Same channel key = shared bus |
| 117 | +}) |
| 118 | +``` |
| 119 | + |
| 120 | +```vue [pages/other.vue] |
| 121 | +// Trigger from anywhere |
| 122 | +const { showNotification } = useNotifications() |
| 123 | +showNotification('Hello!', 'success') |
| 124 | +``` |
| 125 | + |
| 126 | +### ❌ Without Channel Key (Not Recommended for Composables) |
| 127 | + |
| 128 | +```ts [composables/useNotifications.ts] |
| 129 | +export function useNotifications() { |
| 130 | + // ⚠️ Each call creates a NEW isolated bus! |
| 131 | + const events = useEventDispatcher<AppEvents>() |
| 132 | + |
| 133 | + const showNotification = (message: string) => { |
| 134 | + events.emit('notification.show', { message, type: 'info' }) |
| 135 | + } |
| 136 | + |
| 137 | + return { showNotification } |
| 138 | +} |
| 139 | +``` |
| 140 | + |
| 141 | +```vue [pages/index.vue] |
| 142 | +const events = useEventDispatcher<AppEvents>() // Different bus! |
| 143 | +events.on('notification.show', (data) => { |
| 144 | + // ❌ Won't receive events from useNotifications() |
| 145 | +}) |
| 146 | +``` |
| 147 | + |
| 148 | +**When to omit channel keys:** |
| 149 | + |
| 150 | +- Component-local events that don't need to be shared |
| 151 | +- Temporary event buses for isolated features |
| 152 | +- Testing and prototyping on one single file |
| 153 | + |
| 154 | +## API Reference |
| 155 | + |
| 156 | +### `useEventDispatcher<TEvents>(channelKey?: string)` |
| 157 | + |
| 158 | +Creates or retrieves an event dispatcher for the specified channel. |
| 159 | + |
| 160 | +**Parameters:** |
| 161 | + |
| 162 | +- `channelKey` (optional): String identifier for the channel. Omit to create an isolated instance. |
| 163 | + |
| 164 | +**Returns:** Event dispatcher instance with the following methods: |
| 165 | + |
| 166 | +#### `emit<K>(event: K, payload?: T)` |
| 167 | + |
| 168 | +Emit a typed event with an optional payload. |
| 169 | + |
| 170 | +```ts |
| 171 | +events.emit('user.login', { username: 'john', id: 123 }) |
| 172 | +events.emit('user.logout') // No payload |
| 173 | +``` |
| 174 | + |
| 175 | +#### `on<K>(event: K, handler: (payload: T) => void)` |
| 176 | + |
| 177 | +Listen to an event. Automatically cleaned up on component unmount. |
| 178 | + |
| 179 | +```ts |
| 180 | +events.on('user.login', (data) => { |
| 181 | + console.log(data.username, data.id) |
| 182 | +}) |
| 183 | +``` |
| 184 | + |
| 185 | +#### `off<K>(event: K, handler: (payload: T) => void)` |
| 186 | + |
| 187 | +Manually remove an event listener. |
| 188 | + |
| 189 | +```ts |
| 190 | +const handler = (data) => console.log(data) |
| 191 | +events.on('user.login', handler) |
| 192 | +events.off('user.login', handler) |
| 193 | +``` |
| 194 | + |
| 195 | +#### `once<K>(event: K, handler: (payload: T) => void)` |
| 196 | + |
| 197 | +Listen to an event once, then automatically remove the listener. |
| 198 | + |
| 199 | +```ts |
| 200 | +events.once('modal.close', (data) => { |
| 201 | + console.log('Modal closed:', data.modalId) |
| 202 | +}) |
| 203 | +``` |
| 204 | + |
| 205 | +#### `yeet(event: string, payload?: any)` |
| 206 | + |
| 207 | +Fire-and-forget untyped event (no type checking, no history). |
| 208 | + |
| 209 | +```ts |
| 210 | +events.yeet('debug.log', { message: 'Something happened' }) |
| 211 | +events.yeet('analytics.track', 'button-clicked') |
| 212 | +``` |
| 213 | + |
| 214 | +#### `catch(event: string, handler: (payload: any) => void)` |
| 215 | + |
| 216 | +Listen to untyped events. |
| 217 | + |
| 218 | +```ts |
| 219 | +events.catch('debug.log', (data) => { |
| 220 | + console.log('Debug:', data) |
| 221 | +}) |
| 222 | +``` |
| 223 | + |
| 224 | +#### `uncatch(event: string, handler: (payload: any) => void)` |
| 225 | + |
| 226 | +Remove an untyped event listener. |
| 227 | + |
| 228 | +#### `clear()` |
| 229 | + |
| 230 | +Remove all listeners from this dispatcher instance. |
| 231 | + |
| 232 | +```ts |
| 233 | +events.clear() |
| 234 | +``` |
| 235 | + |
| 236 | +## Features |
| 237 | + |
| 238 | +### ✅ Full Type Safety |
| 239 | + |
| 240 | +IntelliSense autocompletes event paths and validates payload types: |
| 241 | + |
| 242 | +```ts |
| 243 | +events.emit('user.login', { username: 'john', id: 123 }) // ✅ Valid |
| 244 | +events.emit('user.login', { wrong: 'data' }) // ❌ TypeScript error |
| 245 | +events.emit('user.logout', { extra: 'data' }) // ❌ TypeScript error |
| 246 | +``` |
| 247 | + |
| 248 | +### ✅ Automatic Cleanup |
| 249 | + |
| 250 | +All event listeners are automatically removed when the component unmounts (using onUnmounted provided by vue) - no manual cleanup needed! |
| 251 | + |
| 252 | +### ✅ Dot-Notation Namespacing |
| 253 | + |
| 254 | +Organize events hierarchically: |
| 255 | + |
| 256 | +```ts |
| 257 | +events.on('window.file.newFile', handler) |
| 258 | +events.on('player.movement.walk', handler) |
| 259 | +events.on('ui.modal.open', handler) |
| 260 | +``` |
| 261 | + |
| 262 | +### ✅ Channel-Based Isolation |
| 263 | + |
| 264 | +Multiple channels for different contexts: |
| 265 | + |
| 266 | +```ts |
| 267 | +const uiEvents = useEventDispatcher<UIEvents>('ui') |
| 268 | +const gameEvents = useEventDispatcher<GameEvents>('game') |
| 269 | + |
| 270 | +// Events don't cross channels |
| 271 | +uiEvents.emit('modal.open', { modalId: 'settings' }) |
| 272 | +// gameEvents won't receive this |
| 273 | +``` |
| 274 | + |
| 275 | +### ✅ Wild Events (UDP-style) |
| 276 | + |
| 277 | +For debug logs, analytics, or temporary events without type constraints: |
| 278 | + |
| 279 | +***Note****: Wild events called using* `yeet()` *can only be caught by using the* `catch()` *function. The wild event bus is separate from the normal event bus.* |
| 280 | + |
| 281 | +```ts |
| 282 | +events.yeet('temp.debug', { whatever: 'data' }) |
| 283 | +events.catch('temp.debug', console.log) |
| 284 | +``` |
| 285 | + |
| 286 | +## Common Patterns |
| 287 | + |
| 288 | +### Global Event Bus |
| 289 | + |
| 290 | +```ts [composables/useGlobalEvents.ts] |
| 291 | +import type { GlobalEvents } from '~/types/events' |
| 292 | + |
| 293 | +export const useGlobalEvents = () => useEventDispatcher<GlobalEvents>('global') |
| 294 | +``` |
| 295 | + |
| 296 | +### Feature-Specific Channels |
| 297 | + |
| 298 | +```ts |
| 299 | +export const useUIEvents = () => useEventDispatcher<UIEvents>('ui') |
| 300 | +export const useGameEvents = () => useEventDispatcher<GameEvents>('game') |
| 301 | +export const usePlayerEvents = () => useEventDispatcher<PlayerEvents>('player') |
| 302 | +``` |
| 303 | + |
| 304 | +### Cross-Component Communication |
| 305 | + |
| 306 | +```vue [components/LoginForm.vue] |
| 307 | +<script setup> |
| 308 | +const events = useEventDispatcher<AppEvents>('app') |
| 309 | +
|
| 310 | +const handleSubmit = async (credentials) => { |
| 311 | + const user = await login(credentials) |
| 312 | + events.emit('user.login', { username: user.name, id: user.id }) |
| 313 | +} |
| 314 | +</script> |
| 315 | +``` |
| 316 | + |
| 317 | +```vue [components/UserForm.vue] |
| 318 | +<script setup> |
| 319 | +const events = useEventDispatcher<AppEvents>('app') |
| 320 | +const isLoggedIn = ref(false) |
| 321 | +
|
| 322 | +events.on('user.login', (data) => { |
| 323 | + isLoggedIn.value = true |
| 324 | + console.log('Welcome', data.username) |
| 325 | +}) |
| 326 | +
|
| 327 | +events.on('user.logout', () => { |
| 328 | + isLoggedIn.value = false |
| 329 | +}) |
| 330 | +</script> |
| 331 | +``` |
| 332 | + |
| 333 | +## Contribution |
| 334 | + |
| 335 | +::accordion{type="single"} |
| 336 | + :::accordion-item |
| 337 | + --- |
| 338 | + description: Commands for Local Dev |
| 339 | + label: Local Development |
| 340 | + --- |
| 341 | + ```bash |
| 342 | + # Install dependencies |
| 343 | + bun i |
| 344 | + |
| 345 | + # Generate type stubs |
| 346 | + bun run dev:prepare |
| 347 | + |
| 348 | + # Develop with the playground |
| 349 | + bun run dev |
| 350 | + |
| 351 | + # Build the playground |
| 352 | + bun run dev:build |
| 353 | + |
| 354 | + # Run ESLint |
| 355 | + bun run lint |
| 356 | + |
| 357 | + # Run Vitest |
| 358 | + bun run test |
| 359 | + bun run test:watch |
| 360 | + |
| 361 | + # Release new version |
| 362 | + bun run release |
| 363 | + ``` |
| 364 | + ::: |
| 365 | +:: |
0 commit comments