Working with events
SALVO-TS components can define custom events which can be listened on by other components, allowing cross-component communication.
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
- With $listen
- Without $listen
src/components/ResizeAlerter.tstsimport {Component ,Prop ,Ref } from '@eastsideco/salvo-ts';import {ThemeComponent } from '@/theme/ThemeComponent';@Component export classResizeAlerter extendsThemeComponent {@Ref sayHelloButton !:HTMLButtonElement ;init () {// Listen to an event on a refthis.$listen (this.sayHelloButton , 'click', () => {alert ('Hello!');});// Listen to an event on windowthis.$listen (window , 'resize', () => {this.$log .info ('This window was resized!');});}}
src/components/ResizeAlerter.tstsimport {Component ,Prop ,Ref } from '@eastsideco/salvo-ts';import {ThemeComponent } from '@/theme/ThemeComponent';@Component export classResizeAlerter extendsThemeComponent {@Ref sayHelloButton !:HTMLButtonElement ;init () {// Listen to an event on a refthis.$listen (this.sayHelloButton , 'click', () => {alert ('Hello!');});// Listen to an event on windowthis.$listen (window , 'resize', () => {this.$log .info ('This window was resized!');});}}
src/components/ResizeAlerter.tstsimport {Component ,Prop ,Ref } from '@eastsideco/salvo-ts';import {ThemeComponent } from '@/theme/ThemeComponent';@Component export classResizeAlerter extendsThemeComponent {@Ref sayHelloButton !:HTMLButtonElement ;resizeListener : (() => void)|null = null;sayHelloListener : (() => void)|null = null;init () {// Listen to an event on a refif (!this.sayHelloListener ) {this.sayHelloListener = () => {alert ('Hello!');};}this.sayHelloButton .addEventListener ('click', this.sayHelloListener );// Listen to an event on windowif (!this.resizeListener ) {this.resizeListener = () => {this.$log .info ('The window was resized!');};}window .addEventListener ('resize', this.resizeListener );}destroy () {// Clean up listenersif (this.resizeListener ) {window .removeEventListener ('resize', this.resizeListener );}if (this.sayHelloListener ) {this.sayHelloButton .removeEventListener ('click', this.sayHelloListener );}}}
src/components/ResizeAlerter.tstsimport {Component ,Prop ,Ref } from '@eastsideco/salvo-ts';import {ThemeComponent } from '@/theme/ThemeComponent';@Component export classResizeAlerter extendsThemeComponent {@Ref sayHelloButton !:HTMLButtonElement ;resizeListener : (() => void)|null = null;sayHelloListener : (() => void)|null = null;init () {// Listen to an event on a refif (!this.sayHelloListener ) {this.sayHelloListener = () => {alert ('Hello!');};}this.sayHelloButton .addEventListener ('click', this.sayHelloListener );// Listen to an event on windowif (!this.resizeListener ) {this.resizeListener = () => {this.$log .info ('The window was resized!');};}window .addEventListener ('resize', this.resizeListener );}destroy () {// Clean up listenersif (this.resizeListener ) {window .removeEventListener ('resize', this.resizeListener );}if (this.sayHelloListener ) {this.sayHelloButton .removeEventListener ('click', this.sayHelloListener );}}}
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:
Examplestsimport {SimpleEvent } from '@eastsideco/salvo-ts';// Event emitterconstonNameChange = newSimpleEvent <string>();onNameChange .dispatch ('there');onNameChange .dispatch ('World');onNameChange .dispatch ('John Doe');// Event consumeronNameChange .subscribe ((name ) => {console .log (`Hello ${name }!`);});
Examplestsimport {SimpleEvent } from '@eastsideco/salvo-ts';// Event emitterconstonNameChange = newSimpleEvent <string>();onNameChange .dispatch ('there');onNameChange .dispatch ('World');onNameChange .dispatch ('John Doe');// Event consumeronNameChange .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.tstsimport {Component ,Prop ,Ref } from '@eastsideco/salvo-ts';import {ThemeComponent } from '@/theme/ThemeComponent';import {SimpleEvent } from '@eastsideco/salvo-ts';@Component export classMyComponent extendsThemeComponent {privatename : string = 'Guest';// Private backing field and public event propertyprivate_onNameChange = newSimpleEvent <string>();public getonNameChange () {return this._onNameChange .asEvent ();}publicchangeName (name : string) {this.name =name ;// Fire the eventthis._onNameChange .dispatch (name );}}
src/components/MyComponent.tstsimport {Component ,Prop ,Ref } from '@eastsideco/salvo-ts';import {ThemeComponent } from '@/theme/ThemeComponent';import {SimpleEvent } from '@eastsideco/salvo-ts';@Component export classMyComponent extendsThemeComponent {privatename : string = 'Guest';// Private backing field and public event propertyprivate_onNameChange = newSimpleEvent <string>();public getonNameChange () {return this._onNameChange .asEvent ();}publicchangeName (name : string) {this.name =name ;// Fire the eventthis._onNameChange .dispatch (name );}}
UsagetsconstmyComponent =document .querySelector ('my-component') asMyComponent |null;if (myComponent ) {myComponent .onNameChange .subscribe ((name ) => {console .log (`Hello ${name }`);});}
UsagetsconstmyComponent =document .querySelector ('my-component') asMyComponent |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.tstsimport {Component ,Prop ,Ref } from '@eastsideco/salvo-ts';import {ThemeComponent } from '@/theme/ThemeComponent';import {MyComponent } from './MyComponent';@Component export classOtherComponent extendsThemeComponent {@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.tstsimport {Component ,Prop ,Ref } from '@eastsideco/salvo-ts';import {ThemeComponent } from '@/theme/ThemeComponent';import {MyComponent } from './MyComponent';@Component export classOtherComponent extendsThemeComponent {@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}
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 callevt.detailto get the payload back at the other end. - The
detailproperty 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.