Salesforce Developers Blog

A Deep Dive into the LightningModal Base Component

Avatar for Mohith ShrivastavaMohith Shrivastava
With the Winter ’23 release, we now ship LightningModal, a base Lightning component that makes it simpler to incorporate modals into your components.
A Deep Dive into the LightningModal Base Component
December 27, 2022
Listen to this article
0:00 / 0:00

A modal is a type of user interface that displays content in a layer above the app. The key characteristic of a modal is that it disables the main content until the user explicitly interacts with the modal. Modals can be effective tools in UX design when used appropriately. In the Salesforce Lightning experience, modals are used for use cases like creating or editing a record, various types of messaging, and wizards. Developers often have requirements to use modals in their custom applications as well.

The Salesforce Lightning Design System (SLDS) provides a blueprint for developers needing to implement modals for their applications. However, maintaining a lot of boilerplate code has been a major pain point for developers building custom Lightning Web Components (LWCs) and wanting to utilize modals. With the Winter ’23 release, we now ship LightningModal (see docs), a base Lightning component that makes it simpler to incorporate modals into your components. LightningModal is based on the SLDS blueprint and follows guidelines for accessibility.

Using LightningModal in an application is covered extensively in the docs, and our LWC sample app, lwc-recipes, has a well-written example. In this blog post, we’ll focus specifically on modal events, which are tricky to understand. Specifically, with an example, we’ll dive deep into how to pass data from your Lightning web component to the modal component and vice versa. The aim is to help you understand how to work with the LightningModal component.

An example use case

To understand how to pass data from your Lightning Web Component to the modal component and vice versa, we’ll explore code snippets from an example LWC app, “Mascot Explorer.” I created this app to help us learn how to use modal events.

The Mascot Picker LWC component within the Mascot Explorer app lets you pick your favorite Salesforce Mascot. Codey is what I would pick, but I will leave it up to you to choose one!

You can grab the full source code for the app or watch the video below to see how the component works.

Component Composition

The Mascot Picker app consists of the following components.

  • A LWC component named “Mascot Picker” launches the modal component.
  • A LWC component named “Mascot Picker Modal” launched as a modal from the “Mascot Picker” LWC component. This component uses the LightningModal base component.
  • A visual picker LWC component named “Visual Picker” is contained within the “Mascot Picker Modal” LWC component (a child of the Mascot Picker Modal LWC component). The Visual Picker component has an input form element of the type radio to allow the selection of your favorite Mascot.

The diagram below can help you visualize how various components are composed.

Component composition of the Mascot Picker app

Creating the Mascot Picker Modal LWC component

mascotPickerModal is an LWC component launched as a modal from the Mascot Picker LWC component. It is built using the LightningModal base component. The mascotPickerModal LWC component is unique in how it is extended. Typically, when creating your component, the JavaScript module of the component extends the ES6 class LightningElement. When creating a modal component, the JavaScript module of the component extends the ES6 class LightningModal.

The code for the mascotPickerModal component is as below.

<template>
    <lightning-modal-header
        label="Mascot Picker Modal"
    ></lightning-modal-header>
    <lightning-modal-body>
        <c-visual-picker items={mascots} onselect={handleSelect}></c-visual-picker>
    </lightning-modal-body>
    <lightning-modal-footer>
        <lightning-button label="Confirm" onclick={handleConfirm}></lightning-button>
    </lightning-modal-footer>
</template>

And here is the JavaScript module:

import { api } from "lwc";
import LightningModal from "lightning/modal";

export default class MascotPickerModal extends LightningModal {
    @api mascots;
    selectedMascot;
    
    // handle the event from the visual picker component
    handleSelect(event) {
        this.selectedMascot = event.detail;
    }
    
    handleConfirm() {
        this.dispatchEvent(
            new CustomEvent("select", {
                detail: {
                    id: this.selectedMascot
                }
            })
        );
        // Close the Modal
        this.close(this.selectedMascot);
    }
}

Code highlights

  • The mascotPickerModal component extends LightningModal as shown below.
