Lightning Modal - Reusable custom modal component

Finally, the long overdue to add this to my blog came to an end. We often use modals for various custom functionalities. But, everytime we have to get the code from lightning design system as we don't have lightning base component for it. So, I built a base modal component which can be reused like other lightning base components. It supports all the features mentioned in lightning design system. Also, it can be used as Prompt.
To use this in your org, create an lwc component named 'modal' and use the below code.

modal.html

<!-- © vkambham.blogspot.com -->
<template>
    <section role="dialog" tabindex="-1" class={_modalClass} aria-labelledby="modalHeading" aria-modal="true"
        aria-describedby="modalContent">
        <div class={_containerClass}>
            <header class={_headerClass}>
                <lightning-button-icon if:false={hideCloseButton} class="slds-modal__close" title="Close"
                    icon-name="utility:close" variant="bare-inverse" size={closeButtonSize} onclick={closeModal}>
                </lightning-button-icon>
                <h2 id="modalHeading" class="slds-text-heading_medium slds-hyphenate">{_header}</h2>
            </header>
            <div class={_contentClass} id="modalContent" style={styleIt}>
                <slot></slot>
            </div>
            <footer class={_footerClass}>
                <slot name="footer" onslotchange={handleFooter}></slot>
            </footer>
        </div>
    </section>
    <div class="slds-backdrop slds-backdrop_open"></div>
</template>

modal.js

/* © vkambham.blogspot.com */
import { LightningElement, api } from "lwc";

const MODAL_CLASS = "slds-modal slds-fade-in-open";
const CONTAINER_CLASS = "slds-modal__container";
const CONTENT_CLASS = "slds-modal__content slds-modal__content_footless";
const HEADER_CLASS = "slds-modal__header ";
const THEME_CLASS = "slds-theme_alert-texture slds-theme_";
const FOOTER_CLASS = "slds-modal__footer slds-hide ";

export default class Modal extends LightningElement {
    isConnected;
    _modalClass = "";
    modalClassVal = "";
    _containerClass = "";
    contentClassVal = "";
    _contentClass = "";
    contentClassVal = "";
    _headerClass = "";
    headerClassVal = "";
    _footerClass = "";
    footerClassVal = "";

    _header = undefined;
    _size;
    _type;
    _variant = "shade";

    @api showModal = false;
    @api closeButtonSize = "large";
    @api hideCloseButton = false;

    @api
    set size(value) { //Supported values: small,medium,large,x-large
        this._size = value;
        if (this.isConnected) this.doInit();
    }
    get size() {
        return this._size;
    }

    @api
    set type(value) { //Applicable for Prompt/Confirm dialogs. Valid values: prompt,confirm
        this._type = value;
        if (this.isConnected) this.doInit();
    }
    get type() {
        return this._type;
    }

    @api
    set variant(value) { //Applicable for Prompt/Confirm dialogs. Valid values: shade, success, warning, error
        this._variant = value;
        if (this.isConnected) this.doInit();
    }
    get variant() {
        return this._variant;
    }

    @api
    set header(value) { //If we don't pass value here, it will display headerless modal
        this._header = value;
        if (this.isConnected) this.doInit();
    }
    get header() {
        return this._header;
    }

    @api
    set modalClass(value) {
        this.modalClassVal = value;
        if (this.isConnected) this.doInit();
    }
    get modalClass() {
        return this.modalClassVal;
    }

    @api
    set containerClass(value) {
        this.containerClassVal = value;
        if (this.isConnected) this.doInit();
    }
    get containerClass() {
        return this.containerClassVal;
    }

    @api
    set headerClass(value) {
        this.headerClassVal = value;
        if (this.isConnected) this.doInit();
    }
    get headerClass() {
        return this.headerClassVal;
    }

    @api
    set contentClass(value) {
        this.contentClassVal = value;
        if (this.isConnected) this.doInit();
    }
    get contentClass() {
        return this.contentClassVal;
    }

