Tuesday, July 17, 2018

Salesforce Lightning Tutorial - Part 5 | Adding Validations



Welcome to the 5th tutorial in the Salesforce Lightning Tutorial Series. In this post, you'll learn about how you can validations in custom lightning component. I'll be extending the code used in my previous posts so, if you are just starting or need to learn only about applying validations only do have a look at my previous posts or at least the code by having a look at my blog posts starting from here or my github repository code in create branch here so that you can understand the progress till now and the further additions that I'll do in this post.

So, let's begin by adding validations in our Lightning Component. Here our main focus is on client side validations only so all this validation part will be handled by making changes only in the Lightning Component and the Lightning Controller.

1. Lightning Component

Let's have a look at the code below and then I'll highlight the changes.

<aura:component implements="flexipage:availableForRecordHome,force:hasRecordId" controller="ContactListController" access="global">
    <!-- Handler to call function when page is loaded initially -->
    <aura:handler name="init" action="{!c.getContactsList}" value="{!this}" />
    <!-- List of contacts stored in attribute -->
    <aura:attribute name="contactList" type="List" />
    <!-- New Contact Object -->
    <aura:attribute name="contact" type="Contact"
    default="{
        'SObjectType': 'Contact',
        'FirstName': '',
        'LastName': '',
        'Email': '',
        'Phone': ''
    }">            
    </aura:attribute>
    <!-- Method to validate new contact -->
    <aura:method name="validateContact" action="{!c.validateContact}" />
    <!-- Lightning card to show contacts -->
    <lightning:card title="Contacts">
        <!-- Body of lightning card starts here -->
        <p class="slds-p-horizontal_small">
            <!-- Aura iteration to iterate list, similar to apex:repeat -->
            <div aura:id="recordViewForm">
                <aura:iteration items="{!v.contactList}" var="contact">
                    <!-- recordViewForm to view the record -->
                    <lightning:recordViewForm recordId="{!contact.Id}" objectApiName="Contact">
                        <div class="slds-box slds-theme_default">
                            <!-- inputfield checkbox used to check wether to delete the contact or not -->
                            <lightning:input type="checkbox" value="{!contact.Id}" label="Mark for Deletion" aura:id="deleteContact" />
                            <br />
                            <!-- outputfield used to output the record field data inside recordViewForm -->
                            <lightning:outputField fieldName="FirstName" />
                            <lightning:outputField fieldName="LastName" />
                            <lightning:outputField fieldName="Email" />
                            <lightning:outputField fieldName="Phone" />
                        </div>
                    </lightning:recordViewForm>
                    <!-- Line break between two records -->
                    <br />
                </aura:iteration>
            </div>
            <div aura:id="recordEditForm" class="formHide">
                <aura:iteration items="{!v.contactList}" var="contact">
                    <div class="slds-box slds-theme_default">
                        <!-- inputfield used to update the record field data -->
                        <lightning:input value="{!contact.FirstName}" />
                        <!-- Validation added -->
                        <lightning:input aura:id="fieldToValidate" value="{!contact.LastName}" messageWhenValueMissing="Contact's Last Name is Mandatory" required="true"/>
                        <lightning:input type="email" value="{!contact.Email}" />
                        <!-- Validation added -->
                        <lightning:input aura:id="fieldToValidate" messageWhenPatternMismatch="Please enter the number in this pattern - (XXX) XXX-XXXX" type="tel" value="{!contact.Phone}" pattern="\([0-9]{3}\) [0-9]{3}-[0-9]{4}" />
                    </div>
                    <br />
                    <!-- Line break between two records -->
                </aura:iteration>
            </div>
        </p>
        <!-- Lightning card actions -->
        <aura:set attribute="actions">
            <!-- New contact modal button added -->
            <lightning:button name="contactModal" label="New Contact" onclick="{!c.openModal}" />
            <!-- Delete button added -->
            <lightning:button variant="destructive" label="Delete" onclick="{!c.deleteContacts}" />
            <!-- New button added -->
            <lightning:button label="New" onclick="{!c.newContact}" />
            <!-- Edit/Save button added -->
            <lightning:button variant="brand" label="Edit" name="edit" onclick="{!c.editContacts}" />
        </aura:set>
    </lightning:card>
    <!-- Contacts Modal Section -->
    <div>
        <section aura:id="contactModal" role="dialog" tabindex="-1" aria-labelledby="contactModalHeading" aria-modal="true" aria-describedby="contactModalBody" class="slds-modal">
            <!-- Modal Container -->
            <div class="slds-modal__container">
                <!-- Modal Header ( consists of close button and heading of modal ) -->
                <header class="slds-modal__header">
                    <lightning:buttonIcon class="slds-modal__close" alternativeText="Close" iconName="utility:close" onclick="{!c.closeModal}" variant="bare-inverse" size="large"></lightning:buttonIcon>
                    <h2 id="contactModalHeading" class="slds-text-heading_medium slds-hyphenate">New Contact</h2>
                </header>
                <!-- Modal Body ( consists of form ) -->
                <div class="slds-modal__content slds-p-around_medium" id="contactModalBody">
                    <!-- Validation added -->
                    <lightning:input aura:id="formFieldToValidate" label="First Name" messageWhenValueMissing="Contact's First Name is Mandatory" required="true" value="{!v.contact.FirstName}" />
                    <!-- Validation added -->
                    <lightning:input aura:id="formFieldToValidate" label="Last Name" messageWhenValueMissing="Contacts's Last Name is Mandatory" required="true" value="{!v.contact.LastName}" />
                    <!-- Custom Validation added -->
                    <lightning:input aura:id="formFieldToValidate" label="Email" name="emailField" value="{!v.contact.Email}" />
                    <lightning:input label="Phone" value="{!v.contact.Phone}" />
                </div>
                <!-- Modal Footer ( consists of cancel and save buttons ) -->
                <footer class="slds-modal__footer">
                    <lightning:button onclick="{!c.closeModal}" variant="neutral">Cancel</lightning:button>
                    <lightning:button onclick="{!c.createContact}" variant="brand" >Save</lightning:button>
                </footer>
            </div>
        </section>
        <!-- Modal Backdrop -->
        <div aura:id="contactModalBackdrop" class="slds-backdrop"></div>
    </div>
