The Event System
Events are how Inglorious Web state changes happen. Understanding the event system is key to building apps.
Event Dispatch
Broadcast Events
Send an event to all entities:
api.notify("refresh") // All entities listen for 'refresh'
Targeted Events
Send an event to a specific entity:
api.notify("#counter:increment") // Only 'counter' entity listens
With Payload
Pass data with your event:
api.notify("#user:setName", "Alice")
api.notify("#todo:update", { title: "Buy milk", completed: true })
api.dispatch({
type: "#cart:addItem",
payload: { itemId: 123, quantity: 5 },
})
2
3
4
5
6
Event Targeting
Inglorious Web supports four levels of event targeting:
1. Global Broadcast
All entities can listen:
// Dispatch
api.notify("appInitialized")
// Any type can handle it
const myType = {
appInitialized(entity) {
console.log("App is ready!")
},
}
2
3
4
5
6
7
8
9
2. By Type
Target all entities of a specific type:
// Dispatch
api.notify("todo:toggle")
// All todo entities listen
const todo = {
toggle(entity) {
entity.completed = !entity.completed
},
}
2
3
4
5
6
7
8
9
3. By ID
Target a specific entity:
// Dispatch
api.notify("#counter:increment")
// Only the counter entity with that ID listens
const counter = {
increment(entity) {
entity.value++
},
}
2
3
4
5
6
7
8
9
4. Type#ID (Specific)
Most specific targeting:
// Dispatch
api.notify("listItem#item1:itemSelected", { highlighted: true })
// Only that entity with that handler listens
const listItem = {
itemSelected(entity, payload) {
entity.highlighted = payload.highlighted
},
}
2
3
4
5
6
7
8
9
Event Handlers
Handler Signature
const myType = {
someEvent(entity, payload, api) {
// entity: The entity being updated
// payload: Data passed with the event
// api: Store API (for triggering more events)
},
}
2
3
4
5
6
7
Optional Parameters
You can omit unused parameters:
const myType = {
// Just entity
doSomething(entity) {
entity.count++
},
// Entity and payload
updateValue(entity, newValue) {
entity.value = newValue
},
// All three
complexEvent(entity, payload, api) {
entity.data = payload
api.notify("dataUpdated")
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Event Patterns
Pattern 1: Toggle State
const toggle = {
toggle(entity) {
entity.isOpen = !entity.isOpen
},
render(entity, api) {
return html`
<button @click=${() => api.notify("#toggle:toggle")}>
${entity.isOpen ? "Close" : "Open"}
</button>
${entity.isOpen ? html`<p>Content is visible</p>` : ""}
`
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
Pattern 2: Form Input
const form = {
setField(entity, { field, value }) {
entity[field] = value
},
render(entity, api) {
return html`
<input
type="text"
value="${entity.name}"
@input=${(e) =>
api.notify("#form:setField", {
field: "name",
value: e.target.value,
})}
/>
`
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Pattern 3: Async Operations
const dataLoader = {
async load(entity, api) {
// Set loading BEFORE await
entity.isLoading = true
try {
const response = await fetch("/api/data")
const data = await response.json()
// After await, dispatch new event with result
api.notify("#dataLoader:loadSuccess", data)
} catch (error) {
// Dispatch error event
api.notify("#dataLoader:loadError", error.message)
}
},
// Handle success result
loadSuccess(entity, data) {
entity.isLoading = false
entity.data = data
entity.error = null
},
// Handle error result
loadError(entity, error) {
entity.isLoading = false
entity.error = error
entity.data = null
},
render(entity, api) {
if (entity.isLoading) return html`<p>Loading...</p>`
if (entity.error) return html`<p>Error: ${entity.error}</p>`
return html`<pre>${JSON.stringify(entity.data)}</pre>`
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Pattern 4: Cross-Entity Communication
Entities communicate through events:
const cart = {
addItem(entity, itemId, api) {
entity.items.push(itemId)
// Tell the store something happened
api.notify("itemAdded", { itemId })
},
render(entity, api) {
return html`
<button @click=${() => api.notify("#cart:addItem", "item-123")}>
Add to Cart
</button>
`
},
}
const notification = {
itemAdded(entity, payload) {
entity.message = `Added ${payload.itemId} to cart`
},
render(entity, api) {
return html`<p>${entity.message}</p>`
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Lifecycle Events
Create
Runs when an entity is first created:
const user = {
create(entity) {
entity.createdAt = new Date()
console.log(`User ${entity.id} created`)
},
render(entity, api) {
return html`<p>Created at: ${entity.createdAt}</p>`
},
}
2
3
4
5
6
7
8
9
10
Destroy
Runs when an entity is removed:
const modal = {
destroy(entity) {
console.log(`Modal ${entity.id} destroyed`)
// Cleanup if needed
},
}
2
3
4
5
6
Event Queue
By default, Inglorious Web batches events into a queue for predictable, atomic updates. This means multiple events in quick succession are processed together.
How It Works
// These events are queued together
api.notify("#item:select", 1)
api.notify("#item:highlight", 1)
api.notify("#list:updateCount")
// All handlers execute, then re-render once
2
3
4
5
6
Manual Update Control
For advanced use cases (like game loops or animations), you can control when updates happen:
import { createStore } from "@inglorious/store"
const store = createStore({
types: {
/* ... */
},
entities: {
/* ... */
},
updateMode: "manual", // Don't update automatically
})
// Now you call store.update() when you want
function gameLoop() {
api.notify("#player:move", { x: 10, y: 20 })
api.notify("#enemy:update")
// Update happens here, re-renders once
store.update()
requestAnimationFrame(gameLoop)
}
gameLoop()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Use cases for manual mode:
- Game loops running at 60 FPS
- Animation loops
- Batching multiple operations
- Performance-critical scenarios
Advanced: Custom Event Handling
With Full Event Object
const myType = {
someEvent(entity, payload, api, meta) {
// meta contains: { type, timestamp, source, ... }
console.log(`Event at ${meta.timestamp}`)
},
}
2
3
4
5
6
Conditional Handling
const form = {
fieldChange(entity, { field, value }) {
// Only update if valid
if (value.length > 0) {
entity[field] = value
}
},
}
2
3
4
5
6
7
8
Event Delegation
const list = {
itemEvent(entity, { itemId, action }, api) {
// Handle different actions for list items
switch (action) {
case "select":
entity.selectedId = itemId
break
case "delete":
delete entity.items[itemId]
break
}
},
}
2
3
4
5
6
7
8
9
10
11
12
13
Type Composition with Events
Type composition allows you to intercept and modify events:
// Base type
const page = {
navigate(entity, route) {
entity.currentRoute = route
},
}
// Guard behavior
const requireAuth = (type) => ({
navigate(entity, route, api) {
if (!isAuthenticated()) {
api.notify("navigate", "/login")
return // Don't call original handler
}
type.navigate(entity, route, api)
},
})
// Compose
const types = {
protectedPage: [page, requireAuth],
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Best Practices
✅ Do:
- Use targeted events (
#id:action) for specific entities - Pass payload as a single object for clarity
- Keep handlers pure (mutations only, no side effects)
- Use broadcast events for global concerns
- Queue related events together for atomic updates
❌ Don't:
- Create multiple event types for the same action
- Pass complex objects as payload without structure
- Make API calls in event handlers (use async handlers instead)
- Trigger events from render methods (except user interactions)
- Rely on event order for correctness
Next Steps
- Entity Render Methods — How to trigger events from templates
- Type Composition — Advanced event interception
- Performance — Event queuing and performance tips
Happy eventing! ⚡
Inglorious Web