Monday 20 November 2017

Animated Lightning Progress Bar

Animated Lightning Progress Bar

Introduction

The Salesforce Lightning Design System has a progress bar component, which can be used to communicate how far through a process the user is, or how close to achieving their target audience they are in BrightMedia. Typically this will be wired up to an attribute so that it updates automatically when the attribute value changes, for example:

<aura:application extends="force:slds" >
    <aura:attribute name="value" type="Integer" default="25" />
    <div class="slds-m-around_small">
        <div class="slds-text-heading_large slds-m-bottom_small">Progress Bar Demo</div>
        <div class="slds-m-bottom_large">
            <label>Enter value : <ui:inputNumber value="{!v.value}"/></label>
        </div>
        <div>
            <div style="width:25%" class="slds-progress-bar slds-progress-bar_circular slds-progress-bar_large"
                    aria-valuemin="0" aria-valuemax="100" aria-valuenow="{!v.value}" role="progressbar">
                <span class="slds-progress-bar__value" style="{! 'width:  ' + v.value + '%;'}">
                    <span class="slds-assistive-text">{!'Progress: ' + v.value + '%'}</span>
                </span>
                <div class="slds-text-align--center"><ui:outputNumber value="{!v.value}"/>
                   /
                <ui:outputNumber value="100"/></div>
            </div>
        </div>
    </div>
</aura:application>

which jumps the progress bar to the specified value:

while this works fine , it's not the greatest user experience. When a progress bar updates I prefer to see an animated version where it gradually makes it's way to the final value. There's no difference functionality-wise, but it just looks better to me.

The Animator

Animating a progress bar in JavaScript is simply a matter of making small changes to move between the current and desired value, typically via a timer that fires a function every ’n’ milliseconds to advance the value by a small amount. When using Lightning Components this is a little more tricky as the function executed by the timer is modifying the component outside of the framework lifecycle. In the revised app, when the user changes the desired value this is stored in a separate attribute and a controller function is executed:

<aura:application extends="force:slds" >
    <aura:attribute name="value" type="Integer" default="25" />
    <aura:attribute name="inputVal" type="Integer" default="25" />
    <aura:attribute name="timeoutRef" type="object" />
    <div class="slds-m-around_small">
        <div class="slds-text-heading_large slds-m-bottom_small">Progress Bar Demo</div>
        <div class="slds-m-bottom_large">
            <label>Enter value : <ui:inputNumber value="{!v.inputVal}" change="{!c.valueChanged}" /></label>
        </div>
        <div>
            <div style="width:25%" class="slds-progress-bar slds-progress-bar_circular slds-progress-bar_large"
                 aria-valuemin="0" aria-valuemax="100" aria-valuenow="{!v.value}" role="progressbar">
                <span class="slds-progress-bar__value" style="{! 'width:  ' + v.value + '%;'}">
                    <span class="slds-assistive-text">{!'Progress: ' + v.value + '%'}</span>
                </span>
                <div class="slds-text-align--center"><ui:outputNumber value="{!v.value}"
                    /
                >/<ui:outputNumber value="100"/></div>
            </div>
        </div>
    </div>
</aura:application>

following best practice, the controller method simply delegates to the associated helper

({
	valueChanged : function(component, event, helper) {
        helper.valueChanged(component, event);
	}
})

which does the actual work :

({
    valueChanged : function(cmp, ev) {
        var times=0;
        var current=cmp.get('v.value');
        var final=cmp.get('v.inputVal');
        var increment=1;
        if (final<current) {
            increment=-1;
        }
        var self=this;
        var timeoutRef = window.setInterval($A.getCallback(function() {
            if (cmp.isValid()) {
                var value=cmp.get('v.value');
                value+=increment;
                if (value==final) {
                    window.clearInterval(cmp.get('v.timeoutRef'));
                    cmp.set('v.timeoutRef', null);
                }
                cmp.set('v.value', value);
            }
        }), 100);
        cmp.set('v.timeoutRef', timeoutRef);
    }
})

the first part of the helper function simply captures the start and end values and figures out if we need to increment or decrement from the current value.  Next the timer is set up to repeat every 100 milliseconds. As the function executed by the timer changes the app component attributes, I have to wrap it in a $A.getCallback function call, which ensures that the lightning components framework rerenders the markup. Once the current value equals the desired final value, the timer is cleared otherwise it will fire forever more.

Change values with care

Refreshing the app now animates the progress bar to apply the changed value. Incrementing by 1 is probably overkill, especially if you are dealing with values of hundreds of thousands for example. In this situation I’d simply decide how many “jumps” I wanted to apply to the progress bar, divide the difference between the current and desired value by the number of jumps and then add the result to the value each time the timer fired.

Related posts