</aura:component>
I have made changes in mainly the edit form and the new record form i.e. the modal which is used to create a new contact. The edit form is mainly the form with aura:id as recordEditForm. In the edit form, focus on the fields with <!-- Validation added --> comments, the first field is the Last name field, as the last name of a contact is required so I have set the required attribute to true and there is another attribute named messageWhenValueMissing i.e. this message will be displayed as a validation error when value of required attribute is missing. So, I have provided a message for the same as - Contact's Last Name is Mandatory. Another validation is for the phone field in which we added a pattern, when we created this form so we have to add that type of validation error message in the respective attribute. So, the attribute used here is messageWhenPatternMismatch and a friendly message is given that will be displayed when this attribute doesn't follow the pattern specified. One more thing to notice is that all the fields that are marked for validation has the same aura:id as fieldToValidate. <lightning:input> allows us to add validations in all fields at once, so I have given the same aura id to access all as an array. You'll see its usage when we move on to controller. The same procedure is applied to add validations in fields included in modal. In those fields, one field has <!-- Custom Validation added --> comment in which there is no particular message as we are going to use it to add custom validation. I have also added an aura:method attribute with name as validateContact and action as {!c.validateContact} which is the function that we have defined in controller. This function will be called from controller itself, that's why we need to specify it using aura:method tag to have it's definition in component so that it can be called using the component reference as component.validateContact(); | general syntax - component.<aura method name attribute>();

2. Lightning Controller

Moving on to the last part of our validation i.e. Lightning Controller, let's have a look at the code below before discussion.