export default class MascotPickerModal extends LightningModal
  • You can define the modal header, body, and footer using out-of-box component markups as shown in the code snippets below.
<lightning-modal-header
        label="Mascot Picker Modal"
    ></lightning-modal-header>
    <lightning-modal-body>
        ...
    </lightning-modal-body>
    <lightning-modal-footer>
        <lightning-button label="Confirm" onclick={handleConfirm}></lightning-button>
    </lightning-modal-footer>
  • The body of the modal in our example uses a custom LWC component, visualPicker. This component fires a select event to indicate mascot selection. The selection event is handled by the mascotPickerModal component.
<lightning-modal-body>
        <c-visual-picker items={mascots} onselect={handleSelect}></c-visual-picker>
    </lightning-modal-body>
handleSelect(event) {
        this.selectedMascot = event.detail;
    }
  • When the Confirm button in the modal footer is clicked, the component dispatches a Select event. This is shown in the code snippets below. The select event is handled in the Mascot Picker LWC component covered in the next section.
<lightning-modal-footer>
        <lightning-button label="Confirm" onclick={handleConfirm}></lightning-button>
    </lightning-modal-footer>
handleConfirm() {
        this.dispatchEvent(
            new CustomEvent("select", {
                detail: {
                    selectedMascot: this.selectedMascot
                }
            })
        );
        // Close the Modal and pass data to the component that launched the modal
        this.close(this.selectedMascot);
    }

Launching the Mascot Picker Modal LWC component as a modal from the Mascot Picker LWC component

The Mascot Picker Modal component we covered in the previous section is launched as a modal from the Mascot Picker LWC component. In our example, the mascotPicker component has a lightning-button to open the modal component.

The code snippet to launch the mascotPickerModal LWC component as modal is shown below.

....
<lightning-button
           onclick={handleBtnClick}
           variant="brand"
           label="Pick your favorite Salesforce mascot"
           title="Pick your favorite Salesforce mascot"
          
 >
            ......

The JavaScript snippet to launch the mascotPickerModal LWC component as modal from the Mascot Picker LWC component is shown below.

import { LightningElement } from "lwc";
import MascotPickerModal from "c/mascotPickerModal";

export default class MascotPicker extends LightningElement {
    selectedMascot;

    mascots = [
        {
            index: 0,
            name: "Astro",
            src: "https://www.salesforce.com/blog/wp-content/uploads/sites/2/2021/12/2021-12-360Blog-2D-IndividualIllustrations-Astro.png"
        },
        ....
        
    ];

    handleBtnClick() {
        MascotPickerModal.open({
            // pass data for the @api properties declared in the Modal component
            mascots: this.mascots,
            // Bind the events dispatched from the Modal component
            onselect: (e) => {
                e.stopPropagation();
                // Call function to handle the selection event
                this.handleSelectEvent(e.detail);
            }
        }).then((result) => {
            // Promise handler for the Modal opening
            // Result has values passed via the close event from the Modal component
            console.log(result);
        });
    }
    // function to handle the selection event
    handleSelectEvent(detail) {
        this.selectedMascot = detail.selectedMascot;
    }
}

Code highlights

  • The mascotPickerModal LWC component is imported to the Mascot Picker LWC component using a simple import statement
import MascotPickerModal from "c/mascotPickerModal";
  • To open the mascotPickerModal LWC component as modal, use the .open() method
MascotPickerModal.open().then((result) => {
            
});

NOTE – The LightningModal also supports the headless variant. You can leave out the lighnting-modal-header and lighnting-modal-footer. Make sure to add the ‘label’ attribute to the .open method for accessibility.

  • You can pass in the data from the Mascot Picker component to the mascotPickerModal component by using the @api properties declared in the mascotPickerModal component’s JavaScript module
MascotPickerModal.open({
    // pass data via the @api properties declared in the Modal component
      mascots: this.mascots
  }).
  then((result) => {
            
  });

The diagram below shows the flow of data between the Mascot Picker LWC component (the component that launches the modal) and the mascotPickerModal (launched as a modal) LWC component.

