[SalesForce] javascript remoting in inline page on standard page layout works only once

I've written an autocomplete component using remoting which I use in a visualforce page with a component that is embedded in a standard page layout.

When I change the autocomplete component, the autocomplete works just once. If I then do a refresh of the page I get a javascript error:
ReferenceError: AutocompleteController is not defined

So it looks like Salesforce sees the page the first time and creates the function to do the autocomplete remoting.
The second time around this javascript seems to be cached and then salesforce doesn't create the function for the remoting.

Can anyone suggest a course of action?

It looks like this:
the page that is embedded in the standard page layout of the placement:

<apex:page standardcontroller="Placement__c" cache="false">
    <c:RequiredCertificatesCheckList CparentObjectId="{!Placement__c.id}" />
</apex:page>

This is the component:

<apex:component controller="RequiredCertificatesCheckList" allowDML="true">

    <apex:attribute name="CparentObjectId" type="String" assignTo="{!parentObjectId}" description="The id of the parentObject" required="true"/>
    <apex:form id="theForm">
    <apex:outputPanel id="thePageToRefresh">
            <apex:pageblock title="New Required Certificate" >
            <apex:pagemessages />
<apex:pageblocksection columns="1">
                    <apex:pageblocksectionitem >
                        <apex:outputLabel value="{!$ObjectType.Required_Certificate__c.Label}" />
                        <apex:InputField value="{!newRC.Required_Certificate__c}" id="acc_auto" >
                            <c:SingleFieldAutocomplete objectname="Property__c" autocomplete_textbox="{!$Component.acc_auto}" additional_filter="Type__c='Certificates'" FieldsToDisplay="Name,Category__r.Name" FieldsToQuery="Name,Category__r.Name" limit="5"/>
                        </apex:InputField>
                     </apex:pageblocksectionitem>
                </apex:pageblocksection>
                <apex:pageBlockButtons location="bottom" >
                    <apex:commandbutton action="{!saveNewRequiredCertificate}" value="{!$Label.Save}" rerender="thePageToRefresh" />
                    <apex:commandbutton action="{!CancelNewRequiredCertificate}" value="{!$Label.Cancel}" rerender="thePageToRefresh" immediate="true"/>
                </apex:pageBlockButtons>
            </apex:pageblock>
        </apex:outputPanel>

then I have the autocomplete component:

<apex:component controller="AutocompleteController" >