({
    // Function called on initial page loading to get contact list from server
        getContactsList : function(component, event, helper) {
        // Helper function - fetchContacts called for interaction with server
                helper.fetchContacts(component, event, helper);
        },

    // Function used to create a new Contact
    newContact: function(component, event, helper) {
        // Global event force:createRecord is used
        var createContact = $A.get("e.force:createRecord");
        // Parameters like apiName and defaultValues are set
        createContact.setParams({
            "entityApiName": "Contact",
            "defaultFieldValues": {
                "AccountId": component.get("v.recordId")
            }
        });
        // Event fired and new contact dialog open
        createContact.fire();
    },

    // Function used to update the contacts
    editContacts: function(component, event, helper) {
        // Getting the button element
        var btn = event.getSource();
        // Getting the value in the name attribute
        var name = btn.get('v.name');
        // Getting the record view form and the record edit form elements
        var recordViewForm = component.find('recordViewForm');
        var recordEditForm = component.find('recordEditForm'); 
        // If button is edit
        if(name=='edit') {
            // Hiding the recordView Form and making the recordEdit form visible
            $A.util.addClass(recordViewForm,'formHide');
            $A.util.removeClass(recordEditForm,'formHide');
            // Changing the button name and label
            btn.set('v.name','save');
            btn.set('v.label','Save');
        }
        else if(name=='save') {
            // Getting the edit form fields to validate
            var contactFields = component.find("fieldToValidate");
            // Initialize the counter to zero - used to check validity of fields
            var blank=0;
            // If there are more than 1 fields
            if(contactFields.length!=undefined) {
                // Iterating all the fields
                var allValid = contactFields.reduce(function (validSoFar, inputCmp) {
                // Show help message if single field is invalid
                inputCmp.showHelpMessageIfInvalid();
                // return whether all fields are valid or not
                return validSoFar && inputCmp.get('v.validity').valid;
                }, true);
                // If all fields are not valid increment the counter
                if (!allValid) {
                    blank++;
                }
            } else {
                // If there is only one field, get that field and check for validity (true/false)
                var allValid = contactFields;
                // If field is not valid, increment the counter
                if (!allValid.get('v.validity').valid) {
                    blank++;
                }
            }
            // Call the helper method only when counter is 0
            if(blank==0) {
                // Calling saveContacts if the button is save
                helper.saveContacts(component, event, helper);                
            }
        }
    },
    
    // Function used to delete the contacts
    deleteContacts: function(component, event, helper) {
        // Calling removeContacts Helper Function
        helper.removeContacts(component, event, helper);
    },

    // Function used to open the contact modal
    openModal: function(component, event, helper) {
        var modal = component.find("contactModal");
        var modalBackdrop = component.find("contactModalBackdrop");
        $A.util.addClass(modal,"slds-fade-in-open");
        $A.util.addClass(modalBackdrop,"slds-backdrop_open");
    },

    // Function used to close the contact modal
    closeModal: function(component, event, helper) {
        var modal = component.find("contactModal");
        var modalBackdrop = component.find("contactModalBackdrop");
        $A.util.removeClass(modal,"slds-fade-in-open");
        $A.util.removeClass(modalBackdrop,"slds-backdrop_open");
    },

    // Function used to create new contact
    createContact: function(component, event, helper) {
        var isContactValid = component.validateContact(component, event, helper);
        if(isContactValid) {
           helper.insertContact(component, event, helper);
        }
    },

    // Function to validate new contact - Aura method used for the same
    validateContact: function(component, event, helper) {
        // Getting all fields and iterate them to check for validity
        var allValid = component.find('formFieldToValidate').reduce(function (validSoFar, inputCmp) {
            // Show help message if single field is invalid
            inputCmp.showHelpMessageIfInvalid();
            // Get the name of each field
            var name = inputCmp.get('v.name');
            // Check if name is emailField
            if(name=='emailField') {
                // Getting the value of that field
                var value = inputCmp.get('v.value');
                // If value is not equal to rahul@gmail.com, add custom validation
                if(value != 'rahul@gmail.com') {
                    // Focus on that field to make custom validation work
                    inputCmp.focus();
                    // Setting the custom validation
                    inputCmp.set('v.validity', {valid:false, badInput :true});
                }                
            }
            // Returning the final result of validations
            return validSoFar && inputCmp.get('v.validity').valid;
        }, true);
        // Returning Validate contact result in boolen
        return allValid;
    }
})
In the above code, I have made some changes in the editContacts and createContact function, and also added a new function named validateContact for which we used the aura:method tag in the component. Starting with the editContacts  function, earlier in this, in the save section we called helper.saveContacts() and performed all the server related tasks there. But now we have to validate the fields first. In save section, First of all, we get all the fields with aura id fieldToValidate and store it in variable contactFields. Now, it may be possible that component.find return a single element too if there is only one field on which the validation is applied. So, if the contactFields is an array (checked by length!=undefined as an array should have a length). We are applying the reduce function to contactFields which is a javascript method in which we iterate each element of array and here we refer it by inputCmp.