Flow of data between the Mascot Picker LWC component and the mascotPickerModal LWC component

  • You can listen for the events in the Mascot Picker component dispatched from the mascotPickerModal component. To do this, bind the events to the .open() method of the mascotPickerModal component’s class. This is shown in the code snippet below.
MascotPickerModal.open({
    // Bind the events dispatched from the Modal component
            onselect: (e) => {
                e.stopPropagation();
                //Call function to handle the selection event
                this.handleSelectEvent(e.detail);
            }
  }).
  then((result) => {
            
  });
  
  // function to handle the selection event
    handleSelectEvent(detail) {
        this.selectedMascot = detail.id;
    }

The diagram below shows how to dispatch and capture events between the Mascot Picker LWC component and the mascotPickerModal LWC component.

Flow of events between the Mascot Picker LWC component and the mascotPickerModal LWC component

NOTE – ‘oneventname’ on the left of the image and ‘eventname’ on the right is the key to naming events.

To summarize, there are two ways of moving data out of the LightningModal:

  1. By closing the Modal and passing the data in the close() method.
  2. Modal events are covered in the previous diagram.

Considerations

At the time of writing this blog post, we found an issue with the LightningModal component for those who have the Lightning Web Security (LWS) setting disabled in their orgs. The issue is that the events dispatched from the lightning-button in the LightningModal do not fire. The error encountered is usually shown when you debug via the Chrome developer console as: 'EventTarget': parameter 1 is not of type 'Event'.

We have a workaround for the above issue till you enable LWS setting for your org. Wrap the component within the modal that dispatches the event into its dedicated LWC component.

For our example app to work in orgs that have LWS disabled, I have created a dedicated LWC component buttonWrapper that dispatches the custom events that can be handled in the component launching the modal. We use this wrapper component instead of the standard lightning-button to get the modal events working.

The code for the Button Wrapper LWC component used as a child component in the mascotPickerModal LWC component is shown below.

The component markup:

<template>
    <lightning-button
        label="Confirm"
        onclick={handleConfirm}
    ></lightning-button>
</template>

The JavaScript module:

import { api, LightningElement } from "lwc";

export default class ButtonWrapper extends LightningElement {
    @api
    selectedMascot;
    handleConfirm() {
        this.dispatchEvent(
            new CustomEvent("select", {
                bubbles: true, 
                composed: true,
                detail: {
                    selectedMascot: this.selectedMascot
                }
            })
        );
    }
}

The modified component markup of the mascotPickermodal LWC is using the new buttonWrapper as a child component is shown below.

<template>
    .....
    <lightning-modal-footer>
        <c-button-wrapper onselect={handleConfirm} selected-mascot={selectedMascot}></c-button-wrapper>
    </lightning-modal-footer>
</template>

To see the complete working source code of the app with custom events for orgs with disabled Lightning Web Security, check out the source code in the branch, lws-disabled.

Conclusion

The LightningModal component also comes with built-in features, such as SLDS blueprint styles, accessibility, and support for styling hooks. Read more on this in the docs. A dedicated component for building modals means less boilerplate code and improved developer efficiency.

Further Resources

About the author

Mohith Shrivastava

Mohith Shrivastava is a Developer Advocate at Salesforce with a decade of experience building enterprise-scale products on the Salesforce Platform. He is presently focusing on the Salesforce Developer Tools, Flow, Apex, and Lightning Web Components at Salesforce. Mohith is currently among the lead contributors on Salesforce Stack Exchange, a developer forum where Salesforce Developers can ask questions and share knowledge. You can follow him via his Twitter @msrivastav13.

Get the latest Salesforce Developer blog posts and podcast episodes via Slack or RSS.

Add to Slack Subscribe to RSS

More Blog Posts

Enhance Flexibility with Custom Property Editors and Types in Experience Builder

Enhance Flexibility with Custom Property Editors and Types in Experience Builder

Learn to make your Lightning Web Components visually interactive and easy to configure in Experience Builder.December 05, 2023