<apex:includeScript value="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"/>
<apex:includeScript value="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.1/jquery-ui.min.js"/>
<apex:stylesheet value="//ajax.googleapis.com/ajax/libs/jqueryui/1.8/themes/base/jquery-ui.css"/>
<style>
.autoCompleteBoxScrolling {display:none !important;}
.ui-autocomplete{font-size:12px;}
.ui-autocomplete-loading { background: white url({!$Resource.circleIndicator}) right center no-repeat; }
.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus {border:0px solid #000 !important;}
</style>

<apex:attribute name="objectname" description="The object name you want to look for." type="String" required="true"/>
<apex:attribute name="autocomplete_textbox" description="The ID for the Autocomplete List Textbox." type="String" required="true"/>
<apex:attribute name="FieldsToDisplay" description="Fields to show. comma separated" type="String" required="true"/>
<apex:attribute name="FieldsToQuery" description="Fields to query. comma separated" type="String" required="true"/>
<apex:attribute name="limit" description="Number of results to return" type="String" required="true"/>
<apex:attribute name="additional_filter" description="Additional filter to use for query" type="String" required="false"/>




<script type="text/javascript">
var q$ = jQuery.noConflict();
q$(document).ready(function() {
        AutoCompleteInputElement = function(){};
        q$(esc('{!autocomplete_textbox}')).attr('onchange','');

        var sObjects;
        var queryTerm;

        q$(esc('{!autocomplete_textbox}')).autocomplete({
                minLength: 1,
                source: function(request, response) {
                        queryTerm = request.term;
                        AutocompleteController.findSObjectsSingle("{!objectname}","{!additional_filter}","{!FieldsToDisplay}", "{!FieldsToQuery}","{!limit}", request.term,  function(result, event){
                                if(event.type == 'exception') {
                                        alert(event.message);
                                } else {
                                        sObjects = result;
                                        response(sObjects);
                        }
                });
        },
                focus: function( event, ui ) {
                        //q$(esc('{!autocomplete_textbox}')).val( ui.item.Name );
                        return false;
                },
                select: function( event, ui ) {
                        q$(esc('{!autocomplete_textbox}')).val( HtmlDecode(ui.item.Name ));
                        q$(esc('{!autocomplete_textbox}_lkid')).val( ui.item.Id );
                        q$(esc('{!autocomplete_textbox}_lkold')).val( ui.item.Name );
                        q$(esc('{!autocomplete_textbox}_mod')).val( '1' );
                        return false;
                }
        })
        .data( "uiAutocomplete" )._renderItem = function( ul, item ) {
                var fields_to_display = '{!FieldsToDisplay}'.split(',');
        var display_items=[];

        for (i in fields_to_display) {
            if ((i/2)*2==i) {
                    fieldBreakDown = fields_to_display[i].split('.');
                    tempItem=item;
                    for (j in fieldBreakDown) {
                            if ((j/2)*2==j) {
                            tempItem=tempItem[fieldBreakDown[j]];
                            }
                    }
                display_items.push(tempItem);
            }
        }
        var display_item = display_items.join(' - ');

                var temp =  queryTerm.replace('&','\&\a\m\p\;');
                var entry = "<a>" + display_item.replace( new RegExp( "(" + temp + ")" , 'gi' ), '<b>$1</b>' );
                entry = entry + "</a>";

                return q$( "<li></li>" )
                .data( "item.autocomplete", item )
                .append( entry )
                .appendTo( ul );
        };
});

function esc(myid) {
return '#' + myid.replace(/:/g,'\\:');
}

function HtmlDecode(s) {
      var out = "";
      if (s==null) return;
      var l = s.length;
      for (var i=0; i<l; i++) {
            var ch = s.charAt(i);
            if (ch == '&') {
                  var semicolonIndex = s.indexOf(';', i+1);
                          if (semicolonIndex > 0) {
                        var entity = s.substring(i + 1, semicolonIndex);
                        if (entity.length > 1 && entity.charAt(0) == '#') {
                              if (entity.charAt(1) == 'x' || entity.charAt(1) == 'X')
                                    ch = String.fromCharCode(eval('0'+entity.substring(1)));
                              else
                                    ch = String.fromCharCode(eval(entity.substring(1)));
                        } else {
                              switch (entity) {
                                    case 'quot': ch = String.fromCharCode(0x0022); break;
                                    case 'amp': ch = String.fromCharCode(0x0026); break;
                                    case 'lt': ch = String.fromCharCode(0x003c); break;
                                                                        ........
                                    case 'clubs': ch = String.fromCharCode(0x2663); break;
                                    case 'hearts': ch = String.fromCharCode(0x2665); break;
                                    case 'diams': ch = String.fromCharCode(0x2666); break;
                                    default: ch = ''; break;
                              }
                        }
                        i = semicolonIndex;
                  }
            }
            out += ch;
      }
      return out;
}
</script>
</apex:component>

and the controller:

global with sharing class AutocompleteController {

    global AutoCompleteController() {}
    global AutoCompleteController(ApexPages.StandardController controller) {}



    @RemoteAction
    global static SObject[] findSObjectsSingle(string objectToQuery, String filterToUse,String FieldsToDisplay,String FieldsToQuery, String limitTo, string qry) {
        String soql;
        // check to see if the object passed is valid
        Map<String, Schema.SObjectType> gd = Schema.getGlobalDescribe();
        Schema.SObjectType sot = gd.get(objectToQuery);
        if (sot == null) {
            // Object name not valid
            return null;
        }

        set<String> displayfields  = new set<String>();
        set<String> queryfields  = new set<String>();

        // fields to display
        list<String> addFieldsDisp = FieldsToDisplay.split(',');
        for (String af:addFieldsDisp) {
            displayfields.add(af);
        }
        list<String> displayFieldsList = new list<String>();
        for (String df:displayfields) {displayFieldsList.add(df); }
        soql = 'select ' + String.join(displayFieldsList,',');

        soql += ' FROM ' + objectToQuery;
        // fields to query
        list<String> addFieldsQuery = FieldsToQuery.split(',');
        for (String af:addFieldsQuery) {
            queryfields.add(af);
        }
        list<String> queryFieldsList = new list<String>();
        for (String df:addFieldsQuery) {queryFieldsList.add(df+' like \'%' + String.escapeSingleQuotes(qry) + '%\''); }
        soql += ' WHERE (' + String.join(queryFieldsList,' OR ') +')';

        soql += ' AND ' + filterToUse;
        soql += ' limit '+limitTo;

        List<sObject> L = new List<sObject>();
        try {
            L = Database.query(soql);
        }
        catch (QueryException e) { return null; }

        return L;
    }

}

Best Answer

The reason it functions properly when you remove the rerender is because of the way the javascript events are bound in the browser to the HTML elements in the page. By removing the rerender attribute and causing a full page post-back, the whole page is loaded from scratch and all of the script is evaluated and bound to the targeted elements in the DOM.

A rerender attribute causes parts of the page structure to be replaced when the response comes back. Because of this, when you bind a javascript function to an HTML element and then later you replace that element, any function that was previously bound isn't ever going to be called again. The element in the DOM that the function was originally bound to doesn't exist. It has been replaced with a new element which may have identical markup, but the new element does not have any of the old element's events bound to it.

In order to make this operate the way you want it to you will want to move all of the autocomplete bindings out of the ready function and into a function defined separately which you can call first within the ready function and then again when the rerender is complete. This will give you control of the bindings on both the 'on initial display' and 'after replacing part of the page' parts of this lifecycle.


One other thing to note, you don't need to use the function esc(SFDC_ID) in your markup in order to escape the colons in the salesforce ID values that are being passed into the component. You can target the ID attribute specifically and put the string containing the ID with colons in quotes.

The selector will look like this: q$('[id="{!autocomplete_textbox}"]').autocomplete({


Example of the way your component markup could be structured:

<script type="text/javascript">

    var bindAutoComplete = function () {

        // beware that this AutoCompleteInputElement on the next line without a var in front of it 
        // is going to create a global variable on the window object, which is generally unadvisable
        AutoCompleteInputElement = function () {};
        q$('[id="{!autocomplete_textbox}"]').attr('onchange', '');

        var sObjects;
        var queryTerm;

        q$('[id="{!autocomplete_textbox}"]').autocomplete({
            minLength: 1,
            source: function (request, response) {
                queryTerm = request.term;
                AutocompleteController.findSObjectsSingle("{!objectname}", "{!additional_filter}", "{!FieldsToDisplay}", "{!FieldsToQuery}", "{!limit}", request.term, function (result, event) {
                    if (event.type == 'exception') {
                        alert(event.message);
                    } else {
                        sObjects = result;
                        response(sObjects);
                    }
                });
            },
            focus: function (event, ui) {
                //q$('[id="{!autocomplete_textbox}"]').val( ui.item.Name );
                return false;
            },
            select: function (event, ui) {
                q$('[id="{!autocomplete_textbox}"]').val(HtmlDecode(ui.item.Name));
                q$('[id="{!autocomplete_textbox}_lkid"]').val(ui.item.Id);
                q$('[id="{!autocomplete_textbox}_lkold"]').val(ui.item.Name);
                q$('[id="{!autocomplete_textbox}_mod"]').val('1');
                return false;
            }
        })
            .data("uiAutocomplete")._renderItem = function (ul, item) {
                var fields_to_display = '{!FieldsToDisplay}'.split(',');
                var display_items = [];

                for (i in fields_to_display) {
                    if ((i / 2) * 2 == i) {
                        fieldBreakDown = fields_to_display[i].split('.');
                        tempItem = item;
                        for (j in fieldBreakDown) {
                            if ((j / 2) * 2 == j) {
                                tempItem = tempItem[fieldBreakDown[j]];
                            }
                        }
                        display_items.push(tempItem);
                    }
                }
                var display_item = display_items.join(' - ');

                var temp = queryTerm.replace('&', '\&\a\m\p\;');
                var entry = "<a>" + display_item.replace(new RegExp("(" + temp + ")", 'gi'), '<b>$1</b>');
                entry = entry + "</a>";

                return q$("<li></li>")
                    .data("item.autocomplete", item)
                    .append(entry)
                    .appendTo(ul);
        };

    };

    var q$ = jQuery.noConflict();
    q$(document).ready(function() {
        // bind the autocomplete here for execution when the page loads initially
        bindAutoComplete();
    });

    function HtmlDecode(s) {
          var out = "";
          if (s==null) return;
          var l = s.length;
          for (var i=0; i<l; i++) {
                var ch = s.charAt(i);
                if (ch == '&') {
                      var semicolonIndex = s.indexOf(';', i+1);
                              if (semicolonIndex > 0) {
                            var entity = s.substring(i + 1, semicolonIndex);
                            if (entity.length > 1 && entity.charAt(0) == '#') {
                                  if (entity.charAt(1) == 'x' || entity.charAt(1) == 'X')
                                        ch = String.fromCharCode(eval('0'+entity.substring(1)));
                                  else
                                        ch = String.fromCharCode(eval(entity.substring(1)));
                            } else {
                                  switch (entity) {
                                        case 'quot': ch = String.fromCharCode(0x0022); break;
                                        case 'amp': ch = String.fromCharCode(0x0026); break;
                                        case 'lt': ch = String.fromCharCode(0x003c); break;
                                                                            ........
                                        case 'clubs': ch = String.fromCharCode(0x2663); break;
                                        case 'hearts': ch = String.fromCharCode(0x2665); break;
                                        case 'diams': ch = String.fromCharCode(0x2666); break;
                                        default: ch = ''; break;
                                  }
                            }
                            i = semicolonIndex;
                      }
                }
                out += ch;
          }
          return out;
    }
</script>

The command buttons would be defined like this so that when the AJAX POST and rerender are complete, the script that binds the autocomplete is executed again and bound to the elements which are now in the page (after the elements targeted by the rerender have all been replaced).

<apex:pageBlockButtons location="bottom" >
    <apex:commandbutton action="{!saveNewRequiredCertificate}" value="{!$Label.Save}" rerender="thePageToRefresh" oncomplete="bindAutoComplete();" />
    <apex:commandbutton action="{!CancelNewRequiredCertificate}" value="{!$Label.Cancel}" rerender="thePageToRefresh" oncomplete="bindAutoComplete();" immediate="true"/>
</apex:pageBlockButtons>