The reduce function takes 2 parameters:- first the function whose result we have to return and 2nd the initial value or initial result. So, initially we have assumed that all fields are valid so we have given true as the 2nd parameter and in the first parameter, the function will take two parameters, first the result till now and 2nd the current element and the return value of this function will further be passed to the same function in next iteration in the first parameter (result till now). We have 2 variables validSoFar and inputCmp. If you want more detailed explaination for the reduce function(), I am sharing a link here. Inside the function which is passed as first argument to reduce, first we called inputCmp.showHelpMessageIfInvalid(); this is a predefined method that will show our error message when we click on save button if the particular inputCmp is invalid and then we simply returned our validSoFar && inputCmp.get('v.validity').valid this means there are 2 possibilities, either our result from the previous iteration was false so validSoFar is false and the result will be false again. If all the fields till now are valid then we and the current result with the validity of current inputCmp therefore, if the inputCmp is not valid, the result of AND operation will be false and this false result is passed to the next iteration as the validSoFar parameter. We have a counter named blank whose value is incremented if all fields are not valid. Similarly if the component.find() doesn't return an array, we checked it's valid attribute i.e. if it returns false, then we further increment the blank counter ( you can check this by removing the fieldToValidate aura id from one of the two input fields as this will lead to only one field left with that aura id and the condition will be executed ). Finally, if the blank counter has a value 0, this means that all our fields are valid. Therefore, saveContacts() method of helper is called and the contacts are saved. Till now we have worked on applying validations to the edit form which looks like this:-

Custom Validation

Moving on to our createContact() function, we called the validContact() function that is also defined in the controller itself using component.validContact(component, event, helper). This is possible only because we have added aura:method tag in our lightning component. So, let's explore our validateContact function now and see what's there. It's much similar to out previous solution, in this also, we are calling component.find() on input fields with id formFieldToValidate. If you remember in the lightning component, I have given formFieldToValidate aura id to inputs that were in the modal which is mainly used to create a new Contact. Here I am not checking for array or single element as I know I have more than one fields with same aura:id so definitely, component.find() will return an array. I again called the showHelpMessageIfInvalid(). Now, I have to add custom validation on field with name emailField so I get the name of field using inputCmp.get("v.name") and if name equals emailField I am going to show an error if it's value is not equal to rahul@gmail.com this is just an example of custom validation you can add any other condition. So, I get the value of field using inputCmp.get("v.value") and if this value is not equal to rahul@gmail.com I focused on the inputCmp using inputCmp.focus() and set the validity attribute of inputCmp to {valid:false, badInput :true}. The validity attribute looks like this:-


This means that I am making the validity attribute of that field invalid and the reason is badInput as I am making that true and finally I am returning the AND of  validSoFar and current input fields validity and the default value is true just like before. But if you notice the contactList component, in the modal email field, we haven't given any attribute and it's value like :- messageWhenBadInput="" so the default message will be displayed i.e. Enter a valid value.So, in this way, we can add custom validations in our lightning component and if you are wondering about that inputCmp.focus(); line so it's necessary to focus the particular field to show the custom validation error message to appear. You can remove this line and give it a try, in that case, you have to manually focus it to show the error message. Actually, the lightning:input tag is still in beta version so there maybe any further advancements possible to make custom validations more easier. If you want to learn more about the various validations we can apply and the respective attributes for lightning:input, you can find them in the official doc here. Just scroll down to the error messages part while viewing the same. Also, if you came across a better approach, feel free to share it in comments section below. We have applied custom validations in the modal section and it looks like this:-

No comments:

Post a Comment