5

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;
    }

}
2
  • 2
    If you remove all of the rerender attributes and allow the page to do a full postback, rather than a partial page refresh, does it operate properly?
    – Mark Pond
    Oct 11, 2013 at 16:41
  • That seems to solve my problem! Thanks a lot. It should work with the rerenders though... Theoretically... :-P Can you post it as an answer so I can credit you for it? Maybe you also know why it works without the rerenders? Oct 14, 2013 at 6:17

1 Answer 1

2

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>
2
  • I'm already pleased to get it working without the rerender. However, when I implement your changes it still only works the first time. The second time I get an error: Error: Permission denied to access property 'Sfdc'. I know this has to do with the fact that the iframe is on a different domain (cs13.salesforce.com for the main page and c.cs13.salesforce.com for the visualforce page in the iframe) Do you have any solution for that? That would really make it all perfect. Oct 15, 2013 at 10:51
  • I don't have a solution for that one handy. I am not sure I will be able to answer in a timely manner as I'll have to set up a page, try to reproduce it and test out some solutions.
    – Mark Pond
    Oct 15, 2013 at 16:34

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.