Search code examples
javascriptsalesforcevisualforce

Why is my link not being passed back from Apex method using JavaScript Remoting?


I have the following code that I adapted from a response to another question on Stackoverflow. I am very appreciative of the answer provided by @eyescream.

I am now trying to display a link to account objects that has come back from a JavaScript Remoting call to an Apex method. I get the name and ID, but not the URL I am passing back.

Here is the page:

<apex:page docType="html-5.0" controller="Stack73082136">
  <h1>Account Type-Ahead Search Demo VF</h1>
  <apex:form >
      <script>
      function callRemote(term){
          // call static method in ClassName.methodName format
          Visualforce.remoting.Manager.invokeAction(
            '{!$RemoteAction.Stack73082136.getMatchingAccounts}',
            term,
            false, // no contacts pls
            function(result, event){
                if (event.status) {
                    //debugger;
                    let target = document.getElementById("out3");
                    while (target.firstChild) {
                      target.removeChild(target.firstChild);
                    }
                    result.forEach((item) => { console.log(item.name); console.log(item.link); target.append('<A href="' + item.link + '">' + item.name + '|' + item.link + '</A>' ) } );
                } else if (event.type === 'exception') {
                    document.getElementById("responseErrors").innerHTML = 
                        event.message + "<br/>\n<pre>" + event.where + "</pre>";
                } else {
                    document.getElementById("responseErrors").innerHTML = event.message;
                }
            }, 
            {escape: true}
        );
      }
      </script>
      <apex:pageBlock title="3. VF remoting, the grandfather of Aura. No viewstate, pure html and js">    
          <input type="text" id="text3" label="type and vait v2" onkeyup="callRemote(this.value)" />
          <div id="out3"></div>
          <div id="responseErrors"></div>
      </apex:pageBlock>
  </apex:form>
</apex:page>

Here is the Apex:

public class Stack73082136 {
    
    // Service part (static, useable in Aura/LWC but eventually maybe also as a REST service)
    // This method would typically throw exceptions, perhaps AuraHandledException
    @RemoteAction @AuraEnabled(cacheable=true)
    public static MatchingAccountsWrapper[] getMatchingAccounts(String searchString, Boolean showContacts) {
        String searchSpec = '%' + searchString + '%';
        List<Account> accountsFound;
        if (showContacts) {
            accountsFound = [
                SELECT Id, Name,
                    (SELECT Id, Name FROM Contacts ORDER BY Name) 
                FROM Account 
                WHERE Name LIKE :searchSpec 
                ORDER BY Name];
        } else {
            accountsFound = [
                SELECT Id, Name
                FROM Account 
                WHERE Name LIKE :searchSpec 
                ORDER BY Name];
        }

        List<MatchingAccountsWrapper> matchingAccounts = new List<MatchingAccountsWrapper>();
        for (Account ma : accountsFound) {
            MatchingAccountsWrapper mar = new MatchingAccountsWrapper(ma.Id, ma.Name, showContacts ? ma.Contacts: null);
            matchingAccounts.add(mar);
            system.debug('#@# matching account.name = ' + ma.Name);
            system.debug('#@# matching mar.name = ' + mar.name);
            system.debug('#@# matching mar.link = ' + mar.link);
        }
        return matchingAccounts;
    }
    
    // Visualforce part (old school, stateful)
    // This would typically not throw exceptions but ApexPages.addMessage() etc.
    // (which means that non-VF context like a trigger, inbound email handler or code called from Flow would crash and burn at runtime; in these you can only do exceptions)
    
    public String searchValue {get;set;}

    public List<MatchingAccountsWrapper> getMatchingAccountsVF() {
        return getMatchingAccounts(searchValue, false);
    }

    public void onKeyUpHandlerVF() {
        system.debug('#@# AccountTypeAheadSearchHelper:onKeyUpHandlerVF(): BEGIN');
        system.debug('#@# AccountTypeAheadSearchHelper:onKeyUpHandlerVF(): accountSearchVFValue = ' + searchValue);
        // do nothing. this method is "stupid", it's only job is to be called, pass the parameter and then the getMatchingAccountsVF
        // will be called by VF engine when it needs to rerender {!matchingAccountsVF} expression
    }

    public class MatchingAccountsWrapper {

        public MatchingAccountsWrapper(String k, String n) {
            key = k;
            name = n;
        }

        public MatchingAccountsWrapper(String k, String n, List<Contact> c) {
            key = k;
            name = n;
            relatedContacts = c;
        }

        public MatchingAccountsWrapper(Account a) {
            key = a.Id;
            name = a.Name;
        }

        @AuraEnabled
        public string key {get; set;}

        @AuraEnabled
        public string name {get; set;}

        @AuraEnabled
        public string link {get {
            return URL.getSalesforceBaseUrl().toExternalForm() + '/' + this.key;
        } set;}

