Users of today’s apps and websites expect a lot from their user interfaces. Gone are the days where a basic design and the ability to complete a task were enough to hook a user on your app. Now, free apps are a dime a dozen, and competition for a space on your user’s home screen or browser is intense. In order for your Salesforce Community or App to meet your user’s baseline expectations, you need to hold up against all the other apps they use day-in and day-out.
In today’s blog post (and future posts), I hope to share a lot of the techniques that we use at 7Summits to provide some of these enhanced user experiences using Salesforce’s Lightning Experience and Community Cloud. We’ll start with an example modal animation.
First, the Easy Way — a Basic On/Off Modal
The easiest, and probably most common method of showing/hiding any component in Lighting is using an attribute to show/hide the HTML that renders that component. In this case, for our foundational modal, we can use an isOpen
Boolean attribute that component can use to show/hide the modal quickly. It kind of looks like this:
Let’s set it up. Below is our basic modal component — note the isOpen
attribute, and some basic HTML that frames out the modal, container, and backdrop behind the modal. We’re also passing title
and body
attributes, and registering an onclose
handler so we can quickly control the isOpen
toggle in whatever parent component we use.
<!-- Modal.cmp -->
<aura:component description="Modal">
<aura:attribute name="isOpen" type="Boolean" default="false" />
<aura:attribute name="title" type="String" /> <aura:registerEvent name="onclose" type="c:Modal_Event" /> <aura:if isTrue="{!v.isOpen}">
<div class="modal_container slds-align_absolute-center">
<div class="modal_backdrop"></div>
<article class="modal">
<header class="modal_header">
<h1>{!v.title}</h1>
<a class="modal_close" onclick="{!c.closeModal}"><lightning:icon iconName="utility:close" size="xx-small" alternativeText="Close Modal" /></a>
</header>
<div class="modal_body">
{!v.body}
</div>
</article>
</div>
</aura:if>
</aura:component>
So now we just need a short controller method to handle that onclick
attribute, which fires the onclose
event:
/* ModalController.js */
({
closeModal: function(component, event, helper) {
let closeEvent = component.getEvent('onclose');
closeEvent.setParams({
type: 'close'
});
closeEvent.fire();
}
})
That’s pretty much it, with the exception of some styling. Let’s try this:
.THIS {
z-index: 1000;
}
.THIS,
.THIS .modal_backdrop {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.THIS .modal_backdrop {
background: rgba(220,220,220,.9);
}
.THIS .modal {
width: 100%;
max-width: 420px;
background: #fff;
border-radius: 10px;
box-shadow: 0 10px 10px -5px rgba(0,0,0,.1);
position: relative;
margin: auto;
}
.THIS .modal_header {
background: aliceblue;
padding: 15px 40px 15px 20px;
position: relative;
border-radius: 10px 10px 0 0;
}
.THIS .modal_body {
padding: 20px;
}
.THIS .modal_close {
position: absolute;
right: 20px;
top: 15px;
}
You can view the entire Basic Modal component on my GitHub.
Now, our parent component uses an onclose
handler to set the isOpen
attribute to false. This hides the modal by doing something like the following:
<!-- Parent.cmp -->
<div>
<p>
<lightning:button type="button" variant="brand" onclick="{!c.openModal}" label="Open Basic Modal" />
</p><c:Modal_Basic isOpen="{!v.isOpen}" onclose="{!c.closeModal}" title="Basic Modal">
<p>This is a basic modal.</p>
</c:Modal_Basic>
</div>
You could handle the c.openModal
and c.closeModal
controller functions like so:
/* ParentController.js */
({
openModal: function(component, event, helper) {
component.set("v.isOpen", true);
},
closeModal: function(component, event, helper) {
component.set("v.isOpen", false);
}
})
Well, that’s all we need for a basic modal. (By the way, the code referenced in our Parent
component and controller won’t change for the rest of this post.)
Next, let’s add a transitional state using CSS animation.
A First Attempt At Adding In CSS Transitions
So, we have the basics, and, with a single isOpen
attribute, it’s very easy to deal with opening and closing the modal, which is awesome. Let’s keep the that single isOpen
attribute. However, our modal is lacking a little in the UX/UI department. To start, let’s try and add some animation when opening with CSS.
Component Additions
Let’s go back to our Modal.cmp
component and make a single modification by adding an openClass
attribute. This is a hook for our CSS animation.
<!-- Modal.cmp -->
<aura:component description="Modal - Basic Functionality">
<aura:attribute name="isOpen" type="Boolean" default="false" />
<aura:attribute name="title" type="String" />
<aura:attribute name="openClass" type="String" default="" /><aura:registerEvent name="onclose" type="c:Modal_Event" />
<aura:handler name="change" value="{!v.isOpen}" action="{!c.handleIsOpenToggle}" /><aura:if isTrue="{!v.isOpen}">
<div class="{!'modal_container slds-align_absolute-center ' + v.class + ' ' + v.openClass}">
<div class="modal_backdrop"></div>
<article class="modal">
...
</article>
</div>
</aura:if>
</aura:component>
With a dynamic class created with openClass
, we can use that as a hook for our CSS animations. The base state of the class tied to the isOpen
property, and we can toggle it by creating the change
handler for v.isOpen
. That handler calls a c.handleIsOpenToggle
method in our controller that does the work of actually adding the class. A simple way to do it is like as follows:
/* ModalController.js */
({
handleIsOpenToggle: function(component, event, helper) {
let isOpen = component.get("v.isOpen");
if (isOpen) {
component.set("v.openClass", 'is-open');
} else {
component.set("v.openClass", '');
}
},
...
})
CSS for the Animation
So, now successfully adding a class to our modal based on the isOpen
state. Now, let’s try to add the CSS animation. Here, I’ve only showed the changed lines:
.THIS .modal_backdrop {
...
transition: opacity .25s linear;
}
.THIS.is-open .modal_backdrop {
opacity: 1;
}
.THIS .modal {
...
transform: translateY(-20px);
opacity: 0;
transition: transform .5s linear, opacity .5s linear;
}
.THIS.is-open .modal {
transform: translateY(0px);
opacity: 1;
}
This is the CSS we need for now, which adds an animated state on both the backdrop and the modal itself.
However, if you try this out as is, there’s a problem: there is still no visible animation. That’s because the openClass
is added immediately upon the toggle of the state, which timed exactly to when the modal HTML is added to the component. That means no animation, because the modal is never displayed on the page without the is-open
class. We can fix that with very short setTimeout
JS function. Let’s add that to our controller.
handleIsOpenToggle: function(component, event, helper) {
const isOpen = component.get("v.isOpen");
if (isOpen) {
setTimeout($A.getCallback(function() {
component.set("v.openClass", 'is-open');
}), 10);
} else {
setTimeout($A.getCallback(function() {
component.set("v.openClass", '');
}), 10);
}
},
The setTimeout
is a pretty standard JS function. Note that we also add a $A.getCallback
to the callback so that the Lightning framework can properly rerender the component.
Now, if you make the updates to the modal, you’ll see the animation when the modal opens. However, we now have a slightly different issue than before when we close the modal — the isOpen
toggle removes the HTML for the modal before the CSS animation can even take place. We’re going to need to adjust our technique to better handle this type of closing animation.
Finally, the Elegant Way — Expanding to Four Fluid States of the States of the Component
Ok, so the two single states that the boolean isOpen
provides us doesn’t appear to be enough. Let’s think about this a bit and try a slightly different approach. Really, an opening and closing modal goes through a bit of a cycle, that consists of four basic states, as shown below:
The single isOpen
toggle works, and makes handling the state of the modal in a parent component very easy, so we don’t want to change that. We need to expand the modal a bit to handle these 4 states, however. This won’t be too difficult, as we have laid some of the groundwork with our previous attempt with the setTimeout
function.
Here we go. First thing to do is get rid of the openClass
attribute. We’ll handle the creation of the class based off of a couple of new attributes we’ll add:
<!-- Modal.cmp -->
...
<aura:attribute name="openState" type="String" default="closed" />
<aura:attribute name="openDelay" type="Integer" default="500" />
...
We’ll use openState
instead of openClass
, and set it to a default of closed
, which lines up with our default isOpen
value of false
. I’ve also added an Integer openDelay
, and defaulted it to 500, which we’ll use to define the length in milliseconds of the closing
and opening
transition states.
Next, in our component, instead of removing the HTML for the modal when isOpen
is false
, we remove it when the openState
is closed
. It’s a quick change to start the modal with a line like this:
<!-- Modal.cmp -->
<aura:if isTrue="{!v.openState != 'closed'}">
<div class="{!'modal_container slds-align_absolute-center modal-' + v.openState}">
...
</div>
</aura:if>
Now, let’s adjust our change
handler to cycle through the states after our defined delay, so our new controller looks a bit like this:
handleIsOpenToggle: function(component, event, helper) {
const isOpen = component.get('v.isOpen');
const openDelay = component.get('v.openDelay');
if (isOpen) {
component.set('v.openState', 'opening');
setTimeout($A.getCallback(function() {
component.set('v.openState', 'open');
}), 1);
} else {
component.set('v.openState', 'closing');
setTimeout($A.getCallback(function() {
component.set('v.openState', 'closed');
}), openDelay);
}
},
So, our toggle takes a look at the isOpen
value, and cycles through the appropriate opening/closing states as needed. The closed
and closing
states now work perfectly, and is an exact representation of our cycles. The opening
and open
states have a bit of oddity to them — note the 1
value for the setTimeout
call, instead of using openDelay
. This goes back to the same reasons as above, where the CSS animation needs a before and an after state to show the transition. Speaking of which, here’s our revised CSS:
.THIS .modal_backdrop {
...
transition: opacity .25s linear;
}
.THIS.modal-opening .modal_backdrop,
.THIS.modal-closing .modal_backdrop {
opacity: 0;
}
.THIS.modal-open .modal_backdrop {
opacity: 1;
}
.THIS .modal {
...
transform: translateY(-20px);
opacity: 0;
transition: transform .5s linear, opacity .5s linear;
}
.THIS.modal-opening .modal {
transform: translateY(-20px);
opacity: 0;
}
.THIS.modal-open .modal {
transform: translateY(0px);
opacity: 1;
}
.THIS.modal-closing .modal {
transform: translateY(20px);
opacity: 0;
}
That should do it! Note we haven’t done anything fancy with renderers yet in Lightning, that’s because we’re not doing any direct DOM manipulation. In my next post, we’ll expand on this technique a bit further, using the openStates
technique along with a render handler on an accordion, so we can animate the height of the accordion appropriately.
Even so, there’s a variety of improvements and adjustments that could still be made, from styling, to z-index fixes. Also, one quick thing to note — potentially, this basic modal setup can cause some z-index issues with other components on the page, but I’ll address global modals in a future blog post. We can address all of that in future blog posts — for now, view all the code along with an example here: