Saturday 19 July 2014

$Component vs Selectors

As most of us in the Salesforce ecosystem are aware, JavaScript is becoming more and more a key part of any solution.  Users now demand pages that react to user input, reflow when a device is rotated and above all are fast.  

JavaScript relies on access to the Document Object Model (DOM), to extract information entered by the user or update sections of the page based on user input. Support for locating elements in the DOM via their ID is provided by the $Component global variable. 

$Component requires the full path to the element, naming each parent element in the hierarchy, in order to resolve correctly, so given the following Visualforce markup:

<apex:page id="pg" >
 <apex:form id="frm">
  <apex:pageBlock id="pb1">
    <apex:pageBlockSection id="pbs1">
      <apex:pageBlockSectionItem id="pbsi1">
        <apex:outputLabel value="Price" />
        <apex:inputText id="val1" />
      </apex:pageBlockSectionItem>
    </apex:pageBlockSection>
    <apex:pageBlockSection id="pbs2">
      <apex:pageBlockSectionItem id="pbs2">
        <button type="button" onclick="alertPrice();">Go!</button>
      </apex:pageBlockSectionItem>
    </apex:pageBlockSection>
  </apex:pageBlock>
 </apex:form>
</apex:page>

To access the price element with the id of ‘val1’ in my onclick handler, alertPrice, I need to specify the parent pageblocksectionitem, pageblocksection, page block, form and page:

function alertPrice()
{
    var ele=document.getElementById('{!$Component.pg.frm.pb1.pbs1.pbsi1.val1}');
    alert('Price = ' + ele.value);
}

The first problem I always have is remembering to add all of the parent ids, so my JavaScript usually turns into something like:

function alertPrice()
{
    var ele1=document.getElementById('{!$Component.pg.frm}');
    var ele2=document.getElementById('{!$Component.pg.frm.pb1}');
    var ele3=document.getElementById('{!$Component.pg.frm.pb1.pbs1}');
    var ele4=document.getElementById('{!$Component.pg.frm.pb1.pbs1.pbsi1}');
    var ele5=document.getElementById('{!$Component.pg.frm.pb1.pbs1.pbsi1.val1}');
    alert('Price = ' + ele5.value);
}

Each time I add a parent, I load the page and view the source to check that the $Component global renders to a  real value:

Screen Shot 2014 07 19 at 12 09 16

The second problem is that the element is now tightly coupled to its current location, so if I decide to switch the input and the button:

<apex:page id="pg" >
 <apex:form id="frm">
  <apex:pageBlock id="pb1">
    <apex:pageBlockSection id="pbs1">
      <apex:pageBlockSectionItem id="pbsi1">
        <button type="button" onclick="alertPrice();">Go!</button>
      </apex:pageBlockSectionItem>
    </apex:pageBlockSection>
    <apex:pageBlockSection id="pbs2">
      <apex:pageBlockSectionItem id="pbs2">
        <apex:outputLabel value="Price" />
        <apex:inputText id="val1" />
      </apex:pageBlockSectionItem>
    </apex:pageBlockSection>
  </apex:pageBlock>
 </apex:form>
</page:page>

My onclick handler now throws an error, as the $Component doesn't evaluate to a valid element id:

Screen Shot 2014 07 19 at 12 15 39

To fix my JavaScript, I have to update the $Component reference to reflect the new location of the element, which always takes me a couple of attempts to get right, as mentioned earlier.

In my example, I have one simple function to fix up, but if I’ve moved a lot of business logic to the client, I could have any number of $Component references waiting to be broken when someone restructures the page markup.  One way to workaround this is to colocate the JavaScript with the element, but that doesn’t adhere to the principles of Unobtrusive JavaScript.

My preferred solution now is to use Selectors (sometimes referred to as CSS selectors, as the technique is borrowed from CSS3. This allows me to specify the a pattern to match against rather than the fully qualified path. As long as I stick to unique ids as I define each element (rather than relying on the full path for uniqueness), I can use a selector to match the element that ends with my specified id. Selectors have good support in modern browsers, but IE7 is a major hole and IE8 only allows CSS 2.1 selectors.  Here at BrightGen most of our customers are large enterprises that don’t update their desktop browsers that often, so we still have to support some of the older versions that don’t have built-in selector support.  For this reason, I use JQuery Selectors,  

After I’ve included JQuery from a CDN (as there’s a good chance a user will have accessed this from another page, possibly one of mine, and their browser has cached it), access the value of the input element whose id ends with ‘val1’ is simply: 

<apex:includeScript 
value="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.min.js"/>
<script>
  function alertPrice()
  {
    var price=$("[id$='val1']").val();
    alert('Price = ' + price);
  }
</script>

The selector is:

$("[id$='val1']")

where $= equates to ends with, so id$=‘val1’ equates to the element whose id ends with the snippet ‘val1’.  Now I can relocate my element anywhere on the page, safe in the knowledge my JavaScript will still be able to access it. For the sake of completeness, the .val() at the end is a JQuery method to extract the value from an input element in the form.

3 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. You can actually do this much more easily. Check out http://arrowpointe-developer-edition.na4.force.com/selectorsExample?id=00Q6000000c5tTQ where I explain it. The key is to add a script tag just below rendering the Component.

    ReplyDelete
    Replies
    1. I.ve never liked that mechanism for a couple of reasons:

      1. It's not adhering to the principles of unobtrusive JavaScript.
      2. It's still fragile - if someone moves the input field without the associated JavaScript it will break.

      Delete