        private List<Contact> relatedContacts {get; set;}

        @AuraEnabled
        public List<MatchingContactsWrapper> contacts {get {
            if (relatedContacts != null) {
                List<MatchingContactsWrapper> matchingContacts = new List<MatchingContactsWrapper>();
                for (Contact matchingContact : relatedContacts) {
                    MatchingContactsWrapper mac = new MatchingContactsWrapper(matchingContact);
                    matchingContacts.add(mac);
                }
                return matchingContacts;
            } else {
                return null;
            }
        } set;}
    }

    private class MatchingContactsWrapper {

        public MatchingContactsWrapper(Contact c) {
            key = c.Id;
            name = c.Name;
        }

        @AuraEnabled
        public string key {get; set;}

        @AuraEnabled
        public string name {get; set;}

        @AuraEnabled
        public string link {get {
            return URL.getSalesforceBaseUrl().toExternalForm() + '/' + this.key;
        } set;}
    }

}

The Apex debug log shows the link property is populated but the JavaScript console.log does not. And this is what I get displayed on my page when I search for st:

<A href="undefined">Express Logistics and Transport|undefined</A><A href="undefined">Pyramid Construction Inc.|undefined</A>

I did find that if I set the value of link in the constructors like this, it works later to just use the simple link property:

            key = k;
            name = n;
            link = URL.getSalesforceBaseUrl().toExternalForm() + '/' + k;
        }

        public MatchingAccountsWrapper(String k, String n, List<Contact> c) {
            key = k;
            name = n;
            relatedContacts = c;
            link = URL.getSalesforceBaseUrl().toExternalForm() + '/' + k;
        }

//        @AuraEnabled
//        global string link {get {
//            return URL.getSalesforceBaseUrl().toExternalForm() + '/' + this.key;
//        } set;}

        @AuraEnabled
        global string link {get; set;}

I don't know why this is necessary, though. It works with this page code:

<apex:page docType="html-5.0" controller="Stack73082136">
  <h1>Account Type-Ahead Search Demo VF</h1>
  <apex:form >
      <script>
      function callRemote(term){
          // call static method in ClassName.methodName format
          Visualforce.remoting.Manager.invokeAction(
            '{!$RemoteAction.Stack73082136.getMatchingAccounts}',
            term,
            false, // no contacts pls
            function(result, event){
                if (event.status) {
                    //debugger;
                    let target = document.getElementById("out3");
                    while (target.firstChild) {
                      target.removeChild(target.firstChild);
                    }
                    result.forEach((item) => {
                        console.log(item.name);
                        console.log(item.link); 
                        let a = document.createElement('a');
                        let text = document.createTextNode(item.name);
                        a.appendChild(text);
                        a.title = item.name;
                        a.href = item.link;
                        a.target = '_blank';
                        target.appendChild(a);
                        let br = document.createElement('br');
                        target.appendChild(br);
                    } );
                } else if (event.type === 'exception') {
                    document.getElementById("responseErrors").innerHTML = 
                        event.message + "<br/>\n<pre>" + event.where + "</pre>";
                } else {
                    document.getElementById("responseErrors").innerHTML = event.message;
                }
            }, 
            {escape: true}
        );
      }
      </script>
      <apex:pageBlock title="3. VF remoting, the grandfather of Aura. No viewstate, pure html and js">    
          <input type="text" id="text3" label="type and vait v2" onkeyup="callRemote(this.value)" />
          <div id="out3"></div>
          <div id="responseErrors"></div>
      </apex:pageBlock>
  </apex:form>
</apex:page>

Solution

  • It's ancient internet history and I might be using wrong terms, don't kill the messenger. In the olden days when Java was widely considered awesome ;) there was a concept of Beans. Simple objects to encapsulate data (but not behavio(u)r) to pass around between systems. The concept's kind of hit dead end but the term somewhat survived as POJO.

    My point is - stuff you pass from Apex to Visualforce/Aura/LWC has to be simple. JSON-serializable. pure data. Primitives (integer, string, date), collections (well, Lists tend to work best really) and objects of classes built out of these primitives. Ideally fields marked public. Don't count on passing transient, private and functions (behavio(u)r!).

    Your original code contains a getter function, not a real field.

    public string link {get {
        return URL.getSalesforceBaseUrl().toExternalForm() + '/' + this.key;
    } set;}
    

    is short hand syntax for defining 2 functions, string getLink(), void setLink(string s)`. (shameless plug: https://salesforce.stackexchange.com/a/9171/799, my answer from 2013)

    Since it's a function and not a real field - it's lost during serialization and passing to JS. JS will not automagically call Apex to run it whenever you're accessing this "field" - that'd be performance killer.

    Make it plain old field and set it in Apex (like you already did) or create the field on the fly in JS when processing the call's results.