SFDC Stop - Always the latest about Salesforce


Full Tutorial Series with videos, free apps, live sessions, salesforce consulting and much more.


Telegram logo   Join our Telegram Channel

Monday, 14 August 2023

Lifecycle Hooks in LWC

Hello Trailblazers,


In this post we'll understand different lifecycle hooks of lightning web components.


What is a Lifecycle Hook?

A lifecycle hook is a callback method which is called automatically at some point during a lightning web component's lifecycle. In simple words, I can say: From the moment a component is initialized, till the moment it's removed from the page, there are various important instances that can be used by developers to perform various actions. So, in order to allow the developers to perform those actions, some callback methods are automatically called during these instances.

What are the different lifecycle hooks of lightning web components?

There are mainly 5 lifecycle hooks in lightning web components. They are:

  1. constructor()

  2. connectedCallback()

  3. renderedCallback()

  4. disconnectedCallback()

  5. errorCallback()

There is a great diagram present in the salesforce official documentation showcasing the lifecycle flow or in other words: the order in which these lifecycle hooks are being called. Let's take a look at that below:


This basically means that whenever a parent and child component is rendered on the page, the order of lifecycle hooks will be as follows:

  1. constructor() called on parent
  2. connectedCallback() called on parent
  3. constructor() called on child
  4. connectedCallback() called on child
  5. renderedCallback() called on child
  6. renderedCallback() called on parent

In case of an error in child component, the errorCallback() of parent component will be called after the first 4 callbacks mentioned above i.e. before calling the renderedCallback() for child + parent component. The updated order in case of an error in child will be:

  1. constructor() called on parent
  2. connectedCallback() called on parent
  3. constructor() called on child
  4. connectedCallback() called on child
  5. errorCallback() called on parent
  6. renderedCallback() called on child
  7. renderedCallback() called on parent

Now it's time to get into action and verify whatever we specified above

constructor()

This callback method is called as a component is constructed. The first statement inside a constructor should always be super(). Calling super() will provide the correct value for this keyword which we can use in our lwc. Some more important points about constructor are:

  1. You cannot access public properties (i.e. the properties annotated with @api) within a component's constructor. If you try to refer anything, it'll come as undefined

  2. You cannot access any html tag/element inside the component's constructor as the component is not rendered yet.

  3. You can access private properties of a component inside a constructor

Now the question is: What should we use constructor() for?

Like a normal javascript class, this lwc class constructor can also be used to initialize some private variables you may have. It can be used to perform an operation like: calling your apex method (or any javascript method) to retrieve information or perform a particular action.

Let's have a look at an example lightning component named Child below:

child.js

import { LightningElement, api } from 'lwc';

export default class Child extends LightningElement {

    count = 0;
    @api
    message = 'default';

    constructor() {
        super();
        console.log('Child constructor called');
        console.log(this.count);
        console.log(this.message);
        console.log(this.template.querySelector('lightning-button'));
    }
}
As you can see in the above js file, a constructor is defined. Inside the constructor, first of all calling super(), then we are displaying a text - Child constructor called, then displaying value of private variable count, after that we're displaying value of public variable message and finally we're trying to display the reference to a lightning-button element present in our html.

child.html

We have a lightning-button with label Increase Count in our html file which will call a js method increaseCount() as the button is clicked:
<template>
    <lightning-button label="Increase Count" onclick={increaseCount}></lightning-button>
    <br /><br />
</template>

We'll use this button later in the tutorial. For now, let's see the console logs as this component is rendered:

First we have the message Child constructor called, the value of private variable count is coming as 0 inside the constructor, the value of public variable message is coming as undefined and finally, the reference to lightning-button is also coming as null inside the constructor, because the component is not rendered yet.

connectedCallback()

This callback method is called when a lwc component is inserted into the dom. It establishes communication of the lwc component with the current document or container where it is inserted. Some important points about connected callback are:

  1. connectedCallback() on the parent component will be called before connectedCallback() on the child component is called. This is the reason that you cannot access child components from connectedCallback() of parent, as the child components are not inserted into the dom yet

  2. This callback is invoked after all the public properties are set. This means that whatever initial values are being passed to the public properties of component, the component will have those values assigned when the connectedCallback() is called

  3. This also means that connectedCallback() can only access the INITIAL VALUES OF PUBLIC PROPERTIES i.e. if you're updating the public property after the component is inserted, connectedCallback() will not be called again with the new value of public property. So, if you've a method which should be called based on the value of public property, it's better to call that method in a setter instead of connectedCallback() as the setter will be called again and again whenever the value of public property is set/updated

  4. You can perform some initialization tasks in the connectedCallback() like: listening for events or getting some initial data from the server

  5. connectedCallback() can be called more than 1 time as well. An example scenario can be: when a component is removed from the DOM and inserted again

Let's add connectedCallback() to our Child lwc component now. The updated code is provided below:

child.js

import { LightningElement, api } from 'lwc';

export default class Child extends LightningElement {

    count = 0;
    @api
    message = 'default';

    constructor() {
        super();
        console.log('Child constructor called');
        console.log(this.count);
        console.log(this.message);
        console.log(this.template.querySelector('lightning-button'));
    }

    connectedCallback() {
        console.log('Child connected callback called');
        console.log(this.count);
        console.log(this.message);
        console.log(this.template.querySelector('lightning-button'));
    }
}
As you can see above, I'm trying to access the private property count, public property message and lightning-button again in my connectedCallback(), if you remember in our constructor, we were only able to access the value of private property i.e. count, message was coming as undefined and lightning-button reference was coming as null. Let's see if we're able to access anything else now out of these.
As you can see in the output above, message is coming as default this time instead of undefined. However, the reference to lightning-button is still coming as null. This is because the public properties of our lwc component are now having the initial value assigned to them. As lightning-button is a child component with respect to our child lwc component, it's still not connected to the DOM and therefore is coming as null (remember the first point specified above: connectedCallback() on the parent is called before connectedCallback() on the child and here child component is lightning-button).

Let's create a parent component as well and pass the value to our message (public property) from there to ensure it gets reflected in our connectedCallback() as well. Our parent component's name is Parent and the code for the same is provided below:

parent.html

<template>
    <c-child message="hello"></c-child>
</template>
As you can see above, we're passing the value of message variable as hello. Let's take a look at the js file as well

parent.js

import { LightningElement } from 'lwc';

export default class Parent extends LightningElement {

    constructor() {
        super();
        console.log('Parent constructor called');
    }

    connectedCallback() {
        console.log('Parent connected callback called');
        console.log(this.template.querySelector('c-child'));
    }

    renderedCallback() {
        console.log('Parent rendered callback called');
        console.log(this.template.querySelector('c-child'));
    }
}
Here also, I've defined a constructor and a connectedCallback() as well to see in which order the parent child lifecycle hooks are being called. I also added a renderedCallback(). This renderedCallback() method should be called post connectedCallback() is called on child as per the order. We'll learn more about renderedCallback() in a bit, I've added this here for a reason. Let's have a look at the updated console.log() now:


