Lightning component code optimization - capture helper enqueued callback within controller method
Sounds to me like a job for Promises. In case you're not familiar with them, they allow you to create a sequence of actions which would normally result in a tortuous number of callbacks.
Caveat: I've never used them in Lightning, but they are mentioned in the docs, see https://developer.salesforce.com/docs/atlas.en-us.lightning.meta/lightning/js_promises.htm
What I would do is wrap the helper.getDataFromServer(....);
in a Promise, and wrap component.set("v.theBooleanValue",false);
.
Then, where you had this:
eventHandler : function(....){
component.set("v.IdValue"....);
helper.getDataFromServer(....);
//ON COMPLETE of the above then execute setTheLabel.
component.set("v.theBooleanValue",false); //This is important needs to be false to control display at this point
}
You could have something like this:
eventHandler : function(....){
component.set("v.IdValue"....);
helper.getDataFromServerPromise(....).then(
// resolve handler
$A.getCallback(function(result) {
component.set("v.theBooleanValue",false); //This is important needs to be false to control display at this point
return anotherPromise();
}),
// reject handler
$A.getCallback(function(error) {
console.log("Promise was rejected: ", error);
return errorRecoveryPromise();
})
);
}
In response to Eric saying that promises are over-complicated here: I'd say promises are a pretty big hammer to crack this particular nut, and they do come with a bit of a learning-curve. However, I took the spirit of the question to be: how do we solve problems like this in the best way. Rather than necessarily how to solve this particular issue.
It lead me to build a demo component so that I could have a play with promises in Lightning, as I'm sure I'll use it one day.
What I've built is a component which appends 'a's onto a string by going to an Apex controller. In the helper for this component, I have the normal implementation of this, but then extended it to take resolve
and reject
functions so that it can act like a promise. Since javascript doesn't care if you skip some arguments, you can still call it without those functions and it will act in the normal way. Or, you can wrap it in a promise.
Component:
<aura:component controller="PromisesDemoController" implements="flexipage:availableForAllPageTypes" access="global">
<aura:attribute name="message" type="String" default="a" description="The string we're appending to"/>
<aura:attribute name="successPercentage" type="Decimal" default="0.75" description="The chance that the apex code will succeed. 0 is definite fail, 1 is definite success" />
<aura:attribute name="usePromises" type="Boolean" default="true" description="Flag to switch between promise/non-promise version"/>
<aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
<div>
Message value: <ui:outputText value="{!v.message}" />
</div>
</aura:component>
Controller:
({
doInit : function(component, event, helper) {
if(component.get('v.usePromises')) {
helper.helperFunctionAsPromise(component, helper.appendViaApex)
.then(function() {
return helper.helperFunctionAsPromise(component, helper.appendViaApex)
})
.then(function() {
return helper.helperFunctionAsPromise(component, helper.appendViaApex)
})
.then(function() {
return helper.helperFunctionAsPromise(component, helper.appendViaApex)
})
.then(function() {
console.log('Done, no errors');
})
.catch(function(err) {
var toastEvent = $A.get("e.force:showToast");
toastEvent.setParams({
title: 'Error',
type: 'error',
message: err.message
});
toastEvent.fire();
})
.then(function() {
console.log('A bit like finally');
});
} else {
helper.appendViaApex(component);
}
}
})
A few of things to note here:
- We've got the chaining of asynchronous function written as if they are synchronous with all those
then()
functions. This is one of the big advantages of promises. - The promise calls are wrapped in anonymous functions so that we can pass in the parameters, but not just call the function immediately.
- There is a single error handler at the top level, acting like a
try..catch
in synchronous code
The helper:
({
appendViaApex : function(component, resolve, reject) {
var appendAction = component.get('c.append');
appendAction.setParams({
's': component.get('v.message'),
'successPercentage': component.get('v.successPercentage'),
});
appendAction.setCallback(this, function(response) {
var state = response.getState();
if (state === 'SUCCESS') {
component.set('v.message', response.getReturnValue());
if(resolve) {
console.log('resolving appendViaApex');
resolve('appendViaApexPromise succeeded');
}
} else {
if(reject) {
console.log('rejecting appendViaApexPromise');
reject(Error(response.getError()[0].message));
}
}
});
console.log('Queueing appendAction');
$A.enqueueAction(appendAction);
},
helperFunctionAsPromise : function(component, helperFunction) {
return new Promise($A.getCallback(function(resolve, reject) {
helperFunction(component, resolve, reject);
}));
}
})
As I said earlier, appendViaApex
can either be used as a normal helper function, or supplied with resolve
and reject
functions. The modifications to it are pretty minor compared to how you would write it without promises. Then, there's just the one extra function to wrap things into a Promise.
Finally, the rather boring Apex controller:
public class PromisesDemoController {
@AuraEnabled
public static String append(String s, Decimal successPercentage) {
if(Math.random() > successPercentage) {
return '' + 1/0;
} else {
return s + 'a';
}
}
}
If you run this component (with your JS console active), you'll see that each event is queued after the previous one is resolved. In the UI, you'll see 'a's being added one at a time.
As I say, it's a hammer to crack your particular nut. But, it's not as baffling as it first seems and there are going to be use-cases where it saves a lot of callback spaghetti. Even someone who hasn't read up about promises enough to write them would probably be able to read that controller and pretty immediate see what it is doing.