Events
Elements in a spec can define event handlers via the on property. When a rendered component calls its emit function, the library looks up the corresponding action binding and dispatches it to a registered handler.
How Events Work
The event flow has three parts:
Element definition -- the on property maps event names to action bindings
Component -- calls emit('eventName') when the user interacts
Handler -- a registered function that executes the action
Component calls emit('submit')
--> Library looks up on.submit
--> Finds { action: 'handleSubmit', params: { formId: 'login' } }
--> Calls handlers['handleSubmit']({ formId: 'login' })
Defining Event Handlers in a Spec
The on property on a UIElement maps event names to action bindings:
{
type : ' Button ' ,
props : { label : ' Submit ' } ,
on : {
click : { action : ' handleSubmit ' , params : { formId : ' login ' } } ,
} ,
}
Each binding has:
Property Type Description actionstringThe key used to look up the handler function paramsRecord<string, unknown>Parameters passed to the handler
Multiple Handlers per Event
An event can trigger multiple actions by using an array:
on : {
click : [
{ action : ' trackAnalytics ' , params : { event : ' button_click ' } } ,
{ action : ' handleSubmit ' , params : { formId : ' login ' } } ,
] ,
}
Both handlers are called in order when the component emits click.
The Emit Function
Every rendered component receives an emit input -- a function with the signature (event: string) => void. Call it from your component to dispatch an event:
import { Component , ChangeDetectionStrategy , input } from ' @angular/core ' ;
@ Component ({
selector : ' app-button ' ,
standalone : true ,
changeDetection : ChangeDetectionStrategy . OnPush ,
template : `
<button (click)="onClick()">{{ label() }}</button>
` ,
})
export class ButtonComponent {
readonly label = input < string > ( '' ) ;
readonly emit = input < ( event : string ) => void > (() => {}) ;
readonly childKeys = input < string [] > ([]) ;
readonly spec = input < unknown > ( null ) ;
onClick () {
this . emit ()( ' click ' ) ;
}
}
Because emit is declared with input(), it is a Signal. Call this.emit() to get the function, then invoke it with the event name: this.emit()('click').
Registering Handlers
Handlers are plain functions registered either globally via provideRender() or per-instance on <render-spec>:
Per-instance Global (provideRender)
@ Component ({
selector : ' app-root ' ,
standalone : true ,
imports : [ RenderSpecComponent ] ,
template : `
<render-spec
[spec]="spec"
[registry]="registry"
[store]="store"
[handlers]="handlers"
/>
` ,
})
export class AppComponent {
store = signalStateStore ({ submitted : false }) ;
handlers = {
handleSubmit : ( params : Record < string , unknown > ) => {
console . log ( ' Form submitted: ' , params [ ' formId ' ]) ;
this . store . set ( ' /submitted ' , true ) ;
} ,
trackAnalytics : ( params : Record < string , unknown > ) => {
console . log ( ' Analytics event: ' , params [ ' event ' ]) ;
} ,
} ;
}
Handler Signature
Each handler receives a params object and can return a value or a Promise:
type Handler = ( params : Record < string , unknown > ) => unknown | Promise < unknown >;
Injection Context
Handlers execute inside Angular's runInInjectionContext. This means you can call inject() to access services:
const handlers = {
saveForm : async ( params : Record < string , unknown > ) => {
const http = inject ( HttpClient ) ;
const snapshot = store . getSnapshot () ;
await firstValueFrom ( http . post ( ' /api/forms ' , snapshot )) ;
store . set ( ' /saved ' , true ) ;
} ,
} ;
This works for handlers passed via [handlers] on <render-spec>, provideRender(), or ChatComponent.
Resolution Priority
Handlers resolve with the same priority as other inputs:
handlers input on <render-spec> (highest priority)
handlers in provideRender() config (fallback)
Action Dispatch Pattern
A common pattern is to use handlers to update the state store in response to user interactions. This creates a unidirectional data flow:
User clicks button
--> emit('click')
--> handler updates store
--> Signals propagate
--> UI re-renders
Here is a complete example:
const spec : Spec = {
root : ' app ' ,
elements : {
app : {
type : ' Container ' ,
props : {} ,
children : [ ' counter ' , ' increment ' ] ,
} ,
counter : {
type : ' Text ' ,
props : { label : { $state : ' /count ' } } ,
} ,
increment : {
type : ' Button ' ,
props : { label : ' Increment ' } ,
on : {
click : { action : ' increment ' , params : {} } ,
} ,
} ,
} ,
state : { count : 0 } ,
} ;
const store = signalStateStore ({ count : 0 }) ;
const handlers = {
increment : () => {
const current = store . get ( ' /count ' ) as number ;
store . set ( ' /count ' , current + 1 ) ;
} ,
} ;
Async Handlers
Handlers can be asynchronous. The library does not await the return value, but you can use async functions for server calls or other asynchronous operations:
const handlers = {
saveForm : async ( params : Record < string , unknown > ) => {
const snapshot = store . getSnapshot () ;
await fetch ( ' /api/forms ' , {
method : ' POST ' ,
body : JSON . stringify ( snapshot ) ,
}) ;
store . set ( ' /saved ' , true ) ;
} ,
} ;
Next Steps