Rerender A Rich Text Field: Hack City

Many of my posts recently have been of a less technical nature, at least as far as developers are likely to be concerned, so this entry falls right on the opposite end of the spectrum to the declarative posts; today we're firmly in Hack City™.

If you use this in a production environment I take no responsibility for any bad things that may happen as a result, including, but not limited to, data-loss, nasty people doing bad things, or your car breaking down.

The Problem

Rich text fields in Salesforce are pretty much text areas which allow restricted HTML tags (i.e. they protect against script tags and the like) to apply formatting to their content. You can't edit the HTML directly in the editor, but a quick SOQL query and you'll see the markup in all it's cut-down glory.

I know the implementation of these fields isn't as simple as that of other field types of the platform, not least because they use an editor (CKEditor and not basic HTML elements, but I was quite astounded when a colleague told me that trying to re-render a rich text field results in an error—it's simply not allowed and/or supported. So naturally, I decided to work out how it could be done.

How To Do It

I'm sure there are plenty of ways of achieving this goal, but mine was to leverage jQuery's wildcard selectors (based on those of CSS 3) to pull the new content from a hidden output field and insert it into the editor. Just to repeat: this is a hack. If Salesforce change the generated markup for these fields it could break, and although I'm confident you can't inject dangerous script into the field or similar, I'm not about to make any guarantees.

In order to create a rather contrived testing scenario, I wrote a trigger which fires before contact records are updated and adds the current date and time to the start of the field's content (there's no smarts involved here, but it does demonstrate the use of HTML in a rich text field).

trigger Contact_BeforeUpdate on Contact (before update)  
{ 
    for(Contact c : trigger.new) 
    { 
        c.Rich_Text_Field__c = '<p>Updated: ' + System.now().format() 
            + '</p></br>' + c.Rich_Text_Field__c; 
    } 
}

With context in place we next create a simple page with the rich text field on it, with the fields we want to re-render in their own <apex:pageBlockSection> so that we can do so without re-rendering the rich text field. If you were to add theRichField to the list in the rerender attribute of the "Save & Edit" button you would see the aforementioned error in play.

<apex:page standardController="Contact">  
    <apex:form > 
        <apex:pageBlock title="Rich Contact Details"> 
            <apex:pageBlockButtons location="bottom"> 
                <apex:commandButton action="{!QuickSave}" value="Save & Edit" rerender="theFields"/> 
                <apex:commandButton action="{!Save}" value="Save & Close"/> 
            </apex:pageBlockButtons> 

            <apex:outputPanel layout="none" id="theFields"> 
                <apex:pageMessages/> 
                <apex:pageBlockSection > 
                    <apex:inputField value="{!Contact.FirstName}"/> 
                    <apex:inputField value="{!Contact.LastName}"/> 
                </apex:pageBlockSection> 
            </apex:outputPanel> 
            
            <apex:pageBlockSection columns="1" id="theRichField"> 
                <apex:inputField value="{!Contact.Rich_Text_Field__c}"/> 
            </apex:pageBlockSection> 
        </apex:pageBlock> 
    </apex:form> 
</apex:page>  

So now the contact information can be edited, but if you hit "Save & Edit" and then view the contact record in another tab, you'll see that Rich_Text_Field__c now has a new time stamp in it, but it doesn't show that on the Visualforce page because it hasn't been re-rendered (I did say this was a contrived example, but I wrote it in response to a real-world scenario).

It's easy enough to get the new value on the page elsewhere, adding an <apex:outputField> which does get re-rendered does the job nicely, and it can be hidden by wrapping it in a <div> which isn't displayed.

<apex:outputPanel layout="none" id="theCode">  
    <div id="hideMe"> 
        <apex:outputField id="hideTheCode" value="{!Contact.Rich_Text_Field__c}"/> 
    </div> 
</apex:outputPanel>  

All you need now is some JavaScript to grab the content of that <apex:outputField> and insert it into the editor, and this is where things get a bit messy.

<script type="text/javascript">  
    function RefreshText() 
    { 
        var newContent = jQuery("#hideMe").find("div[id*=hideTheCode]").html(); 
CKEDITOR.instances[jQuery("*[id$=_Rich_Text_Field__c]").attr("id")].setData(newContent);  
    } 
</script>  

I've split this using a variable just for (some) clarity; the jQuery selector in the first line finds any <div> element with "hideTheCode" somewhere in its ID attribute (the previous snippet shows there's only one, so no worries there). The second line then relies on the fact that the regular CKEditor environment applies, and CKEDITOR.instances["_<< some id >>_"] allows us access to an editor instance, so long as we specify the ID of the text input field that was used to set it up. We don't have this ID to hand, but by viewing the generated source there is only once place in the page where the string "Rich_Text_Field__c" is used in an ID, and it happens to be in the text area we're looking for. By using a wild card in the jQuery selector we can grab that element, and hence, it's full ID. Hopefully the setData() method is self-explanatory.

All that's left to do is to ensure that the panel "theCode" is re-rendered and that our function is called on completion of the command button's ajax request, and the job's a good one.

<apex:commandButton action="{!QuickSave}" value="Save & Edit" rerender="theFields,theCode" oncomplete="RefreshText();"/>  

Related Posts

comments powered by Disqus