Salesforce: $Component-merge fields behaving unexpectedly

Visits: 327

So I was writing a slightly complicated form validation in Javascript. Therefore I needed access to the values of all fields of an <apex:form>. But worse, some fields were shown conditionally.

So I assumed that the validation would merely need to check whether the fields existed, as a conditional rendering does not add the fields to the DOM (in contrast to hiding them with display: none). So my Visualforce page looked something like this:

<apex:page controller="ConditionalRerenderController">
    <apex:form>
        Click me
        <apex:inputCheckbox value="{!condition}" id="firstId">
            <apex:actionSupport event="onclick" rerender="conditionalBlock"/>
        </apex:inputCheckbox><br/>
        <apex:pageBlock id="conditionalBlock">
            <apex:pageBlockSection rendered="{!condition}" columns="1">
                <apex:inputText id="secondId" value="{!stringValue}" /><br/>
            </apex:pageBlockSection>
        </apex:pageBlock>
    </apex:form>
</apex:page>

The JS to get the value of the text input field would be rather simple

var inputfield = document.getElementById('secondId');
if(inputField != null)
    alert(inputfield.value);

However, the attribute id in Visualforce does not translate directly to the HTML attribute of the same name. To make sure a produced id is in fact unique, Salesforce adds information on the context, practically making it unusable to hard-code the id in javascript.

But there is the global merge field $Component which allows to resolve the produced HTML id. So I expected this to work:

var inputfield = document.getElementById('{!$Component.secondId}');
if(inputfield != null)
    alert(inputfield.value);

But inputfield would always be null, no matter whether the checkbox had been clicked before, rendering the input field.

This is quite a problem, as it turns out this merge field is not re-evaluated outside of the block that gets re-rendered. Instead, the expression always evaluates to an empty string outside of the conditionally rendered block. So you would need to put all Javascript that needs an id of an element within the conditionally rendered block.

Or – maybe more workable – you could use class names as pseudo-IDs. If you would add

styleClass="pseudoId"

to the input-field you can access it with

var elements = document.getElementsByClassName('pseudoId');
if(elements != null && elements.length > 0) {
    var inputField = elements[0];
    alert(inputField.value);
}

Have a try, and notice how the id of the text input only appears in the conditionally rendered block.
The page:

<apex:page controller="ConditionalRerenderController">
    <apex:form>
        Click me
        <apex:inputCheckbox value="{!condition}" id="firstId">
            <apex:actionSupport event="onclick" rerender="conditionalBlock"/>
        </apex:inputCheckbox><br/>
        firstId: <apex:outputText value="{!$Component.firstId}" /><br/>
        secondId: <apex:outputText value="{!$Component.secondId}" /><br/>

        <apex:pageBlock id="conditionalBlock">
            <apex:pageBlockSection rendered="{!condition}" columns="1">
                <apex:inputText id="secondId" value="{!stringValue}" /><br/>
                secondId: <apex:outputText value="{!$Component.secondId}" /><br/>
            </apex:pageBlockSection>
        </apex:pageBlock>
    </apex:form>
</apex:page>

The controller:

public class ConditionalRerenderController {
 public boolean condition {get;set;}
 public String stringValue {get;set;}
 
 public ConditionalRerenderController() {
 this.condition = false;
 this.stringValue = 'empty';
 }
}