    @api
    set footerClass(value) {
        this.footerClassVal = value;
        if (this.isConnected) this.doInit();
    }
    get footerClass() {
        return this.footerClassVal;
    }

    connectedCallback() {
        this.doInit();
        this.isConnected = true;
    }

    doInit() {
        let modalSize = this._size == "x-large" ? "large" : this._size;
        this._modalClass = `${MODAL_CLASS} slds-modal_${modalSize} ${this.modalClassVal}`;
        this._containerClass = `${CONTAINER_CLASS} vk-modal-container_${this._size} ${this.containerClassVal}`;
        this._contentClass = `${CONTENT_CLASS} vk-modal-content_${this._size} ${this.contentClassVal}`;
        this._headerClass = `${HEADER_CLASS} ${this.headerClassVal}`;
        this._footerClass = `${FOOTER_CLASS} ${this.footerClassVal}`;

        if (!this._header) this._headerClass += " slds-modal__header_empty";
        if (this._type == "confirm" || this._type == "prompt") {
            this._headerClass += " " + THEME_CLASS + this._variant;
            this._modalClass += " slds-modal_prompt";
            this._footerClass += " slds-theme_default";
        }
    }

    closeModal() {
        this.dispatchEvent(new CustomEvent("close"));
    }
    handleFooter() {
        const footerEl = this.template.querySelector("footer");
        footerEl.classList.remove("slds-hide");
        const content = this.template.querySelector(".slds-modal__content");
        content.classList.remove("slds-modal__content_footless");
    }
}

modal.css

/* © vkambham.blogspot.com */
.slds-modal__header .slds-modal__close {
    top: -2rem;
}
.slds-modal__content {
    overflow: unset;
    overflow-y: unset;
}
.vk-overflow-y {
    overflow-y: auto;
}
.vk-modal-container_small {
    width: 25%;
    max-width: 25%;
}
.vk-modal-content_small {
    max-height: max-content;
}
.vk-modal-container_medium {
    width: 50%;
    max-width: 50%;
}
.vk-modal-content_medium {
    max-height: max-content;
}
.vk-modal-container_large {
    width: 80%;
    max-width: 80%;
}
.vk-modal-content_large {
    max-height: max-content;
}
.vk-modal-container_x-large {
    width: 97%;
    max-width: 97%;
    padding-bottom: 1rem;
}
.vk-modal-content_x-large {
    max-height: max-content;
}
.slds-modal_prompt .slds-modal__content {
    padding-left: 0;
    padding-right: 0;
}

Now, let's see how can we use it.

Using as modal

<c-modal if:true={showModal} size="small" header="Reusable Lightning Modal"
    content-class="slds-p-around_medium slds-theme_shade" footer-class="slds-theme_default" onclose={closeModal}>
    <lightning-input label="First Name" placeholder="Enter first name..."
        onchange={handleInputChange}></lightning-input>
    <lightning-input label="Last Name" placeholder="Enter last name..."
        onchange={handleInputChange}></lightning-input>
    <lightning-input type="email" label="Email" placeholder="Enter email..." onchange={handleInputChange}>
    </lightning-input>
    <span slot="footer">
        <lightning-button label="Cancel" onclick={closeModal} class="slds-p-right_small"></lightning-button>
        <lightning-button label="Save" variant="brand" onclick={save}></lightning-button>
    </span>
</c-modal>

Using as Prompt

<c-modal if:true={showPrompt} type="prompt" variant="error" size="small" header="Prompt/Confirm Dialog" hide-close-button>
    <div class="slds-text-heading_small slds-align_absolute-center slds-p-around_medium">
        Are you sure to delete this record?
    </div>
    <span slot="footer">
        <lightning-button label="No" onclick={closePrompt} class="slds-p-right_x-small"></lightning-button>
        <lightning-button label="Yes" variant="brand" onclick={deleteRecord}></lightning-button>
    </span>
</c-modal>
PS: Hope this helps! Please share your feedback/suggestions/any exciting posts you come across. Happy Coding!
Post a Comment (0)
Previous Post Next Post