The order of events is shown below:

  1. Parent constructor() is called

  2. Parent connectedCallback() is called and the reference to child component c-child is coming as null inside it

  3. Child constructor() is called. Value of count is 0, message is undefined and reference to lightning-button is coming as null as it's further a child component for our child lwc.

  4. Child connectedCallback() is called where value of count is 0. Notice that the value of message is hello instead of default this time because this is the value which is passed from our parent lwc to child lwc for this public property, reference to lightning-button is still coming as null

  5. Finally, our renderedCallback() method is called in our parent component as per the order and as it's called after the child's connectedCallback() method, this means that the child lwc is now connected to the DOM. Therefore, reference to child lwc is not coming as null this time, as it was coming in the parent's connectedCallback() method.

I hope all of this is clear. Now, let's move on to the renderedCallback() method.

renderedCallback()

As the name suggests, this callback method is called once the component has rendered. As the component can render multiple times, this callback method will also be called each time the component is re-rendered. Some important points about renderedCallback() are:

  1. renderedCallback() on the child component is called before it's called on the parent component

  2. Whenever the component renders, the expressions used in a template are re-evaluated. This means that if we've created a getter method which is used in our html file and that getter is returning a dynamic value based on some properties, it'll be re-evaluated as well

  3. Whenever the component's state is changed, the component will be re-rendered

  4. When a component is re-rendered, the lwc engine attempts to reuse the existing elements. For example: if you update something in the parent component due to which the parent re-renders, it'll not re-render the child component. Another example can be: if you're displaying a list of child components and if you re-order the list, then although the components are placed at different positions now, they're not re-rendered. The engine assumes that the same components are being used and just placed at a different positions now, so they're not re-rendered

    However, if you use a different key/update the key of child component, it might get re-rendered - I'm not going to show a demo of this, this is your homework. Try and let me know how it works in the comments down below!

  5. As I specified in point 3, whenever the component's state is changed, the component will be re-rendered. Therefore, we need to make sure that we don't update the state of the component (for example: a property which is being displayed in component's html) in the renderedCallback() itself as it'll re-render the component and will call renderedCallback() again. In this case, the renderedCallback() will be called again and again recursively which will result in an infinite loop

We've already defined renderedCallback() in our parent component as specified in the connectedCallback() section. Let's define renderedCallback() in our child lwc as well.

child.js

import { LightningElement, api } from 'lwc';

export default class Child extends LightningElement {

    count = 0;
    @api
    message = 'default';

    constructor() {
        super();
        console.log('Child constructor called');
        console.log(this.count);
        console.log(this.message);
        console.log(this.template.querySelector('lightning-button'));
    }

    connectedCallback() {
        console.log('Child connected callback called');
        console.log(this.count);
        console.log(this.message);
        console.log(this.template.querySelector('lightning-button'));
    }

    renderedCallback() {
        console.log('Child rendered callback called');
        console.log(this.template.querySelector('lightning-button'));
    }
}
Let's have a look at the updated logs now:


The order  of events is provided below:

  1. Parent constructor called

  2. Parent connected callback called where reference to child lwc is coming as null

  3. Child constructor called where count is 0, message is undefined and reference to lightning-button is coming as null

  4. Child connected callback called where count is 0, message is hello and reference to lightning-button is again coming as null

  5. Child rendered callback called where reference to lightning-button is coming properly as the lightning-button is connected to the DOM now

  6. At last, parent rendered callback is called where reference to child lwc is coming properly as the child lwc is connected to DOM now

We are now going to re-render the parent lwc to see when renderedcallback() is called in parent and child LWCs. Let's add some more code!

If you remember, our child lwc html is calling a method increaseCount() when the lightning-button is clicked, let's add that method to our child lwc js file as shown below:
increaseCount() {
    this.dispatchEvent(new CustomEvent('increasecount', {
        detail: {
            message: 'Increased count to ' + (++this.count)
        }
    }));
}
This method will fire an event named increasecount whenever the button is clicked which will contain a message with the value of count variable increased by 1. The full code of child.js is provided below:

child.js

import { LightningElement, api } from 'lwc';

export default class Child extends LightningElement {

    count = 0;
    @api
    message = 'default';

    constructor() {
        super();
        console.log('Child constructor called');
        console.log(this.count);
        console.log(this.message);
        console.log(this.template.querySelector('lightning-button'));
    }

    connectedCallback() {
        console.log('Child connected callback called');
        console.log(this.count);
        console.log(this.message);
        console.log(this.template.querySelector('lightning-button'));
    }

    renderedCallback() {
        console.log('Child rendered callback called');
        console.log(this.template.querySelector('lightning-button'));
    }

    increaseCount() {
        this.dispatchEvent(new CustomEvent('increasecount', {
            detail: {
                message: 'Increased count to ' + (++this.count)
            }
        }));
    }
}
Notice the increaseCount() added at the end. Let's update our parent.html file as well now:

parent.html

<template>
    <lightning-card title={message}>
        <p class="slds-var-p-around_small">
            <c-child onincreasecount={updateMessage} message="hello"></c-child>
        </p>
    </lightning-card>
</template>
As you can see above, I've covered my child lwc with a lightning card which is displaying the value of message variable as title. I'm also capturing the increasecount event and calling another method in my parent.js named updateMessage() which will update the value of message variable displayed in the card title. Finally, let's take a look at our updateMessage() defined in parent.js as well:
updateMessage(event) {
    this.message = event.detail.message;
}
As you can see above, it's updating the message variable with the value of message coming from the event. This message variable will be displayed as the title of lightning-card. Let's have a look at the full code below:

parent.js

import { LightningElement, track } from 'lwc';

export default class Parent extends LightningElement {

    message = 'Updated count will appear here!';

    constructor() {
        super();
        console.log('Parent constructor called');
    }

    connectedCallback() {
        console.log('Parent connected callback called');
        console.log(this.template.querySelector('c-child'));
    }

    renderedCallback() {
        console.log('Parent rendered callback called');
        console.log(this.template.querySelector('c-child'));
    }

    updateMessage(event) {
        this.message = event.detail.message;
    }
}
Notice the default value of message variable as: Updated count will appear here!. I've defined the updateMessage() method at the end which is updating the value of this message variable. Let's take a look at the component in action:


As we click the Increase Count button present in child lwc, it fires an event with updated value of count. This increasecount event is captured by parent lwc and it'll update the value of message variable shown as a title of lightning-card as shown above.

The thing to notice here is that, each time we click the button and the event is fired, it re-renders the parent component as shown below:


Notice that only the parent lwc's rendered callback is called again and again and not the child one as I increased count from 1 to 5. This means that even though the parent is rendered multiple times, the child LWC is just reused as there's no change in the state of child lwc. It's still showing the same Increase Count button. This covers our point 3 and 4  under important points about renderedCallback(). It's time to move on to the next callback method now i.e. disconnectedCallback()

disconnectedCallback()

disconnectedCallback() will be called whenever the component is disconnected from the DOM, it's mainly useful to clean up the work done in connectedCallback(). You can use it for simple purposes like to remove cache or event listeners. Let's define disconnectedCallback() on our child component js. You can simply add the below method:
disconnectedCallback() {
    console.log('Child disconnected callback called');
}

Our updated child.js file is shown below:

child.js

import { LightningElement, api } from 'lwc';

export default class Child extends LightningElement {

    count = 0;
    @api
    message = 'default';

    constructor() {
        super();
        console.log('Child constructor called');
        console.log(this.count);
        console.log(this.message);
        console.log(this.template.querySelector('lightning-button'));
    }

    connectedCallback() {
        console.log('Child connected callback called');
        console.log(this.count);
        console.log(this.message);
        console.log(this.template.querySelector('lightning-button'));
    }

    renderedCallback() {
        console.log('Child rendered callback called');
        console.log(this.template.querySelector('lightning-button'));
    }

    disconnectedCallback() {
        console.log('Child disconnected callback called');
    }

    increaseCount() {
        this.dispatchEvent(new CustomEvent('increasecount', {
            detail: {
                message: 'Increased count to ' + (++this.count)
            }
        }));
    }
}
Notice the disconnectedCallback() added above the increaseCount() and below renderedCallback(). Let's update our parent component a little bit as well to make sure we're able to disconnect child lwc from the DOM.

Updated parent.html file is provided below:

parent.html

<template>
    <lightning-card title={message}>
        <p class="slds-var-p-around_small">
            <template if:true={show}>
                <c-child onincreasecount={updateMessage} message="hello"></c-child>
            </template>
            <lightning-button label="Toggle Child" onclick={toggleChild}></lightning-button>
        </p>
    </lightning-card>
</template>

As you can see above, I've added a template tag with if:true condition which is checking a boolean variable named show. Only when this variable is true, our child component will be displayed to us. I'm going to create this variable in our parent.js file. I've added another lightning-button with label Toggle Child which is calling the toggleChild() when clicked. On click of this button, I'm going to toggle the value of show variable from true -> false or from false -> true which will hide/show the child lwc component. This will utlimately call our disconnectedCallback() on our child lwc as well. Let's take a look at the updated parent.js now:

parent.js

import { LightningElement, track } from 'lwc';

export default class Parent extends LightningElement {

    message = 'Updated count will appear here!';
    show = true;

    constructor() {
        super();
        console.log('Parent constructor called');
    }

    connectedCallback() {
        console.log('Parent connected callback called');
        console.log(this.template.querySelector('c-child'));
    }

    renderedCallback() {
        console.log('Parent rendered callback called');
        console.log(this.template.querySelector('c-child'));
    }

    updateMessage(event) {
        this.message = event.detail.message;
    }

    toggleChild() {
        this.show = !this.show;
    }
}
As you can see above, I've added show variable below message variable whose default value is true. I've also added another method named toggleChild() at the end. This method will be called when we click the Toggle Child lightning button and it'll toggle the value of show variable from true to false and from false to true.

This toggling will automatically hide/show the child lwc or I can say connect/disconnect child lwc from the DOM. Let's take a look at the component in action first:
As you can see in the above demo, first of all I increased the count using Increase Count lightning-button in the child component to 2. Then I clicked on Toggle Child button which removed the child component from the DOM. I brought it back by clicking the Toggle Child button again and then I clicked on Increase Count button again which increased the value of count starting from 1 to 5. It started from 1 again as the child lwc is reinitialized and therefore is having the default value of count as 0. Let's take a look at related logs now.

After the components were loaded initially and I clicked on Increase Count button twice and then the Toggle Child button which removed the child lwc from DOM. The console.log statements for these 3 operations are shown below:


As you can see, for the first two operations, when count is increased, parent renderedCallback() is called and I can refer the child lwc easily as it's connected to the DOM. Then I clicked Toggle Child button, it called child's disconnectedCallback() and we have the statement: Child disconnected callback called printed to the console. It also called parent's renderedCallback() as the child is removed from the DOM so the parent is also re-rendered. Notice that this time, the child lwc reference in the parent's renderedCallback() is coming as null as the child component is no more connected to the DOM.

Let's click the Toggle Child button again now:


As you can see above, as I clicked on Toggle Child button again, the child lwc is again connected to DOM. Therefore, the child constructor() is called again, then child connectedCallback() is called, then renderedCallback() and finally parent's renderedCallback() is called once again and this time the reference to child lwc is not coming as null.

Post that, I clicked on Increase Count button 5 more times, the count is increased from 1 to 5 and the parent lwc is rendered 5 times as shown below:

errorCallback()

Now, let's take a look at our last method in the lwc lifecycle i.e. errorCallback(). This callback method will be called whenever an error occurs in lifecycle hook and it captures errors in all the child (descendent) components in it's tree. Let's understand with an example. I'm going to throw error from the connectedCallback() of my child.js file. I'll also define errorCallback() methods in both child and parent lwc to understand which method is being called and the information received in the errorCallback() method. Let's update our child lwc first.

child.js

import { LightningElement, api } from 'lwc';

export default class Child extends LightningElement {

    count = 0;
    @api
    message = 'default';

    constructor() {
        super();
        console.log('Child constructor called');
        console.log(this.count);
        console.log(this.message);
        console.log(this.template.querySelector('lightning-button'));
    }

    connectedCallback() {
        console.log('Child connected callback called');
        console.log(this.count);
        console.log(this.message);
        console.log(this.template.querySelector('lightning-button'));
        let error = {
            code: 100,
            message: 'Error from child connected callback!'
        };
        throw error;
    }

    renderedCallback() {
        console.log('Child rendered callback called');
        console.log(this.template.querySelector('lightning-button'));
    }

    disconnectedCallback() {
        console.log('Child disconnected callback called');
    }

    errorCallback(error, stack) {
        console.log('Child error callback called, error = ' + JSON.stringify(error) + ', stack = ' + JSON.stringify(stack));
    }

    increaseCount() {
        this.dispatchEvent(new CustomEvent('increasecount', {
            detail: {
                message: 'Increased count to ' + (++this.count)
            }
        }));
    }
}
As you can see above, I've updated the connectedCallback(). I'm also showing this update again below:
connectedCallback() {
    console.log('Child connected callback called');
    console.log(this.count);
    console.log(this.message);
    console.log(this.template.querySelector('lightning-button'));
    let error = {
        code: 100,
        message: 'Error from child connected callback!'
    };
    throw error;
}
I've added 4 more lines after console.log statements where I'm defining an error object with two properties, code and message. Then I'm throwing that error object. I also defined errorCallback() method as shown in the below snippet:
errorCallback(error, stack) {
    console.log('Child error callback called, error = ' + JSON.stringify(error) + ', stack = ' + JSON.stringify(stack));
}

errorCallback() has two parameters:
  1. error: This is the JavaScript native error object. It's the error which was thrown by component where it occured. In our case it'll be the error object we're throwing which is having two properties: code and message.

  2. stack: This is a string specifying - in which component the error occured. It'll show path from the component whose errorCallback() was called till the child component where error was thrown

Let's add the errorCallback() in parent lwc as well. I'm going to add the below method to parent.js:
errorCallback(error, stack) {
    console.log('Parent error callback called, error = ' + JSON.stringify(error) + ', stack = ' + stack);
    console.log(this.template.querySelector('c-child'));
}
Let's take a look at the full parent.js file as well after updates:

parent.js

import { LightningElement, track } from 'lwc';

export default class Parent extends LightningElement {

    message = 'Updated count will appear here!';
    show = true;

    constructor() {
        super();
        console.log('Parent constructor called');
    }

    connectedCallback() {
        console.log('Parent connected callback called');
        console.log(this.template.querySelector('c-child'));
    }

    renderedCallback() {
        console.log('Parent rendered callback called');
        console.log(this.template.querySelector('c-child'));
    }

    errorCallback(error, stack) {
        console.log('Parent error callback called, error = ' + JSON.stringify(error) + ', stack = ' + stack);
        console.log(this.template.querySelector('c-child'));
    }

    updateMessage(event) {
        this.message = event.detail.message;
    }

    toggleChild() {
        this.show = !this.show;
    }
}
Notice the errorCallback() added below renderedCallback(). Now, let's take a look at the console statements as the components are loaded to understand how errorCallback() is being called:


A couple of things to notice above:

  1. Only the parent errorCallback() is called and not the errorCallback() present in the child lwc

  2. Error object is received in the errorCallback() which is exactly the same as thrown by the child lwc. The stack string received in the errorCallback() is showing the stack/path from the parent lwc (the component whose errorCallback() is called) to child lwc (where the error was thrown) as: <c-parent> <c-child>

  3. I am trying to display a reference to child lwc in the errorCallback() as well and it's working fine. This means that once the child lwc is connected to the DOM it can be referred in any of the callback methods be it errorCallback() or renderedCallback()

Let's take a look at the order in which the callback methods are executed as well:

  1. Parent: constructor() called

  2. Parent: connectedCallback() called (reference to child lwc is null)

  3. Child: constructor() called (count is coming as 0, message as undefined and reference to lightning-button is coming as null)

  4. Child: connectedCallback() called (count is coming as 0, message as hello and reference to lightning-button is still null as lightning-button is not connected to DOM yet). This callback method is also throwing error now

  5. Parent: errorCallback() called (child lwc can now be referenced as it's now connected to DOM)

  6. Child: renderedCallback() called (reference to lightning-button is coming properly now as lightning-button is now connected to DOM)

  7. Parent: renderedCallback() called (child lwc can now be referenced here as well because it's now connected to DOM)

So that's the final series of events/callbacks we have for this post in our demo components.

We covered all the callback methods/lifecycle hooks of lwc in this post.

That's all for this tutorial everyone, I hope you liked it. Let me know your feedback in the comments down below.

Happy Trailblazing!!

Friday, 24 February 2023

Child to Parent communication using LWC Events | LWC Tutorial | Lightning Events in LWC

Hello Trailblazers, 

I recently posted the below video on SFDC Stop YouTube Channel where we learned how can we communicate from a child lwc to a parent lwc using events.

Tutorial Video

In this post, I'm going to share the code snippet we used in the above tutorial with a brief explanation of the same. You can watch the small ~9min video, I shared above to learn the concept in detail.

Let's have a look at the code snippets now!

Child LWC

First of all, we created a child lwc. This component will fire the event on a button click, which will be handled by the parent lwc. Let's have a look at the HTML and Js code of our child lwc one by one:

child.html

<template>
    <lightning-button label="Increase Count" onclick={increaseCount}></lightning-button>
</template>
As you can see above, we defined a lightning button with label Increase Count, this button will call the js method increaseCount() which will increase the value of a counter we'll define in our js and fire an event. The parent LWC will capture the event and will display the value of this counter along with some text received in the event body.

child.js

import { LightningElement } from 'lwc';

export default class Child extends LightningElement {

    count = 0;

    increaseCount() {
        this.dispatchEvent(new CustomEvent('increasecount', {
            detail: {
                message: 'Increased count to ' + (++this.count)
            }
        }));
    }
}
As you can see above, we defined a variable count whose initial value is 0. As we click the Increase Count button, this increaseCount() will be called. It'll dispatch a new event named increasecount and in the body of this event (which is an object), we defined a property named detail. Now, in this detail property, we can pass anything, it can be a string, an array, an integer, an object...anything.

For now, in the detail of this event, we're passing an object, which has a single property named message and the value of message is: Increased count to <increased value of count variable>. This means, each time this method is called, count variable will be incremented by 1 and the string message will be passed in event detail which has the updated count. For example, when the first time, this method is called, we will have the message Increased count to 1 in the event detail. Similarly, the second time this method is called when the button is clicked again, count variable will increase again by 1 and the message: Increased count to 2 will be passed in the event detail.

Our parent lwc will accept this event and will display the message. Let's have a look at that now!

Parent LWC

Let's start by looking at the html part again:

parent.html

<template>
    <lightning-card title={message}>
        <p class="slds-var-p-around_small">
            <c-child onincreasecount={updateMessage}></c-child>
        </p>
    </lightning-card>
</template>
In this component, first of all we defined a lightning-card with title equal to the message variable that we'll define in our js. Then, for the card body, we defined a paragraph with a small padding and within that paragraph, we called our child component. Now, we know that our child lwc will fire increasecount event when the button is clicked, so we're handling the same event as: onincreasecount={updateMessage}. This means: whenever this increasecount event is fired, it'll call the updateMessage() method defined in the js of our parent lwc.

For every event which is fired by a child lwc, you can handle it by adding a prefix on before it's name and then binding it to a method defined in your js. For example: here our event name is increasecount so we added the on prefix before event name and it became: onincreasecount and we binded it to our updateMessage() method defined in our js. This updateMessage() method will receive the same event that we fired from our child component. Let's checkout the js to understand how we're handling this event.

parent.js

import { LightningElement } from 'lwc';

export default class Parent extends LightningElement {

    message = 'Updated count will appear here!';

    updateMessage(event) {
        this.message = event.detail.message;
    }
}
As you can see in the above code snippet, we defined a message variable in our js. This is the same variable which is used as the value of title in our lightning card. The default value of this variable is Updated count will appear here! so by default the lightning card will display this message as you can see in the below screenshot:


We also defined a method updateMessage(event), this method will receive the same event which is fired by our child lwc in the parameter and will update the message variable with the value that is coming from the message property of our event detail object. Remember, we defined an object in the detail property of our event body, with a single property named: message whose value was Increased count to <increased value of count variable>? So, when we do: this.message = event.detail.message; we're basically saying, get the object defined in detail property of event body (event.detail) and then get the value of message property from that object (event.detail.message). We're assigning the value of this message property from event detail object to our message variable and this message variable is displayed as the title of our lightning card.

parent.js-meta.xml

I'm going to add this component to my homepage for the demo, so I've added a target named lightning__HomePage as shown below:
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>56.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__HomePage</target>
    </targets>
</LightningComponentBundle>

Demo

Now, as we click on the Increase Count button once, the child component's counter (count variable) will increment to 1, the value of message will be Increased count to 1, the lightning card title will update and the output for the same is shown below:


Similarly, as we click on this button again, the child component's count variable will update to 2, the message passed through the event will be Increased count to 2 and the same message will be displayed in the title of our lightning card as shown below:


I'm sharing a small screen recording below so that it's clear how the component is behaving in real time:


That's all for this tutorial everyone, I hope you liked it. Let me know your feedback in the comments down below.

Happy Trailblazing!!

Saturday, 10 December 2022

Introduction to Platform Events in Salesforce

Hello Trailblazers,

In this post we're going to learn about platform events. Platform Events are a part of salesforce's enterprise messaging platform. In short, we can say that a platform event is an event using which applications can communicate with each other inside and outside of salesforce. 

Therefore, it can be used to send messages/notifications from one app to another within salesforce and also we can send messages to other applications which are outside of salesforce, thereby using it as a means to integrate salesforce with other systems.

What is an Event-Driven Software Architecture?

In order to understand platform events, we should know what an event-driven software architecture looks like. It's described very well in the original salesforce documentation in detail. We're going to focus on some of the important stuff here.

An event driven architecture consist of an Event Bus i.e. the service which ensures the delivery of the event in the correct order 🚌. It consist of multiple Event Channels through which the event travel. We also have an Event Producer i.e. the application which will fire the event and an Event Consumer i.e. the application which will receive the event. Each event will have an Event Message which is the actual information the event is carrying. And That's All. As simple as that!

Why and When should I use platform events?

When a system/application request salesforce for the latest information of some records let's say by hitting the API, there is a separate connection established between the requestor application and salesforce. 

Let's consider a hypothetical scenario where you are creating/updating leads. When a condition is fulfilled (let's say personal info is updated) you need all the updated lead information to be synced with 5-10 external systems in near-real time

Now, for each such system (requestor application) there will be a separate connection to salesforce maybe via REST API (or any other mechanism) and this connection also depends on the availability of the service. Let's say if the service (i.e. salesforce api if the external system is requesting information) is not available for some time (maybe a few seconds) due to any reason, the requests to get the latest information will fail for some of the systems while the other systems will remain up to date. This leads to Inconsistency between receiver systems.

So, in this case, we can say:

  1. The availability of the service is critical

  2. To add (integrate with) a new system, we need to establish a separate connection

However, if we have an event based model, Salesforce (Event Producer) can fire a platform event - each time the lead record is created/updated and it's satisfying the condition. It's not necessary for salesforce to know who's consuming the event, there can be 5, 10, 15, 20..... consumers who would like to receive the same information. Even if an external system is not available for some time, the events fired will still remain in the event bus and can be received/consumed once the system is available again. If salesforce (producer system) is not available, it can fire the events once it's back and publish it to the event bus.

Therefore, we can say:

  1. The availability of service is not critical. If the producer is not available, it's fine as it'll publish the events to the bus when it's available again and similarly if the consumer is not available for some time, it can also retrieve the events from the same event bus when it's back

  2. Adding/integrating a new system is fairly easy as we don't need to establish a separate connection to the service (event producer). The new consumer can simply connect to the event bus and can start receiving events

  3. The only consideration/dependency between event producer and consumer here is the format of the event message content as it'll be the same for all consumers

Note: Each platform event message consist of a ReplayId using which we can identify the event. We can also replay the stream of events after a particular event using the replay id.

Types of Platform Events

There are two types of platform events:

  1. Standard Platform Events

  2. Custom Platform Events

Standard Platform Events

Standard platform events consist of some limited events that are pre-defined by salesforce. Most of these events are published in response to an action that occured in the org or to report errors. A good example of a standard platform event is BatchApexErrorEvent which can be fired by our batch class to report any errors that are encountered. You can implement the Database.RaisesPlatformEvents interface in your batch class to fire these events and capture them in your event subscriber to log details of the errors that are encountered.

A full list of standard platform events is available here. Apart from this list, the Change Data Capture Events (CDC) also comes under standard platform events and can be used to notify when a record is created, updated, deleted OR undeleted from recycle bin. We'll need a separate blog post to cover CDC in detail, let me know in the comments down below if you're interested in the same.

Custom Platform Events

Custom Platform Events are user-defined events that can be used to convey any specific information that you would like to publish using platform events. Defining a custom platform event is very similar to defining a custom object in salesforce. You can specify the name of the platform event, the field names and their types. There are also some limitations if we compare it to a salesforce objects:

  • These event records cannot be viewed in salesforce directly so we don't have a page layout for them.

  • Out of the CRUD operation, only CREATE and READ are valid for platform events. You CANNOT UPDATE or DELETE a platform event record.

  • Published event messages cannot be rolled back

  • The type of fields for platform event objects are limited. Only Checkbox, Date, Date/Time, Number, Text and Text Area (Long) fields are supported

All the above points, kind of makes sense as well, because once you fire a platform event record and it's published to the event bus, now it can be read by any subscriber so there is no point of updating or deleting an event which is already published. Also, rollbacking a platform event is also risky because the publisher is not aware of the subscribers, so we can't say which subscriber has already consumed it and which hasn't. Lastly, we don't really need fields like: lookup, master-detail and other field types in platform events which we have in custom objects.

Publishing Platform Events

We have two options for publishing platform events:

  1. Publish Immediately - Platform events are published as they're fired. They won't wait for the current transaction to complete successfully. A good use case can be if you want to create error log (custom object to store errors) records when an exception is thrown. You can fire a platform event with the error message and it's published immediately. Therefore, an error log record will be created when this platform event is received (a separate transaction) even when the actual transaction is rolled back because of an exception.

  2. Publish After Commit - Platform events are published only after a transaction commits successfully. Let's say you want to send an email when something happens, so you want to be sure that the transaction is completed successfully before you fire a platform event which is subscribed by a flow (let's say) and it sends an email. In this case, Publish After Commit is a better option.

Before we wrap up this post, I would like to tell you about some more important points about platform events:

  1. Platform Events can be High Volume or Standard Volume. In API version 45.0 and later, all new custom platform events are high volume by default, so we'll focus mainly on that. A big difference between high volume and standard volume platform events is: High Volume platform events are stored in the event bus for 3 days whereas Standard Volume platform events are stored in the event bus for 1 day. The higher retention time makes the integration more robust as the system can be down for some time (due to any issue), can be up again and receive the event messages it lost when it was down

  2. The order in which the events are published remains the same - as we specify at the time of publishing these events, when you're publishing multiple events in the same publish call. If you're publishing multiple events separately using different calls to publish(), the order is not guaranteed.

  3. Platform Events are suffixed with __e

  4. Each platform event message has a replay id using which it can be replayed from the event bus.

  5. Some important terms include: Event Bus, Event Channel, Event Producer, Event Consumer and Event Message (we discussed all of these above in the Event-Driven software architecture section)

Ok, enough of theory and this was the only theory I wanted to share before we deep dive into practical stuff - actually fire and subscribe to platform events which we'll do in the similar upcoming posts.

That's all for this tutorial. I hope you liked it, let me know your feedback in the comments down below. You can connect with my on Connections app by scanning the QR code given below:
Or search in the code scanner screen using my username: rahulmalhotra

Happy Trailblazing!!

Friday, 25 November 2022

Simplifying SOQL with Polymorphic Relationships

Hello Trailblazers,

In this post, we're going to learn about TYPEOF clause in SOQL using which we can query data that contains polymorphic relationships. I recently came across this Stackoverflow Question which was asked 7 years 3 months ago considering the time I am writing this post where the user has asked about "Unable to query FederationIdentifier on case owner" The question is pretty old but is still valid. You cannot query case owner's FederationIdentifier using a simple SOQL query on case object. Let's see why?

The Problem

I have a case record in my org as shown below:

Let's say you want to query the case subject, as well as the case owner's FederationIdentifier field. As you can see above, the owner of case is a person named User User and as we open this user's record, you can see below that the Federation ID field has a value 12345.

Well, if I tell you to write a SOQL query in order to get this federation id from case, maybe the first query that comes to your mind is something like this:
SELECT Subject, Owner.FederationIdentifier FROM Case WHERE Id = '5005D000008nxIkQAI'
Now, let's run this SOQL query and see the results:
As you can see above, we're getting this error:
SELECT Subject, Owner.FederationIdentifier FROM
                ^
ERROR at Row:1:Column:17
No such column 'FederationIdentifier' on entity 'Name'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.
This error is coming because the OwnerId field on Case is a polymorphic relationship field which can refer to a Group or a User. In case of a User, we'll have the FederationIdentifier field but in case of a Group, we won't have it. Therefore, we can say that, while you're executing this query, salesforce is not sure if the owner is a Group or a User and therefore, the query could not be executed.

Ok, got the issue now but how to resolve it? Should I create a formula field on case to get the case owner's federation id? You can do it, but let's also see how we can query it!

The Solution

Salesforce has provided an optional TYPEOF clause that can be used in a SOQL query including polymorphic relationships. TYPEOF clause is available in API version 46.0 or LATER. Let's solve our issue first and then we'll see the format of TYPEOF clause in detail. In order to query FederationIdentifier from case owner we can write a query as follows:
SELECT Id, TYPEOF Owner WHEN User Then FederationIdentifier END FROM Case WHERE Id = '5005D000008nxIkQAI' AND Owner.Type = 'User'
As you can see above, I've included the statement TYPEOF Owner WHEN User Then FederationIdentifier END. It's basically saying that when the Case Owner's type is User, then query FederationIdentifier field. I've also added a Type filter for case owner at the end of query: AND Owner.Type = 'User'.  The result for this query is given below:
Starting SFDX: Execute SOQL Query...

07:30:06.358 sfdx force:data:soql:query --query SELECT Id, TYPEOF Owner WHEN User Then FederationIdentifier END FROM Case WHERE Id = '5005D000008nxIkQAI' AND Owner.Type = 'User'
Querying Data... done
 ID                 OWNER                                                              
 ────────────────── ────────────────────────────────────────────────────────────────── 
 5005D000008nxIkQAI {                                                                  
                      "attributes": {                                                  
                        "type": "User",                                                
                        "url": "/services/data/v56.0/sobjects/User/0055D000005BiagQAC" 
                      },                                                               
                      "FederationIdentifier": "12345"                                  
                    }                                                                  
Total number of records retrieved: 1.
07:30:10.900 sfdx force:data:soql:query --query SELECT Id, TYPEOF Owner WHEN User Then FederationIdentifier END FROM Case WHERE Id = '5005D000008nxIkQAI' AND Owner.Type = 'User'
 ended with exit code 0
As you can notice above, we're getting the related Owner record with FederationIdentifier field in response. Let's say you want to get the FederationIdentifier using this query in apex, you can refer to the below code:
List<Case> caseList = [SELECT Id, TYPEOF Owner WHEN User Then FederationIdentifier END FROM Case WHERE Id = '5005D000008nxIkQAI' AND Owner.Type = 'User'];
User caseOwner = caseList[0].Owner;
System.debug(caseOwner.FederationIdentifier);
You'll get the FederationIdentifier from case owner in the debug as shown below:
Awesome! Now that we've solved the issue, let's learn more about the TYPEOF clause. The TYPEOF statement in SOQL is of the format:

TYPEOF <PrimarySObjectField> WHEN <PossibleRelatedObject1APIName> THEN < PossibleRelatedObject1FieldsListToQuery> WHEN <PossibleRelatedObject2APIName> THEN < PossibleRelatedObject2FieldsListToQuery>.... ELSE <LisfOfFieldsToQueryWhenAllAboveWHENConditionsAreFalse> END

It's similar to switch case in apex. For example, we can understand our TYPEOF clause as: TYPEOF Owner WHEN User Then FederationIdentifier END, here we checked if the TYPEOF Owner (primary sObject field) WHEN (is equal to) User (possible related object) THEN FederationIdentifier (field to query) END

Notice that the ELSE part is optional and we can have multiple WHEN-THEN combinations to check for multiple objects that are possible considering our polymorphic relationship field.

Another example can be WhatId field of Task object. We know that this field can be linked with multiple sObjects. Let's consider Account, Opportunity and Case for now and query different fields from each of these objects. If the task is not linked to any of the Account, Opportunity or Case object, I'll just query the name field of that related object's record.

Let's create 4 tasks: First one related to an account, second one related to an opportunity, third one related to a case and fourth one related to a product. Have a look at the tasks I created below:
Task linked to account
Task linked to account
Task linked to opportunity
Task linked to opportunity
Task linked to case
Task linked to case
Task linked to product
Task linked to product
My apex code with the SOQL query is as follows:
List<Task> taskList = [SELECT Subject, 
    TYPEOF What 
    	WHEN Account THEN Type, NumberOfEmployees 
    	WHEN Opportunity THEN Amount, CloseDate 
    	WHEN Case THEN Subject, CaseNumber 
    	ELSE Name 
    END 
FROM Task];
for(Task taskRecord: taskList) {
    System.debug(taskRecord.Subject + ' -> ' + taskRecord.What);
}
As you can see above, I am querying Type and NumberOfEmployees when the related object is Account, Amount and CloseDate when the related object is Opportunity, Subject and CaseNumber when the related object is Case, otherwise, I'm just querying Name of the related record. The result when the above apex code is executed is shown below:
As you can see above, we're getting all the tasks with relevant related fields from each of Account, Opportunity and Case. In case of product, we're getting the Name field. 

If you remember, in our initial query on Case (SELECT Id, TYPEOF Owner WHEN User Then FederationIdentifier END FROM Case WHERE Id = '5005D000008nxIkQAI' AND Owner.Type = 'User'), we also added WHERE condition on Type field as: AND Owner.Type = 'User'. This is another good way to filter records while dealing with the specific type of related objects we would like to consider. You can even skip the ELSE part if you've already added WHEN-THEN for each related object and the corresponding conditions in WHERE clause.

So that's how you can Simplify SOQL with Polymorphic Relationships. You can have a look at this detailed article from salesforce official documentation to learn more about TYPEOF clause. Before coming to the end of this blog, let's talk about some of the important considerations of TYPEOF clause as well.

Considerations for TYPEOF

I am highlighting some of the considerations of TYPEOF clause here, you can checkout the full list in official documentation.
  • TYPEOF cannot be used with with queries that don't return objects such as COUNT() and aggregate queries
  • TYPEOF cannot be used in SOQL used in Bulk API
  •  TYPEOF cannot be used in semi-join query. Semi Join query is a query used to filter the original query - For example: SELECT Name FROM Contact WHERE AccountId IN (SELECT Id FROM Account), here the text in bold is semi-join query
  • TYPEOF cannot be used with a relationship field whose relationshipName or namePointing attribute is false.
You can checkout the namePointing and relationshipName attribute of a field very easily. For example, consider the below code:
System.debug(Case.OwnerId.getDescribe());
Here, we're getting the DescribeFieldResult for OwnerId field of Case object. The output for the same is provided below:
Schema.DescribeFieldResult[
    getByteLength=18;
    getCalculatedFormula=null;
    getCompoundFieldName=null;
    getController=null;
    getDataTranslationEnabled=null;
    getDefaultValue=null;
    getDefaultValueFormula=null;
    getDigits=0;
    getFilteredLookupInfo=null;
    getInlineHelpText=null;
    getLabel=Owner ID;
    getLength=18;
    getLocalName=OwnerId;
    getMask=null;
    getMaskType=null;
    getName=OwnerId;
    getPrecision=0;
    getReferenceTargetField=null;
    getRelationshipName=Owner; // <---- Relationship Name
    getRelationshipOrder=null;
    getScale=0;
    getSoapType=ID;
    getSobjectField=OwnerId;
    getType=REFERENCE;
    isAccessible=true;
    isAggregatable=true;
    isAiPredictionField=false;
    isAutoNumber=false;
    isCalculated=false;
    isCascadeDelete=false;
    isCaseSensitive=false;
    isCreateable=true;
    isCustom=false;
    isDefaultedOnCreate=true;
    isDependentPicklist=false;
    isDeprecatedAndHidden=false;
    isDisplayLocationInDecimal=false;
    isEncrypted=false;
    isExternalId=false;
    isFilterable=true;
    isFormulaTreatNullNumberAsZero=false;
    isGroupable=true;
    isHighScaleNumber=false;
    isHtmlFormatted=false;
    isIdLookup=false;
    isNameField=false;
    isNamePointing=true; // <---- Name Pointing
    isNillable=false;
    isPermissionable=false;
    isQueryByDistance=false;
    isRestrictedDelete=false;
    isSearchPrefilterable=false;
    isSortable=true;
    isUnique=false;
    isUpdateable=true;
    isWriteRequiresMasterRead=false;
]
If you notice, the relationshipName for this field is Owner and namePointing attribute is also true. A smaller documentation on TYPEOF clause is also available here.

That's all for this tutorial. I hope you liked it, let me know your feedback in the comments down below. You can connect with my on Connections app by scanning the QR code given below:
Or search in the code scanner screen using my username: rahulmalhotra

Happy Trailblazing!!

Saturday, 19 November 2022

Create modals using the new LightningModal component (Winter '23 Release)

Hello Trailblazers,

In this post we're going to learn about the new LightningModal component that can be extended by any lwc which you would like to use as a modal. Salesforce has provided 3 helper components to create a modal:

  1. lightning-modal-header
  2. lightning-modal-body
  3. lightning-modal-footer

Let's create a testModal component and try to use these 3 tags to see what we get as a result.

Creating a simple testModal LWC

testModal.js-meta.xml

I am specifying the meta file first of all so that we can focus on our html and js files throughout this tutorial. Our meta file is pretty simple as shown below:
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>55.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__HomePage</target>
    </targets>
</LightningComponentBundle>

As you can see above, I've specified lightning__HomePage as a target so that we can embed this component in our homepage. This is NOT REALLY REQUIRED as we're going to use our component as a modal, but I'm just keeping this for now, so that we can see what the helper components render for us. I've also marked isExposed as true to make this component available in our app builder.

testModal.html

The simplest HTML content for our modal is shown below:
<template>
    <lightning-modal-header>Test Modal Header</lightning-modal-header>
    <lightning-modal-body>Test Modal Body</lightning-modal-body>
    <lightning-modal-footer>Test Modal Footer</lightning-modal-footer>
</template>

We've only called the header body and footer tags with some content in them. I embedded our component on the homepage. Let's see the output below:


As you can see above, we're having 3 different sections: Header, Body and Footer coming from our lightning-modal-header, lighnting-modal-body and lightning-modal-footer tags.

It's better and easier to use label attribute of lightning-modal-header to specify the heading for our modal header. You can just update the lightning-modal-header tag as shown below:
<lightning-modal-header label="Test Modal Label">Test Modal Header</lightning-modal-header>
Let's see the output of this change as well:
You might not see any difference here but it'll be more clear as we'll start using this component as a modal.

Okay that's fine but how do I actually use this as a modal?

In order to open it as a modal, we'll create another lwc named: useModal but first of all let's update the js file of this testModal as well:

testModal.js

import LightningModal from 'lightning/modal';

export default class TestModal extends LightningModal {}
Notice the two changes I did above which makes it different from other LWCs:

  1. Instead of the statement, import { LightningElement } from 'lwc'; it's importing LightningModal from lightning/modal.
  2. Instead of extending LightningElement, our component is extending LightningModal using extends LightningModal.

That's all for our testModal for now, let's create our useModal component now:

useModal.js-meta.xml

Starting with the meta file, it's the exact same as we had above because we'll be embedding this component as well in our homepage:
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>55.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__HomePage</target>
    </targets>
</LightningComponentBundle>
Note: You can remove the lightning__HomePage target from your testModal now and remove it from the hompage, as we'll be using it as a modal now

Now, let's move on to the HTML part of useModal LWC:

useModal.html

<template>
    <lightning-card title="Use Modal">
        <p class="slds-var-p-horizontal_small">
            <lightning-button label="Open Modal" onclick={openModal}></lightning-button>
        </p>
    </lightning-card>
</template>
As you can see above, the code is pretty simple. We have a lightning-card (for better UI) and inside it, we have a paragraph which contains a lightning-button with label Open Modal. This button will call our js function openModal as it's clicked by the user. It's time to move on to our js file now!

useModal.js

For now, we'll just try to open our testModal:
import { LightningElement } from 'lwc';
import TestModal from 'c/testModal';

export default class UseModal extends LightningElement {

    openModal() {
        TestModal.open();
    }
}
As you can see above, we've imported our TestModal using the import statement and called open() on it in order to open our modal component. Let's see how it works!

This is how our useModal component looks like on the homepage:
I just embedded it before our testModal component. As we click on Open Modal button, we get the modal as shown below:
I hope that the usage of label attribute in lightning-modal-header component is clear now. You should use label attribute to specify a label for your modal. If you want to have any custom HTML like: a button or something else, that can come between our lightning-modal-header tags.

Isn't it amazing? just a few lines of code and you have your modal ready. The close button that you see on the top right works perfectly and your modal will be closed automatically as you click on that button.

Note: You can also close the modal by pressing the ESC key.

Resizing our modal

There are some properties that you can pass in the open function. One of the property is size which supports small, medium and large values. By default, the size is medium. You can pass any sizes out of small, medium or large. The output of all 3 are shown below.

To have a small sized modal, you can just do:
    TestModal.open({
        size: 'small'
    });
And you'll have the below output:
For a medim sized modal, you can just do nothing (as the default size is medium) or pass in the medium value as shown below:
    TestModal.open({
        size: 'medium'
    });
And you'll have the below output:
Similarly, for a large modal, you can just do:
    TestModal.open({
        size: 'large'
    });
And you'll have the below output:

Defining a Custom Close Button for our Lightning Modal

A very common requirement is to have two buttons: Cancel and Save in our modal footer. If someone clicks on Cancel, we'll just close the modal and if someone clicks on Save, we'll save the information and then close our modal. Let's see how we can implement that. It's time to update our testModal.html
<template>
    <lightning-modal-header>Test Modal Header</lightning-modal-header>
    <lightning-modal-body>Test Modal Body</lightning-modal-body>
    <lightning-modal-footer>
        <lightning-button label="Cancel" onclick={closeModal} class="slds-var-p-right_x-small"></lightning-button>
        <lightning-button label="Save" variant="brand" onclick={save}></lightning-button>
    </lightning-modal-footer>
</template>
As you can see above, I've removed the Test Modal Footer text which was present in between lightning-modal-footer tags and I added two lightning buttons instead: one for cancel and another for save. The Cancel button is calling closeModal() from our js and have an extra small right padding so that the two buttons don't stick to each other. The Save button is having a variant as brand and is calling save() from our js file. The updated testModal.js file is given below:
import LightningModal from 'lightning/modal';

export default class TestModal extends LightningModal {

    closeModal() {
        this.close();
    }

    save() {
        console.log('We will save the data and then close modal');
    }
}

You might have noticed above that I added two methods here:
  1. closeModal() which is calling close() here as this.close(). This close() is defined in the LightningModal component which we're extending and it'll close the modal.
  2. save() which is doing nothing as of now but adding a statement to the console that: We'll save the data and then close modal.

The updated modal is shown below:
As you can see, we have two buttons now: Cancel and Save. As you click on Cancel button, the modal will be closed. If you click on Save button, there will be a message in console but the modal will not close.

Let's say the user clicks on save and then while the information is being saved, the user clicks on Close button at the top right and the modal is closed, how do you ensure that the information was saved successfully? In this scenario, preventing the user from accidentally closing the modal is important, let's see how we can do that!

Prevent the user from closing Lightning Modal using disableClose attribute

For now, we'll consider a scenario that our save operation takes 5 seconds. So, we'll disable the Close operation for 5 seconds when the save button is clicked. We're not dealing with apex in this tutorial, so we'll just use setTimeout() to simulate our server call. Our testModal.js is updated as shown below:
import LightningModal from 'lightning/modal';

export default class TestModal extends LightningModal {

    closeModal() {
        this.close();
    }

    save() {
        console.log('We will save the data and then close modal');
        this.disableClose = true;
        const that = this;
        setTimeout(() => {
            console.log('Information saved! You can now close the modal');
            that.disableClose = false;
        }, 5000);
    }
}
Inside the save(), we're setting disableClose to true. Then we're calling setTimeout() (you can consider it similar to calling any apex method and waiting for the response). In setTimeout(), we can pass a function and specify the time (in milliseconds) after which that function will be called. Here, we've specified the time as 5000 milliseconds i.e. 5 seconds and after 5 seconds the function passed will be called. That function will print the text Information saved! You can now close the modal in the console and set disableClose to false again. This can be considered - as our save operation is successful and we want to allow the user to close the modal now.
Notice the above image, this is how our modal looks like. Have a look at the cross in the red rectangle, it's enabled for now. As I click Save button the modal will look like as shown below:
As you can see above, the cross icon is disabled, this means we cannot close our modal and it'll automatically be enabled after 5 seconds. Even if you click on Cancel button the modal will not close because the call this.close() will not work. It's even better if we can disable the Save and Cancel buttons as well, until the save operation is performed, so that the user doesn't click these buttons again and again. Let's do that quickly!

For this, I am going to add disabled={disableClose} to both Save and Cancel buttons of my modal so that these buttons are disabled when disableClose is true. Below is the updated testModal.html:
<template>
    <lightning-modal-header label="Test Modal Label">Test Modal Header</lightning-modal-header>
    <lightning-modal-body>Test Modal Body</lightning-modal-body>
    <lightning-modal-footer>
        <lightning-button label="Cancel" onclick={closeModal} class="slds-var-p-right_x-small" disabled={disableClose}></lightning-button>
        <lightning-button label="Save" variant="brand" onclick={save} disabled={disableClose}></lightning-button>
    </lightning-modal-footer>
</template>
Notice the disabled attribute applied to both the buttons above. The updated output as I click on Save button of my modal is given below:
As you can see above, all my buttons are disabled now when I clicked Save, they'll be enabled back together after 5 seconds (OR you can do it after your apex call is successful in a real implementation). You can also call the close() again once your apex call is successful so that the modal get closed automatically. In our case, it can be after 5 seconds when we set disableClose back to false as shown below:
import LightningModal from 'lightning/modal';

export default class TestModal extends LightningModal {

    closeModal() {
        this.close();
    }

    save() {
        console.log('We will save the data and then close modal');
        this.disableClose = true;
        const that = this;
        setTimeout(() => {
            console.log('Information saved! Closing the modal...');
            that.disableClose = false;
            that.close();
        }, 5000);
    }
}
Notice that we called close() after we set disableClose to false in our save(). Now, the modal will close automatically after 5 seconds as we click on the save button.

So that's how you can create modals using the new LightningModal component. I hope you have a good idea of how it works and you can go ahead and create your own modals now. Some of the information that we didn't cover here are:
  • Custom styling of modal's header, footer and body
  • Passing information to modal while opening the modal (Just create an @api attribute in your testModal and provide value to it in the TestModal.open() like we did for size attribute)
  • Passing information back to component that called the modal when the modal is closed (You can pass the value inside close() as a parameter and have a then() linked to TestModal.open() which will receive the return value as: TestModal.open({...params}).then((valuePassedToCloseFunction) => {})
  • Firing an event from modal and capturing it in parent.
You can refer to the official documentation or comment below and let me know if you would like to learn about these in detail and I'll create another post. You can also find my contact details on Connections (username: rahulmalhotra)

So that's all for this tutorial. I hoped you liked it, let me know your feedback in the comments down below.

Happy Trailblazing!!