Skip to main content
Version: 0.2.x

Working with events

SALVO-TS components can define custom events which can be listened on by other components, allowing cross-component communication.

info

SALVO-TS does not include a global event bus (a generic object through which all custom event are routed) - this is intentional, as event buses complicate troubleshooting when working with dynamically loaded components and are hard to make type-safe.

Please avoid inventing an event bus in your theme. This also applies to using document.dispatchEvent() as a makeshift global event bus.

Events associated with global functionality/entities can be structured explicitly as services in your theme, similar to how cart events are handled.

Native (DOM) events

If your component registers event listeners, it's crucial that they are cleaned up when the component is removed - otherwise, a component which is removed or replaced may still respond to events from elsewhere on the page.

You can use the built-in this.$listen helper to add an event listener, which will be automatically removed again when the component is destroyed.

ts
$listen(el: ElementOrWindow, eventName: TEvent, callback: (e: TEventPayload) => void): void
ts
$listen(el: ElementOrWindow, eventName: TEvent, callback: (e: TEventPayload) => void): void
src/components/ResizeAlerter.ts
ts
import { Component, Prop, Ref } from '@eastsideco/salvo-ts';
import { ThemeComponent } from '@/theme/ThemeComponent';
 
@Component
export class ResizeAlerter extends ThemeComponent {
@Ref sayHelloButton!: HTMLButtonElement;
 
init() {
// Listen to an event on a ref
this.$listen(this.sayHelloButton, 'click', () => {
alert('Hello!');
});
// Listen to an event on window
this.$listen(window, 'resize', () => {
this.$log.info('This window was resized!');
});
}
}
src/components/ResizeAlerter.ts
ts
import { Component, Prop, Ref } from '@eastsideco/salvo-ts';
import { ThemeComponent } from '@/theme/ThemeComponent';
 
@Component
export class ResizeAlerter extends ThemeComponent {
@Ref sayHelloButton!: HTMLButtonElement;
 
init() {
// Listen to an event on a ref
this.$listen(this.sayHelloButton, 'click', () => {
alert('Hello!');
});
// Listen to an event on window
this.$listen(window, 'resize', () => {
this.$log.info('This window was resized!');
});
}
}

Custom events

Components and objects like the Cart can use custom events to notify each other of changes, just like native elements do. In order to achieve this in a convenient and type-safe way, an interface named SimpleEvent is provided.

The SimpleEvent class provides subscribe and dispatch functions for listening and firing event events respectively:

Examples
ts
import { SimpleEvent } from '@eastsideco/salvo-ts';
 
// Event emitter
const onNameChange = new SimpleEvent<string>();
onNameChange.dispatch('there');
onNameChange.dispatch('World');
onNameChange.dispatch('John Doe');
 
// Event consumer
onNameChange.subscribe((name) => {
console.log(`Hello ${name}!`);
});
Examples
ts
import { SimpleEvent } from '@eastsideco/salvo-ts';
 
// Event emitter
const onNameChange = new SimpleEvent<string>();
onNameChange.dispatch('there');
onNameChange.dispatch('World');
onNameChange.dispatch('John Doe');
 
// Event consumer
onNameChange.subscribe((name) => {
console.log(`Hello ${name}!`);
});

You can pass anything as the payload type - for more complex events, defining your own payload type can help clarify what the event means (see this example.) A SimpleEvent can have multiple subscriptions - when it is dispatched, all of the subscribers will be called.

Custom events in components

Events are most often defined as properties on components. You should use a backing field and expose the value of .asEvent() publically - this exposes a version of the event without the dispatch methods, and ensures only the owner class can dispatch the event.

src/components/MyComponent.ts
ts
import { Component, Prop, Ref } from '@eastsideco/salvo-ts';
import { ThemeComponent } from '@/theme/ThemeComponent';
import { SimpleEvent } from '@eastsideco/salvo-ts';
 
@Component
export class MyComponent extends ThemeComponent {
private name: string = 'Guest';
 
// Private backing field and public event property
private _onNameChange = new SimpleEvent<string>();
public get onNameChange() {
return this._onNameChange.asEvent();
}
 
public changeName(name: string) {
this.name = name;
// Fire the event
this._onNameChange.dispatch(name);
}
}
src/components/MyComponent.ts
ts
import { Component, Prop, Ref } from '@eastsideco/salvo-ts';
import { ThemeComponent } from '@/theme/ThemeComponent';
import { SimpleEvent } from '@eastsideco/salvo-ts';
 
@Component
export class MyComponent extends ThemeComponent {
private name: string = 'Guest';
 
// Private backing field and public event property
private _onNameChange = new SimpleEvent<string>();
public get onNameChange() {
return this._onNameChange.asEvent();
}
 
public changeName(name: string) {
this.name = name;
// Fire the event
this._onNameChange.dispatch(name);
}
}
Usage
ts
const myComponent = document.querySelector('my-component') as MyComponent|null;
if (myComponent) {
myComponent.onNameChange.subscribe((name) => {
console.log(`Hello ${name}`);
});
}
Usage
ts
const myComponent = document.querySelector('my-component') as MyComponent|null;
if (myComponent) {
myComponent.onNameChange.subscribe((name) => {
console.log(`Hello ${name}`);
});
}

Just as with native DOM events, it's important to clean up any component events that you subscribe to inside components. You can use the $subscribe helper for this, which works just like the $listen helper but for custom events:

ts
$subscribe(evt: ISimpleEvent<T>, listener: (payload: T) => void): void
ts
$subscribe(evt: ISimpleEvent<T>, listener: (payload: T) => void): void
src/components/OtherComponent.ts
ts
import { Component, Prop, Ref } from '@eastsideco/salvo-ts';
import { ThemeComponent } from '@/theme/ThemeComponent';
import { MyComponent } from './MyComponent';
 
@Component
export class OtherComponent extends ThemeComponent {
@Ref myComponent!: MyComponent;
 
init() {
this.$subscribe(this.myComponent.onNameChange, (name: string) => {
this.$log.info(`Hello ${name}`);
});
}
// No need to do anything else, $subscribe will clean up automatically
}
src/components/OtherComponent.ts
ts
import { Component, Prop, Ref } from '@eastsideco/salvo-ts';
import { ThemeComponent } from '@/theme/ThemeComponent';
import { MyComponent } from './MyComponent';
 
@Component
export class OtherComponent extends ThemeComponent {
@Ref myComponent!: MyComponent;
 
init() {
this.$subscribe(this.myComponent.onNameChange, (name: string) => {
this.$log.info(`Hello ${name}`);
});
}
// No need to do anything else, $subscribe will clean up automatically
}
Why not use the native CustomEvent?

The browser's native CustomEvent is good, but it's got a few issues that make it difficult in real projects:

  • Passing payloads is cumbersome. You have to const evt = new CustomEvent(), evt.detail = payload, and you have to call evt.detail to get the payload back at the other end.
  • The detail property on CustomEvent isn't typed, so you have to assert the type at each end.

The SimpleEvent class has a more convenient interface and leads to clearer, safer code.