Skip to content

Commit 4ed4984

Browse files
Type-32nuxtstudio
andcommitted
feat(dispatcher): added dispatcher to projects
Co-authored-by: Nuxt Studio <noreply@nuxt.studio>
1 parent 70f008f commit 4ed4984

File tree

1 file changed

+365
-0
lines changed

1 file changed

+365
-0
lines changed

content/projects/dispatcher.md

Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
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

Comments
 (0)