Handle Events in Lightning Web Components
Learning Objectives
After completing this unit, you’ll be able to:
- Create an app that includes multiple components.
- Describe the file structure of a complex component.
- Handle events.
Follow an Event’s Journey
You’ve built a component and pushed it to a scratch org. Let’s start adding some interactivity with event handling. We follow an event’s journey through several components for sophisticated event handling in an application. This application is a product selector for a bicycle shop. Users click a bike name and image to see more detail.
This app has four components working together.
- tile: displays an individual item.
- list: arranges the tiles.
- detail: displays item details when a tile is clicked (similar to the bikeCard you just created).
- selector: contains the whole set of components. A container component isn’t required, but we’re using one here to help with event handling.
For now, the application uses a data file to load static data for testing. In the next unit, you learn how to pull dynamic data from an org.
Component Composition
Let’s add some files to our project that we can deploy to an org.
- Download the files for this app here: Bike Selector App for Trailhead.
- Unzip the files into the lwc folder of the bikeCard project.
Component Relationships
In this app, multiple components work together; some components are nested inside other components. Just as you nest HTML elements inside each other, Lightning web components—which are custom HTML elements—can be nested inside other Lightning web components.
In our file system, the components’ folders don’t really give insight into the relationships between them.
Let’s look at how the components nested at the UI level in a diagram.
The selector component lays out the page and renders the list and detail component. The list component renders several tile components, one for each bike in the data.
<template> <div class="wrapper"> <header class="header">Available Bikes</header> <section class="content"> <div class="columns"> <main class="main" > <c-list onproductselected={handleProductSelected}></c-list> </main> <aside class="sidebar-second"> <c-detail product-id={selectedProductId}></c-detail> </aside> </div> </section> </div> </template>
If you look in detail.html, you see conditional rendering. If nothing has been chosen from the list, then what’s displayed is a message asking the user to choose something. If something is chosen, it displays the bike information.
<template> <template if:true={product}> <div class="container"> <div>{product.fields.Name.value}</div> <div class="price">{product.fields.MSRP__c.displayValue}</div> <div class="description">{product.fields.Description__c.value}</div> <img class="product-img" src={product.fields.Picture_URL__c.value}></img> <p> <lightning-badge label={product.fields.Material__c.value}></lightning-badge> <lightning-badge label={product.fields.Level__c.value}></lightning-badge> </p> <p> <lightning-badge label={product.fields.Category__c.value}></lightning-badge> </p> </div> </template> <template if:false={product}> <div>Select a bike</div> </template> </template>
This nesting is achieved in the HTML for each parent component. For example, the list component has the following HTML, including the tile component as c-tile
.
<template> <div class="container"> <template for:each={bikes} for:item="bike"> <c-tile key={bike.fields.Id.value} product={bike} ontileclick={handleTileClick}> </c-tile> </template> </div> </template>
Notice how each iteration of the bike item spawns a new tile component. Simply including the c-tile
component tag makes each tile component its child. The div class definition “container” is used for styling, so you can control the arrangement of tiles. If you look at list.css, you see that it wraps the content.
.container { display: flex; flex-direction: row; flex-wrap: wrap; }
The parent/child relationship is important, not only for the design of the app, but also for event handling.
Let’s dig a little deeper into the event handling.
Events Up, Properties Down
In a complex component (one that contains several parent and child components), the components can communicate up and down.
- The c-todo-item child component dispatches an event to the parent c-todo-app component. For example, the child can pass an event object to the parent when a user clicks a button so the parent can handle the event and change the current page.
- The c-todo-app parent component passes a property or invokes a method in the child component. For example, the parent can set a text value in a child component, or invoke a method in the child component.
Let’s look at how this communication works.
Passing Information Up
Information can be passed up using events and event listeners.
The child component dispatches the event and the parent component listens for it. Dispatching the event includes creating an event object the child can pass to the parent component. The parent has a handler to respond to the event.
For example, a child component like this one contains a nextHandler()
method that creates a simple event object using CustomEvent()
and dispatches the value next when the user clicks a Next button.
// todoItem.js import { LightningElement } from 'lwc'; ... nextHandler() { this.dispatchEvent(new CustomEvent('next')); } }
The parent component listens for the event.
<!– todoApp.html --> <template> <c-todo-item onnext={nextHandler}></c-child> </template>
And passes the event object to an event handler.
// todoApp.js import { LightningElement } from 'lwc'; export default class TodoApp extends LightningElement { ... nextHandler(){ this.page = this.page + 1; } }
Passing Information Down
Information can be passed down using public properties and public methods.
You can make a component property public by prefacing it with the @api
decorator. Then, set the public property by an external component.
For example, if the c-todo-item child component has the following:
// todoItem.js import { LightningElement, api } from 'lwc'; export default class TodoItem extends LightningElement { @api itemName; }
Set the value from the parent with the following:
<!– todoApp.html --> <template> <c-todo-item item-name="Milk"></c-todo-item> </template>
Public properties are great solutions for passing down primitive values, simple objects, and arrays.
Also, you can use getters & setters to execute some logic when properties are get or set. And remember, annotate them with the @api
decorator to make them public for other components.
Similarly, you can create public methods that are callable from a parent component. Create a public method in the child component by defining it with the @api
decorator, then call it from the parent component.
Let’s say that we have a child component like this one.
// videoPlayer.js import { LightningElement, api } from 'lwc'; export default class VideoPlayer extends LightningElement { @api play() { // Play music! } }
When the c-video-player component is included in a parent component, we can invoke the method from the parent component like this:
// methodCaller.js import { LightningElement } from 'lwc'; export default class MethodCaller extends LightningElement { handlePlay() { this.template.querySelector('c-video-player').play(); } }
We defined a method handlePlay()
that fires the event. Then we use the querySelector()
DOM method to search for a DOM element called c-video-player and invoke its public method.
Handling Events in HTML
So our selector app needs to handle one type of event—the user clicking a tile. When this happens, the detail component should re-render with the information from the related tile. You can handle events in HTML (add an event listener in the template) or JavaScript (write an event listener function). We recommend using the HTML approach, as follows.
Each tile component listens for the user click because the tile component’s HTML (tile.html) contains an onclick
event listener.
<template> <div class="container"> <a onclick={tileClick}> <div class="title">{product.fields.Name.value}</div> <img class="product-img" src={product.fields.Picture_URL__c.value}></img> </a> </div> </template>
When a user clicks one of the tile instances in the UI, the onclick
listener calls the handler function tileClick
in the tile.js JavaScript file.
import { LightningElement, api } from 'lwc'; export default class Tile extends LightningElement { @api product; tileClick() { const event = new CustomEvent('tileclick', { // detail contains only primitives detail: this.product.fields.Id.value }); // Fire the event from c-tile this.dispatchEvent(event); } }
Selector App Event Pattern
In our product selector app, we use a complex component (one that contains several parent and child components), We recommend that you propagate the event up through the component hierarchy, so parent components can respond to child events. If you have other child components (not the one firing the event), you can pass a property down to those children in response to the event.
The pattern looks like this:
To do this, we need to chain event listeners and handlers up the hierarchy to the ebikes component. Then pass a property down to the detail component.
In our example files, you see the following.
- tile.html has the
onclick
event listener that calls thetileClick
handler. - tile.js has the
tileClick
method that creates a new CustomEvent object containing a detail value (this.product.fields.Id.value
). - List.html has the
ontileClick
listener that calls thehandleTileClick
handler. - List.js has the
handleTileClick
method that creates another event object (evt
) also containing a detail value. And it dispatches the event in JavaScript:// Fire the event from c-list this.dispatchEvent(event);
- selector.html has the
onproductselected
event listener that calls thehandleProductSelected
handler. - selector.js has the
handleProductSelected
method setselectedProductId
to theevt.detail
value. - detail.html has a conditional directive (remember those from Unit 2?) waiting for a product value:
<template if:true={product}>
- detail.js brings the pieces together. It creates a private
variable _productId
to track the state of theproductId
value. Then it uses a get/set pattern to get the value and set it to a private variable product which lets detail.html load the conditional content.
Getters and setters are a common JavaScript construction. They allow you to add logic and conditions to property assignments.
import { LightningElement, api } from 'lwc'; import { bikes } from 'c/data'; export default class Detail extends LightningElement { product; // Private var to track @api productId _productId = undefined; // Use set and get to process the value every time it's // requested while switching between products set productId(value) { this._productId = value; this.product = bikes.find(bike => bike.fields.Id.value === value); } // getter for productId @api get productId(){ return this._productId; } }
Each time you click a tile, this process repeats itself.
Deploy Your Files to Your Org
Let’s deploy these files to your Dev Hub enabled org to see how it works. Using the same steps you performed in the last unit, deploy your new files, open the org, and create a page in Lightning App Builder with this app.
- To deploy the project files, right-click on the default folder and select SFDX: Deploy Source to Org from the Command Palette in VS Code.
- To open your org, use SFDX: Open Default Org from the Command Palette in VS Code.
- In Setup, enter Lightning App Builder in the Quick Find box and then select Lightning App Builder.
- Click New.
- Create a one region page using the selector component.
You’ve got a fully interactive page composed of several components working together. Next, we experiment with styling and getting live data from an org.