[SalesForce] Making a Generic Lightning Modal Component

I have an lightning application in which I am eventually going to have multiple modal popups being fired from various location in the app. I want to make a generic modal component that will serve as the structure for all of them instead of making lots of different modals with duplicate code. For now, I have just one popup with a save button at the bottom which will fire the save method and a cancel button which will close the modal. Currently, I have a structure like this:

<c:modalCmp >
   <c:my_modal />
<c:/modalcmp>

and inside my modalCmp I have:

<div aria-hidden="true" role="dialog" class="slds-modal slds-modal--large slds-fade-in-hide" aura:id="modaldialog">
    <div class="slds-modal__container modalSpacing">
        <div class="slds-modal__content slds-p-around--medium">
            {!v.body}
        </div>
    </div>
</div>
<div class="slds-backdrop slds-backdrop--hide" aura:id="backdrop">
</div>

Now, inside the my_modal component, I have the details of the specific modal I want and the generic shell uses the body to populate it. The issue here is that some Modals, like the one I am making require the save and cancel. In order to save, i'll need to be inside the specific component to get and save the data. And it order to press cancel, ill need to be inside the generic modal to access the close method. Additionally, the way i have seen to close a Modal in salesforce is something like this:

 toggleClassInverse: function(component,componentId,className) {
    var modal = component.find(componentId);
    $A.util.addClass(modal,className+'hide');
    $A.util.removeClass(modal,className+'open');
},

Where it simply hides it. This works well until I need to do actions on inputted data and then press cancel, in which it keeps what ever I have typed in. Now i'm having to clear my attributes on close of the modal. So my question is, is there a good way to make this generic Modal component?
Thanks

Best Answer

I have implemented something like this. Here is the code :

My modal component which will contain the other component. You will see that this modal take the name of another component as parameter. This is because it will load the other component within itself :

 <aura:component >
<!-- global/shared attributes definition -->
<aura:attribute name="recordId" type="string" description="The record id of the request" /> 
<aura:attribute name="windowTitle" type="string" description="The title to show on the modal window" />

<aura:attribute name="cmpName" type="string" description="The component name to encapsulate. e.g : c:LC19_GestionMembre" />    

<aura:attribute name="showModal" type="boolean" default="true" description="Control the modal view" />

<aura:attribute name="refreshOnClose" type="boolean" default="false" description="If this variable is set to true, then when this modal will close, it will send instruction to the caller component, so that it refresh itself. (The refresh is implemented by the calling component)" />

<aura:attribute name="refresh" type="boolean" default="false" description="tell the calling component that the component has finished execution and can now refresh the set of result" />

<aura:attribute name="showFooter" type="boolean" default="true" description="If set to false, the modal will not display the cancel and save button" />

<aura:attribute name="showHeader" type="boolean" default="true" description="If set to false, the modal will display the title header" />

<aura:attribute name="displayWith" type="boolean" default="true" description="If set to false, the modal will display the title header" />

<aura:attribute name="saveButtonText" type="string" default="{!$Label.c.VFP02Sauvegarder}" description="Component can replace the save text by another text. e.g Executer" />

<aura:attribute name="large" type="boolean" default="false" description="whether to display a 90% modal or 50%" />


<!-- local attributes definition -->
<aura:attribute name="saveTriggered" type="integer" default="0" description="This variable tell the component that the modal save button has been triggered" />

<aura:attribute name="closeTriggered" type="boolean" default="false" description="This variable tell the component to close the modal" />    

<aura:attribute name="disableSave" type="boolean" default="false" description="This variable control the state of the save button. It is disabled when action in progress" />

<!-- handlers definition -->
<aura:handler name="change" value="{!v.closeTriggered}" action="{!c.handleClose}" />
<aura:handler name="change" value="{!v.showModal}" action="{!c.loadCmp}" />

