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
andcount
) 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 (thelabel
andcount
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 }
asevent.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.
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, setbubbles
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, setcomposed
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.
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.