7

So I have the following structure:

Lightning Controller

({
   doInit : function(....){
       helper.getDataFromServer(......);
   },
   onSelectChangeFunction(.....){
       component.set("v.theBooleanValue",....);
       helper.setTheLabel(....);
   },
   eventHandler : function(....){
       component.set("v.IdValue"....);
       //the following two lines are the issue. I need to call setTheLabel after the callback from this server call. Can I do a getCallback or something here
       helper.getDataFromServer(....); //Updates Select List and populates a Map
       component.set("v.theBooleanValue",false); //This is important needs to be false to control display at this point

   }
)}

Helper

({

    getDataFromServer : function(component, event,helper){
        var theBoolean = component.get("v.theBooleanValue");

        ...server callback...{
            //I would like to get rid of the above var and the below if block
            if(theBoolean){
              helper.setTheLabel(....);
            }
        } 
    },
    setTheLabel : function(.....){
       component.find("...").set("v.label",.....); //gets value from a map populated in the getDataFromServer
    }    

)}

There are two flows:

  1. Select List Item selected - can get label fine
  2. theBooleanValue is set to true so we know we are doing this event
  3. Even is fired and need to update the pick list values (getDataFromServer) then call setTheLabel only if theBooleanValue is true

I would like to remove the calling of setTheLabel in the callback of getDataFromServer so that it is not checking when it does not need to. The only time this comes into play is in the eventHandler method. In that method the callback is executed after theBoolean has been set to true. I also need the setTheLabel in this case to execute after the callback is completed in getDataFromServer but would like to somehow capture that in the eventHandler method instead as it is the only method where this pattern happens.

Does that make sense at all? I guess the real question is how can I do something like this:

   eventHandler : function(....){
       //theBooleanValue was set to true in another function that got us here
       helper.getDataFromServer(....); 

       //ON COMPLETE of the above then execute setTheLabel.

       component.set("v.theBooleanValue",false); //This is important needs to be false so the getDataFromServer does not setTheLabel if not called from this path

   }
  • I think you want to call helper.getDataFromServer() from different locations, but you don't want to execute the ...server callback... code black is this right? – itzmukeshy7 May 13 '17 at 6:28
  • In the eventHandler I want to execute something after getDataFromServer callback is done without having to add code to the getDataFromServer just to handle the one off scenario from the eventHandler. – Eric May 13 '17 at 6:31
  • @Eric. Will you be able to explain what you are trying to avoid?. Sorry for whatever reasons did not get what you are trying to explain – RedDevil May 13 '17 at 14:51
  • Is getDataFromServer setting any attibutes on your component? – Jake Richter May 19 '17 at 12:30
2
+100

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.

  • It was much simpler to just keep the current boolean check. I would have had to rework the entire method everywhere to return a promise, handle it, etc just to provide support for that one method. Unless I am missing something the promise seems a lot more complicated. I did not get it to work but did not put a lot of time into it either. Never got the getDataFromServerPromise working. I think I would have had to copy all the code from the original function into the promise which was too much bloat. Or I am simply not getting it. – Eric May 18 '17 at 0:53
  • @Eric You got me. I've been meaning to look into this properly for ages. So, I've updated my answer with a fully worked solution (not based on your code, so that I could actually run it myself). Maybe it will convince you - it will certain help me when I finally do need promises in Lightning one day. – Aidan May 19 '17 at 9:22
1

I think only callback function can help you in this:

Controller

({
   doInit : function(component, event, helper){
       /* With this call, you don't want to execute the setTheLabel logic, so don't add anything in callback function body(Leave it blank) */
       helper.getDataFromServer(component, function(){/* callback function body */});
   },
   onSelectChangeFunction(component, event, helper){
       component.set("v.IdValue",....);
       helper.setTheLabel(....);
   },
   eventHandler : function(component, event, helper){
       component.set("v.IdValue"....);
       //the following two lines are the issue. I need to call setTheLabel after the callback from this server call. Can I do a getCallback or something here


       /* With this call you want to execute the setTheLabel logic, so pass setTheLabel logic in callback function body */
       helper.getDataFromServer(component, function(){
           /* Add the setTheLabel logic here which will execute after getting data from the server */
           var theBoolean = component.get("v.theBooleanValue");
           if(theBoolean){
              setTheLabel(....);
           }
       }); //Updates Select List and populates a Map

       component.set("v.theBooleanValue",false); //This is important needs to be false to control display at this point

   }
})

Helper

({    
    getDataFromServer : function(component, callback){
        var theBoolean = component.get("v.theBooleanValue");

        /* Execute this block of code if the cllback function is provided. */ 
        if(callback){
            callback();
        }
    },
    setTheLabel : function(component){
       component.find("...").set("v.label",.....); //gets value from a map populated in the getDataFromServer
    }    

})

So basically we are passing the 2nd parameter to the helper method which is a function and in the helper method, We have added a condition if the 2nd parameter is passed execute this method.

  • yea, but thats what I was trying to avoid and the point if the question. Maybe its the only/best way. – Eric May 13 '17 at 7:02
  • I don't think there is another way to do this; – itzmukeshy7 May 13 '17 at 7:03
  • probably right. just did not want to add code to the callback just to support a single function elsewhere – Eric May 13 '17 at 7:04
0

If getDataFromServer is setting an attribute in its callback, you could put a change listener on that attribute and do setTheLabel in the handler.

  • Thanks, thought of that but the values that are set in that method get set at times when I would not want the label to change. I think promises are the way to go. The crux of the matter is I need to handle this in the calling method while reusing the common method that queues the action. – Eric May 19 '17 at 15:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.