<div role="dialog" class="{!v.showModal ? (v.large ? 'slds-modal slds-modal--large slds-fade-in-open' : 'slds-modal slds-fade-in-open'): 'slds-hide'}">
    <div class="slds-modal__container">
        <div class="{!v.showHeader ? 'slds-modal__header': 'slds-modal__header slds-modal__header--empty'}">
            <button class="slds-button slds-modal__close slds-button--icon-inverse" title="Close" onclick="{!c.handleCancel}">
                <lightning:icon iconName="utility:close" size="small" />
                <span class="slds-assistive-text">Close</span>
            </button>
            <h2 class="{!v.showHeader ? 'slds-text-heading--medium': 'slds-hide'}">{!v.windowTitle}</h2>
        </div>            

        <div aura:id="xid-cmp-body" class="slds-modal__content slds-p-around--medium slds-is-relative xcs-fix-max-height">
            <lightning:spinner variant="brand" size="small"/>
            <!--####################################################### -->
            <!-- The component is dynamically loaded here -->
            <!--####################################################### -->                              
        </div>
        <div class="{!v.showFooter ? 'slds-modal__footer': 'slds-hide'}">
            <button class="slds-button slds-button--neutral" onclick="{!c.handleCancel}">{!$Label.c.GLO_Annuler}</button>
            <button class="slds-button slds-button--brand" disabled="{!v.disableSave}" onclick="{!c.sendActionSave}">{!v.saveButtonText}</button>
        </div>
    </div>
</div>
<aura:if isTrue="{!v.showModal}">
    <div class="slds-backdrop slds-backdrop--open"></div>
</aura:if>   
</aura:component>

This component in fact share some variable with the other component which will load inside this one. And then when you click on the save button, from the modal, this will trigger an action on the child component which will execute.

Here is the controller :

 ({
    handleClose : function(component, event, helper) {
    var container = component.find("xid-cmp-body");
    container.set("v.body",[]);
    component.set("v.showModal",false);

    if(component.get("v.refreshOnClose")) component.set("v.refresh",true);      

},
handleCancel : function(component, event, helper) {
    var container = component.find("xid-cmp-body");
    container.set("v.body",[]);
    component.set("v.showModal",false);
},
sendActionSave : function(component, event, helper) {
    var i = component.get("v.saveTriggered");
    component.set("v.saveTriggered",(i + 1));
},
handleSaveButton: function(component, event, helper) {      
    component.set("v.disableSave",component.get("v.inProgress"));
},
loadCmp : function(component, event, helper) {
    var container = component.find("xid-cmp-body");
    //console.log("container :",container);

    if(component.get("v.showModal")){
        console.log("starting loading component");
        //determine which component to load             
        $A.createComponent(component.get("v.cmpName"),
                       {"recordId": component.getReference("v.recordId"),
                        "triggerSave": component.getReference("v.saveTriggered"),
                        "triggerClose" : component.getReference("v.closeTriggered"),
                        "triggerCloseInnerModal" : component.getReference("v.closeTriggered"),
                        "actionInProgress" : component.getReference("v.disableSave")},
                       function(cmp) {                               
                            container.set("v.body", cmp);
                       });
    }

},
  handlePress : function(cmp) {
    console.log("button pressed");
  }
})

Now the child component will have some shared attributes defined. Here is the code for the child component. You will have to readapt it :

 <aura:component controller="LC19_GestionDemandeAcces" >
<!-- global/shared attributes definition -->
<aura:attribute name="recordId" type="string" description="The account id of the request " />


<aura:attribute name="triggerSave" type="integer" default="0" description="This variable is used in the case the component is used in a modal which actually trigger the save action " />
<aura:attribute name="triggerClose" type="boolean" default="false" description="This component tell its modal window to close if this is set to true" />
<aura:attribute name="actionInProgress" type="boolean" default="false" description="This variable is set to true when the server is creating the request" />

<aura:attribute name="disableCustomSave" type="boolean" default="false" description="This variable control the state of the save button. It is disabled when action in progress" />


<!-- handlers definition -->

<aura:handler name="change" value="{!v.triggerSave}" action="{!c.handleSave}" />

<!-- ************************************************************* -->
<!-- ************************** THE BODY SECTION *****************-->
<!-- ************************************************************* -->


</aura:component>

I removed much of the thing. But which is important it to implement the handleSave action in your child component. To close the parent modal, just set the variable triggerClose to false.

Related Topic