Soft alerts

In several projects I have done, the users don’t want to be bothered with required fields or validation errors on Accounts or Opportunities until they get closer to quotation or order fulfillment. Yet marketing and management reports frequently want ‘categorization’ or ‘taxonomic’ fields filled in (but can live with them blank).

To resolve this in a proactive way and still use SFDC out-of-box page layouts – no VF custom controller or controller extension , I use soft alerts on Account and Opportunity that warn the user that data is not complete and that it should be completed –but it is not urgent that it is done so right this minute.

Here is an example Foo__c object with a soft alert when the Name field contains a ‘colorful’ word
Foo_fubar

This is done with a formula field that leverages the IMAGE() function as shown here:

/* Form of an alert is a set of concatenated strings as follows:

IF(<condition 1>,
IMAGE("/img/samples/flag_yellow|red|green.gif" , 'Yellow|Red|Green' ,12,12) & ' alert text with leading space' & BR() ,
NULL
) &
next condition as above for second alert */


IF (CONTAINS(Name, 'fubar') ,
IMAGE("/img/samples/flag_red.gif" , 'Red' ,12,12) & ' Name contains a colorful slang word' & BR(),
NULL)
&
IF (CONTAINS(Name, 'sylvan') ,
IMAGE("/img/samples/flag_green.gif" , 'Green' ,12,12) & ' Name contains a peaceful word' & BR(),
NULL)

The solution has some limitations as the total size of the formula must be under 5000 characters but for a dozen or so possible alerts, you can avoid using a VF controller. Note the second alert that appears if the Foo__c.name field has a nice word as shown here:
foo_sylvan

What if there are no alerts – how to suppress the custom field on the standard page layout?
If there are no alerts, why waste the valuable line to show nothing? Unfortunately, SFDC standard page layouts don’t permit conditional rendering of fields. Now, you need to escape to Visualforce but the goal is to preserve the advantages of your page layout using apex:detail.

We’ll start with a simple VF page (and we assume that inlineEdit is enabled for your users as it is so convenient):

<apex:page standardcontroller="Foo__c" >
	<apex:pageMessages id="pageMsgs" showDetail="true"/>
	<apex:detail inlineEdit="true" relatedList="true" showChatter="false" subject="{!Foo__c.id}" />	

</apex:page>

In order to suppress the Alerts field, we’ll need to use jQuery to locate the DOM elements and remove them when the value is null. So, here is the expanded page:

<apex:page standardcontroller="Foo__c" >

<apex:includescript value="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js" />

<script>
		$j = jQuery.noConflict();		//	reassign $ to $j to avoid conflict with VF
		var	fooViewJQApp	= {};       //	define the application
		$j(function() {					// 	jQuery shorthand for $(document).ready(function() ..  Wait until page loads 

			(function(app) {			//	Define anon function
				//	--------------------------
				//	variable definitions
				//	--------------------------
				
				//	--------------------------
				//	init:	do initializations
				//	--------------------------
				app.init	= function() {
					app.bindings();		// establish bindings
					app.hideNullAlerts();  // hide the alerts field on page load if null
				};				
				//	-----------------------------------
				//	bindings:  events to event handlers
				//	-----------------------------------
				app.bindings = function() {
					// set up binding for events
                };	// end all bindings				
				
				//	------------------------------------
				//	hideNullAlerts:	Alerts formula field takes up a row and should be suppressed if null (Caption + field)
				//	------------------------------------
				app.hideNullAlerts = function() {
					$j('.labelCol').each(function() {
						if ($j(this).text() == 'Alerts') {		// found the <td> for the caption
							var $alertVal = $j(this).next();	// get adjacent td for the inlineEdit value
							if ($alertVal.text().length == 0) {	// formula field if no value has length 0
								$j(this).parent().remove();			// locate the parent of the alerts label (a tr) remove the <tr> and everything inside it
								return false;						// breaks out of each loop	as we found the alerts field
							}	
						}
					});
				};

				app.init();			// Anon function invokes its own init function
			}) (fooViewJQApp);		// Execute anon function passing in an object representing our application
		});			
	</script>


	<apex:pageMessages id="pageMsgs" showDetail="true"/>
	<apex:detail inlineEdit="true" relatedList="true" showChatter="false" subject="{!Foo__c.id}"  
                     oncomplete="fooViewJQApp.hideNullAlerts();"/>	

</apex:page>

Now, this deserves some explanation:

  1. The jQuery pattern I borrowed from the kittyDressUp pattern in “jQuery Mobile Web Development Essentials” Chapter 7. It provides a nice place to hang all your functions and provide some order and consistency to using jQuery and javascript on your VF page. When you inspect a VF page in Eclipse, it is jarring to see VF markup and javascript interspersed and it can make the page harder to read as there are two different technologies at play working on a common DOM model. I didn’t need to set up any bindings but I left in the placeholder for other future jQuery uses.
  2. When the page loads, the jQuery app’s hideNullAlerts() function is invoked. When the page partial refreshes on an inlineEdit save, the apex:detail oncomplete event is invoked by VF and that too, calls hideNullAlerts().
  3. Using standard page layouts, there is no obvious id field to look for to find the DOM element corresponding to the ‘Alerts’ caption and alerts content field. You could use the SFDC-generated ID but I hate hardcoding ids in anything. Firebug or its equivalent is your friend here to understand the SFDC VF DOM structure for a standard page layout (see next figure). The $j('.labelCol') locates all elements on the page which are captions. Then, each() is used to go through every caption looking for the Alerts caption. Upon locating it (a td tag), the function then looks for the adjacent next sibling (also a td) and tests to see if the result of text() is of length 0. Note that jQuery text() returns the combined contents of the selected element and all descendents. If so, then the alert is null. So, go up one level to the tr and remove it. Since there is only one alert field to remove, stop the each() loop by returning false.

  4. Here is an excerpt of the HTML generated by VF for a standard pagelayout that has inlineEdit enabled. As always, when messing about with the SFDC-generated HTML, you are subject to changes in future releases.

    <table class="detailList" cellspacing="0" cellpadding="0" border="0">
    <tbody>
    <tr>
      <td class="labelCol">Alerts</td>
      <td id="00NK0000001OD2cj_id0_j_id28_ilecell" class="data2Col inlineEditLock" onmouseover="if (window.sfdcPage && window.sfdcPage.hasRun) sfdcPage.mouseOverField(event, this);" onmouseout="if (window.sfdcPage && window.sfdcPage.hasRun) sfdcPage.mouseOutField(event, this);" onfocus="if (window.sfdcPage && window.sfdcPage.hasRun) sfdcPage.mouseOverField(event, this);" ondblclick="if (window.sfdcPage && window.sfdcPage.hasRun) sfdcPage.dblClickField(event, this);" onclick="if (window.sfdcPage && window.sfdcPage.hasRun) sfdcPage.clickField(event, this);" onblur="if (window.sfdcPage && window.sfdcPage.hasRun) sfdcPage.mouseOutField(event, this);" colspan="3">
        <div id="00NK0000001OD2cj_id0_j_id28_ileinner">
         <img width="12" border="0" height="12" alt="Green" src="/img/samples/flag_green.gif">
    Name contains a peaceful word
        <br>
        </div>
      </td>
    </tr>
    <tr>
    <tr>
    <tr>
    </tbody>
    </table>
    

One thought on “Soft alerts

  1. Jay Kilian

    Another way to make the alerts “pop” more would be to use a colored background to the image. It then leaps off the screen. Additionally, the alert images can be added to reports as a way to make a very boring visualization (usually) have some color.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *