Skip to main content

Event handling

The core aspects of event handling are:

  • Component Communication. You need to enable a component (such as <my-button>) to notify or interact with parent components or other parts of the application. Here, we achieve this by emitting a custom event from <my-button> when it’s clicked.

  • Custom Events. You can create your own custom events (such as button-clicked in the example below), which communicate specific actions or states. You can use the $emit helper to create and dispatch a custom event.

  • Event Data Passing. When emitting custom events, you can send additional data along with the event. In the example below, event.detail passes contextual information ( label and count) to any listener that handles the event.

These aspects are all implemented in the example below:

import {
GenesisElement,
customElement,
html,
observable,
} from "@genesislcap/web-core";

@customElement({
name: "my-button",
template: html<MyButton>`
<button @click="${(x, c) => x.handleClick(c.event)}">
${(x) => `${x.label}: ${x.count}`}
</button>
`,
styles,
})
export class MyButton extends GenesisElement {
@observable label = "Clicks"; // Label text for the button
@observable count = 0; // Tracks the number of clicks

handleClick(event: Event) {
this.count += 1; // Increment the click count

console.log("Event Target:", event.target); // Log the event target element
console.log("Component Label:", this.label); // Log the current label

// Emit a custom "button-clicked" event with the current label and count
this.$emit("button-clicked", {
label: this.label,
count: this.count,
});
}
}

Component communication

The example below creates a new component that listens to <my-button>.

import { GenesisElement, customElement, html } from "@genesislcap/web-core";
import MyButton from "./my-button"; // Import my-button component

MyButton;

@customElement({
name: "parent-component",
template: html<ParentComponent>`
<button
@button-clicked="${(x, c) =>
x.handleButtonClicked(
c.event
)}" // custom button-clicked event from my-button
></button>
`,
})
export class ParentComponent extends GenesisElement {
handleButtonClicked(event: CustomEvent) {
console.log("Custom event received in ParentComponent:", event.detail);
// Output: { label: "Clicks", count: X }
}
}

So, <my-button> is able to communicate with its parent component (or other parts of the application) by notifying them whenever it’s clicked. The $emit helper method in the configuration of <my-button> emits a button-clicked event that any parent component or listener can handle.

Let's focus on the button-clicked custom event and the $emit method.

  • The button-clicked event is unique to <my-button>. It indicates that the button has been clicked, providing a specific trigger for event listeners.

  • The $emit method simplifies the creation and dispatch of the button-clicked event with {bubbles: true} and {composed: true}. This makes it available to parent components and global listeners.

  • The button-clicked event is able to pass data (the label and count properties) to provide context on the button’s state when the event was emitted. Specifically, the $emit method’s second parameter passes { label: this.label, count: this.count } as event.detail. Any event listener can access this information for further processing.

Event bubbling

Event bubbling enables an event to travel up the DOM from the component that triggered it, and through all components to the root (yes, the root is at the top of the structure). This enables all parent components to handle the event.

  • Components higher up in the DOM tree can listen for and react to events emitted by child components.
  • This reduces the need for coupling child and parent components tightly.
note

When a custom event is emitted with $emit, it bubbles up the DOM by default unless bubbles is explicitly set to false.

import {
GenesisElement,
customElement,
html,
observable,
} from "@genesislcap/web-core";

@customElement({
name: "my-button",
template: html<MyButton>`
<button @click="${(x) => x.emitCustomEvent()}">${(x) => x.label}</button>
`,
})
export class MyButton extends GenesisElement {
@observable label: string = "Click Me";
@observable count: number = 0;

emitCustomEvent() {
this.count += 1;
this.$emit("button-clicked", {
label: this.label,
count: this.count,
}); // By default, this event bubbles up the DOM.
}
}

Listening for events on a parent component:

@customElement({
name: "parent-container",
template: html<ParentContainer>`
<div @button-clicked="${(x, c) => x.handleButtonClick(c.event)}">
<my-button></my-button>
</div>
`,
})
export class ParentContainer extends GenesisElement {
handleButtonClick(event: CustomEvent) {
console.log("Button clicked event bubbled to parent:", event.detail);
// Logs: { label: "Click Me", count: <number> }
}
}

In the example above, we didn’t explicitly set bubbles and composed because they are set to true by default. However, if you want to customize this behaviour, you can pass an 'options object' as the third parameter to $emit.

Here's how to modify the emitCustomEvent method to include bubbles and composed settings:

emitCustomEvent() {
this.count += 1;

this.$emit(
"button-clicked",
{
label: this.label,
count: this.count,
},
{
bubbles: true, // Event will bubble up the DOM.
composed: true, // Event will cross the shadow DOM boundary.
}
);
}

In this modified event, this is how bubbling works:

  • When bubbles is true, the event propagates upward through the DOM. (If you want the event to be confined to the component emitting it and not propagate up the DOM tree, set bubbles to false.)
  • When bubbles is false, the event does not bubble.

This is how the `compose setting behaves:

  • When composed is true, the event crosses the shadow DOM boundary and can be listened to outside the shadow DOM. (If you want the event to stay within the shadow DOM of the component, set composed to false.)
  • When composed is false, the event is confined within the shadow DOM.

You don't need to make any changes to the parent component if you keep bubbles: true. It will still handle the event the same way.

tip

If you set bubbles: false or composed: false, make sure you either attach the event listener directly to the emitting component or you adjust the shadow DOM handling to handle this.