[SalesForce] setTimeout function closes Modal Dialog from Visualforce Page

So, I have some code in which setTimeout is not working as wanted. I need getParameters() to run (which is a method in my Apex controller). This method needs to finish running which is where the values of buttonClicked and bDialogParameters come from. Finally, I need the BIGIANT_DIALOG.init(this, "Redeem", parameters, "{!$Api.Session_ID}") function to run which uses the 'parameters' JavaScript variable.

The BIGIANT_DIALOG.init(this, "Redeem", parameters, "{!$Api.Session_ID}") actually opens up a Modal dialog. I have added setTimeout calls in random places. I have messed around with quotes with no luck either.

For some reason, the setTimeout closes the Modal dialog as soon as it opens after 6 seconds. Can anyone tell me why?

Snippits from VF Page

<apex:commandButton value="{!button}" onclick="javascript:RedeemActionButton(this); return false;" rerender="hiddenBlock" styleClass="btn btn-primary nbtn" >
    <!-- Allows for retrieval of SAN for a given row where a button is pressed -->
    <apex:param name="rowIndex" assignTo="{!rowCount}" value="{!rowCount}"/>
    <!-- The type of button that was clicked (e.g. Redeem) -->
    <apex:param name="buttonClicked" assignTo="{!buttonClicked}" value="{!button}"/>
    <!-- Hidden pageBlock allows for the param above to be passed within a commandButton -->
    <apex:pageBlock id="hiddenBlock" rendered="false"/>
</apex:commandButton>

<!--- To be used to get values for bDialog Modal creation. Called from javascript function -->     
<apex:actionFunction action="{!calcBDialogParameters}" name="getParameters"/>

function RedeemActionButton(button) {
    setTimeout('getParameters()', 6000); <!-- call to Apex:actionFunction which will set the values below in the Apex controller -->
    var recordName="{!buttonClicked}"; <!-- retrieve value from Apex controller -->
    var parameters="{!bDialogParameters}"; <!-- retrieve value from Apex controller -->
    window.alert(recordName); <!-- Test to see what recordName is currently set to. Always shows up empty -->
    window.alert(parameters); <!-- Test to see what parameters is currently set to. Always shows up empty -->

    setTimeout(function(){BIGIANT_DIALOG.init(this, "Redeem", parameters, "{!$Api.Session_ID}")}, 6000); <!-- Opens a Modal dialog -->
}

Best Answer

TL;DR;

The variables aren't rendered yet when they're accessed, and even if they were they're not rendered in the current running version of the function. See a solution/flow below for a way around this problem.

The Problem

The problem is the flow. The function RedeemActionButton is rendered during the last pass of the Visualforce rendering engine (during rerender or initial render), and put into the browser's memory and a reference to it stored to the Window object for calling (window.RedeemActionButton).

When this function is called it is calling your apex:actionFunction synchronously, which then performs async call to the controller/extension and then re-renders the page/specified sections, and during this time, the other function that kicked off that async apex:actionFunction has already completed running.

Even if the re-render were to update the definition of the function while it is running, it's already been pulled into memory while running and you're only really redefining the function variable's reference.

Imagine this function if you will:

var x = function(){
  x = function() { console.log('horse'); };
  console.log('cow');
}

On first run it will output cow. On second run it'll output horse, even though it was edited at the start of the first run. It can't edit itself mid-run, and the same is true for the actionFunction's re-render of the function.

A Solution

To work around this you can employ a few tricks by using rerender and oncomplete on the actionFunction. By using oncomplete and re-rendering the function you're about to call, you can call to another method in Javascript and reference variables that were recently refreshed, and the method is only called after those variables are populated.

<apex:actionFunction name="getParameters" rerender="getparamscomplete" oncomplete="getParametersCompleted();"/>
<apex:outputPanel id="getparamscomplete">
    <script type="text/javascript">
    function RedeemActionButton(button) {
        // left your timeout, but potentially not needed unless you need a 6 second delay for some reason before executing all of this
        setTimeout('getParameters()', 6000); <!-- call to Apex:actionFunction which will set the values below in the Apex controller -->
    }
    function getParametersCompleted() {
        var recordName="{!JSENCODE(buttonClicked)}"; <!-- retrieve value from Apex controller -->
        var parameters="{!JSENCODE(bDialogParameters)}"; <!-- retrieve value from Apex controller -->
        window.alert(recordName); <!-- Test to see what recordName is currently set to -->
        window.alert(parameters); <!-- Test to see what parameters is currently set to -->

        // also possibly/probably not necessary unless you want another 6 second wait until the dialog loads after the apex is done loading those values above
        setTimeout(function(){BIGIANT_DIALOG.init(this, "Redeem", parameters, "{!$Api.Session_ID}")}, 6000); <!-- Opens a Modal dialog -->
    }
    </script>
</apex:outputPanel>

Additional Info

You can also do this without calling a second method in the oncomplete by actually rerendering the actionFunction itself (and storing the code in the oncomplete). I find this to be a little more clear though and recommend doing it this way.

You might also notice I added JSENCODE around your variables coming from the controller/extension. In JS this is critically important to prevent someone from hacking your page if those variables can be edited (via query params or stored data, or just a bug possibly from a tired dev with weary eyes).

Update for question edit with more specifics

Here is the same concept applied to your code. Instead of using a hidden pageBlock to utilize the rerender attribute (thus allowing apex:params to controller) it re-renders a script block, then oncompletes that function after it re-renders it (with the populated data because the button now also calls the action in the controller).

<apex:commandButton value="{!button}" action="{!calcBDialogParameters}" oncomplete="openBDialog();" rerender="bDialogScript" styleClass="btn btn-primary nbtn" >
    <!-- Allows for retrieval of SAN for a given row where a button is pressed -->
    <apex:param name="rowIndex" assignTo="{!rowCount}" value="{!rowCount}"/>
    <!-- The type of button that was clicked (e.g. Redeem) -->
    <apex:param name="buttonClicked" assignTo="{!buttonClicked}" value="{!button}"/>
</apex:commandButton>


<apex:outputPanel id="bDialogScript">
    <script type="text/javascript">
        function openBDialog() {
            var recordName="{!buttonClicked}"; <!-- retrieve value from Apex controller -->
            var parameters="{!bDialogParameters}"; <!-- retrieve value from Apex controller -->
            window.alert(recordName); <!-- Test to see what recordName is currently set to. Always shows up empty -->
            window.alert(parameters); <!-- Test to see what parameters is currently set to. Always shows up empty -->

            // timeout of 6 seconds probably not necessary here
            setTimeout(function(){BIGIANT_DIALOG.init(this, "Redeem", parameters, "{!$Api.Session_ID}")}, 6000); <!-- Opens a Modal dialog -->
        }
    </script>
</apex:outputPanel>
Related Topic