+ Start a Discussion
docbilldocbill 

DetailsEdit component

The following series of posts is an implementation of the idea:

 

https://sites.secure.force.com/success/ideaView?c=09a30000000D9xt&id=08730000000BrQtAAK

 

When viewing the text, the linewrap looks mangled, but it appears the code will cut & paste correctly back into eclipse.   At some point in the future I'll probably offer this on the app exchange.

 

The following is an example of a visual force page for editing accounts with locked fields.

 

 

<apex:page standardController="Account">
<c:DetailsEdit value="{!account}" helpURL="/help/doc/user_ed.jsp?loc=help&target=account_edit.htm&section=Accounts" save="{!save}" cancel="{!cancel}">
<apex:pageBlockSection columns="1">
<apex:repeat value="{!$ObjectType.Account.FieldSets.EditFields}" var="f">
<apex:inputField value="{!account[f]}" />
</apex:repeat>
<apex:repeat value="{!$ObjectType.Account.FieldSets.LockedFields}" var="f">
<apex:outputField value="{!account[f]}"/>
</apex:repeat>
</apex:pageBlockSection>
</c:DetailsEdit>
</apex:page>

 

 

docbilldocbill

DetailsEdit.component:

 

 

<!-- Copyright Red Hat Inc, 2011.  All Rights Reserved. -->
<apex:component controller="DetailsEditController">
  <apex:attribute name="value" description="The Object Being Edited" type="SObject" required="true" assignto="{!value}" />
  <apex:attribute name="help" description="Help Link" type="String" required="false" />
  <apex:attribute name="save" description="Save Action" type="ApexPages.Action" required="true" />
  <apex:attribute name="cancel" description="Cancel Action" type="ApexPages.Action" required="true" />
  <!-- The following attributes only need to be specified if one wishes to use non-standard values. -->
  <apex:attribute name="subtitle" description="Subtitle" type="String" required="false"  assignto="{!subtitle}" />
  <apex:attribute name="entityName" description="The object type being edited (e.g. Account)" type="String" required="false" assignto="{!entityName}" />
  <apex:attribute name="newURL" description="The original url for creating the object. (e.g. /001/e)" type="String" required="false" assignto="{!newURL}" />
  <apex:attribute name="editURL" description="The original url for editing the object. (e.g. /001P0000004OMUU/e)" type="String" required="false" assignto="{!editURL}" />
  <apex:attribute name="missingFieldText" description="The text that will be shown in missing fields." type="String" required="false" assignTo="{!missingFieldText}"/>
  <apex:attribute name="missingFieldAlert" description="True if an alert should be shown for the first missing field." type="Boolean" required="false" assignTo="{!missingFieldAlert}"/>
  <apex:sectionHeader title="{!entityName}" subtitle="{!subtitle}" help="{!help}"/>
  <apex:form id="editForm">
    <apex:pageBlock title="{!entityLabel} Edit" mode="edit" id="Edit">
      <apex:pageBlockButtons >
        <apex:commandButton action="{!save}" value="Save"/>
        <apex:commandButton action="{!saveAndNew}" value="Save & New"/>
        <apex:commandButton action="{!cancel}" value="Cancel"/>
      </apex:pageBlockButtons>
      <apex:pageMessages id="PageMessages"/>
      <c:DetailsEditPageBodySections value="{!value}" entityName="{!entityName}" newURL="{!newURL}" editURL="{!editURL}" missingFieldText="{!missingFieldText}" missingFieldAlert="{!missingFieldAlert}" >
        <apex:componentBody />
      </c:DetailsEditPageBodySections>
    </apex:pageBlock>
  </apex:form>
</apex:component>

 

 

docbilldocbill

DetailsEditPageBodySections.component:

 

 

<!-- Copyright Red Hat Inc, 2011.  All Rights Reserved. -->
<apex:component controller="DetailsEditController" id="HiddenFields" selfClosing="false">
  <apex:attribute name="value" description="The Object Being Edited" type="SObject" required="true" assignto="{!value}" />
  <!-- The following attributes only need to be specified if one wishes to use non-standard values. -->
  <apex:attribute name="entityName" description="The object type being edited (e.g. Account)" type="String" required="false" assignto="{!entityName}" />
  <apex:attribute name="newURL" description="The original url for creating the object. (e.g. /001/e)" type="String" required="false" assignto="{!newURL}" />
  <apex:attribute name="editURL" description="The original url for editing the object. (e.g. /001P0000004OMUU/e)" type="String" required="false" assignto="{!editURL}" />
  <apex:attribute name="missingFieldText" description="The text that will be shown in missing fields." type="String" required="false" assignTo="{!missingFieldText}"/>
  <apex:attribute name="missingFieldAlert" description="True if an alert should be shown for the first missing field." type="Boolean" required="false" assignTo="{!missingFieldAlert}"/>
  <apex:includeScript value="{!$Resource.DetailEdit}"/>
  <div style="display:none" id="{!$Component.HiddenFields}:HiddenBlock">
    <apex:componentBody />
  </div>
  <script type="text/javascript">
    detailsEdit('{!$Component.HiddenFields}:HiddenBlock','{!JSENCODE(pageContents)}','{!JSENCODE(missingFieldText)}',{!missingFieldAlert});
  </script>
</apex:component>

 

 

docbilldocbill

DetailEdit.resource:

 

 

// <!-- Copyright Red Hat Inc, 2011.  All Rights Reserved. -->
// Parse the page contents of a standard edit page, replacing the fields
// with the ones in a hidden fields block.  The general assumptions are
// that the edit form has an id of 'editPage', the fields of interest are
// inside a div with css class 'pbBody', and inside that we can exclude div
// elements with class 'pbError'.  We also assume label cells either contain
// a label tag, or they use the 'labelCol' css class. 
//
// @param hiddenFieldsId  the id of the div containing the hidden pageblocksection.
// @param pageContents    the page contents of the standard edit page
// @param missingFieldText  the text shown for missing fields
// @param missingFieldAlert  if not false an alert will be shown for the first missing field
//
function detailsEdit(hiddenFieldsId,pageContents,missingFieldText,missingFieldAlert) {

  // First parse through all the hidden fields to map the labels elements by value
  var hiddenLabelCellMap = new Object();
  var hiddenFields = document.getElementById(hiddenFieldsId);
  var hiddenCells = hiddenFields.getElementsByTagName('td');
  for(var i=0;i<hiddenCells.length;) {
    var hiddenLabelCell = hiddenCells[i++];
    // find the label
    var hiddenLabels = hiddenLabelCell.getElementsByTagName('label');
    if(hiddenLabels.length > 0) {
      var key=hiddenLabels[0].innerHTML.replace(/<span.*span>/,'');
      hiddenLabelCellMap[key] = hiddenLabelCell; // add the label to the map
    }
    else if (hiddenLabelCell.getAttribute('class') == 'labelCol' || hiddenLabelCell.getAttribute('className') == 'labelCol') {
      var key = hiddenLabelCell.innerHTML;
      hiddenLabelCellMap[key] = hiddenLabelCell; // add the label to the map
    }
  }

  // Now create an element to parse the page contents
  var pcNode = document.createElement('div');
  pcNode.innerHTML = pageContents;

  // Find the 'editPage' form in the pageContents
  var formNodes = pcNode.getElementsByTagName('form');
  var formNode = null;
  for(var i=0;i<formNodes.length;i++) {
    formNode = formNodes[i];
    if(formNode.getAttribute('id') == 'editPage') {
      break;
    }
  }

  // Now we need to actually make the page content nodes we will make visible.
  // Assumption everything of is inside the first div element with class of 
  //'pbBody'.
  var errorCount=0;
  var divs = formNode.getElementsByTagName('div');
  for(var i=0;i<divs.length;i++) {
    var div = divs[i];
    // if this div has a class of 'pbBody' it is the one we are looking for
    if(div.getAttribute('class') == 'pbBody' || div.getAttribute('className') == 'pbBody') {
      // add all the child nodes to the page, except those with div elements
      // with a class of 'pbError'.
      while(div.hasChildNodes()) {
        var n = div.firstChild;
        if(n.nodeName.toLowerCase() != 'div' || (n.getAttribute('class') != 'pbError'||n.getAttribute('className') != 'pbError')) {
          // Parse the labels in the page contents
          // We need to loop backwards, since the labels will be
          // destroyed in the loop, possibly modifying the indexes
          // of the list.
          var visibleLabels = n.getElementsByTagName('label');
          for(var j=visibleLabels.length;j-- > 0;) {
            // Eventually we will make this stuff visible
            var visibleLabel = visibleLabels[j];
            // If a label does not have 'for' attribute, we assume it is a read 
            // only field.  In that case there is no need to copy the hidden data.
            if(visibleLabel.getAttribute('for')||visibleLabel.getAttribute('htmlFor')) {
              var visibleLabelCell = visibleLabel.parentNode;
              var visibleDataCell = visibleLabelCell.nextSibling;
              if(visibleDataCell) {
                var key=visibleLabel.innerHTML.replace(/<span.*span>/,'')
                var hiddenLabelCell = hiddenLabelCellMap[key];
                if(typeof hiddenLabelCell == 'undefined') {
                  if(errorCount++ < 1 && missingFieldAlert != false) {
                    alert(j+' failed to find: "'+key+'" in hidden fields');
                  }
                  visibleDataCell.innerHTML='<b>'+missingFieldText+'</b>';
                }
                else {
                  // we clear the map entry to avoid using the same fields twice
                  delete hiddenLabelCellMap[key];
                  // Copy the hidden label cell contents into the visible cell
                  while(visibleLabelCell.hasChildNodes()) {
                    visibleLabelCell.removeChild(visibleLabelCell.lastChild);
                  }
                  while(hiddenLabelCell.hasChildNodes()) {
                    var child = hiddenLabelCell.firstChild.cloneNode(true);
                    hiddenLabelCell.removeChild(hiddenLabelCell.firstChild);
                    visibleLabelCell.appendChild(child);
                  }
                  // next parse the data cell
                  var hiddenDataCell = hiddenLabelCell.nextSibling;
                  // we'll want to keep some formatting attributes from the 
                  // visible cells while removing the elements.
                  var colsArray = new Array();
                  var rowsArray = new Array();
                  var sizeArray = new Array();
                  while(visibleDataCell.hasChildNodes()) {
                    var child = visibleDataCell.firstChild;
                    switch(child.nodeName.toLowerCase()) {
                      case 'textarea':
                        colsArray.push(child.getAttribute('cols'));
                        rowsArray.push(child.getAttribute('rows'));
                        break;
                      case 'input':
                        sizeArray.push(child.getAttribute('size'));
                        break;
                    }
                    visibleDataCell.removeChild(child);
                  }
                  // Add all the hidden data cell elements to the visible data cell
                  // and apply the saved attributes.
                  while(hiddenDataCell.hasChildNodes()) {
                    var child = hiddenDataCell.firstChild.cloneNode(true);
                    hiddenDataCell.removeChild(hiddenDataCell.firstChild);
                    visibleDataCell.appendChild(child);
                    switch(child.nodeName.toLowerCase()) {
                      case 'textarea':
                        colsArray.push(null);
                        var cols = colsArray.shift();
                        if(cols != null) {
                          child.setAttribute('cols',cols);
                        }
                        rowsArray.push(null);
                        var rows = rowsArray.shift();
                        if(rows != null) {
                          child.setAttribute('rows',rows);
                        }
                        break;
                      case 'input':
                        sizeArray.push(null);
                        var size = sizeArray.shift();
                        if(size != null) {
                          size = child.getAttribute('size');
                        }
                        break;
                    }
                  }
                  // Finally, remove the hiddenRow, so we do not submit two sets
                  // of the data, or have multiple elements with the same id values.
                  var hiddenRow = hiddenLabelCell.parentNode;
                  hiddenRow.parentNode.removeChild(hiddenRow);
                }
              }
            }
          }
          // insert the element in the document to make it visible
          hiddenFields.parentNode.insertBefore(n.cloneNode(true),hiddenFields);
        }
        div.removeChild(n);
      }
      break;
    }
  }
  delete pcNode;
}

 

 

docbilldocbill

DetailsEditController.cls:

 

 

// <!-- Copyright Red Hat Inc, 2011.  All Rights Reserved. -->
public with sharing class DetailsEditController {
  public static final String START_PAGE_CONTENT = '<!-- Start page content -->';
  public static final String END_PAGE_CONTENT = '<!-- End page content -->';
  public static Blob testContents = null;

  public static Boolean hasMessages(ApexPages.Severity severity,List<ApexPages.Message> messages) {
    for(ApexPages.Message message : messages) {
      System.debug('hasMessages '+message);
      if (message.getSeverity() == severity) {
        return true;
      }
    }
    return false;
  }
  
  public static Boolean hasErrors() {
    return hasMessages(ApexPages.Severity.ERROR,ApexPages.getMessages());
  }

  private static String getContents(PageReference pr) {
    Blob content = testContents;
    if(content == null) {
      content = pr.getContent();
    }
    String retval = null;
    if(content != null) {
      retval = content.toString();
    }
    return retval;
  }

  public SObject value {
    get;
    set;
  }

  public Boolean missingFieldAlert {
    get {
      if(missingFieldAlert == null) {
        missingFieldAlert = true;
      }
      return missingFieldAlert;
    }
    set;
  }

  public String missingFieldText {
    get {
      if(missingFieldText == null) {
        missingFieldText = 'MISSING FIELD';
      }
      return missingFieldText;
    }
    set;
  }

  private transient Schema.DescribeSObjectResult valueDescribeResults;
  
  private Schema.DescribeSObjectResult getDescribeResults() {
    if(valueDescribeResults == null && value != null) {
      valueDescribeResults = value.getSObjectType().getDescribe();
    }
    return valueDescribeResults;
  }

  public String entityName {
    get {
      if(entityName == null && value != null) {
        entityName = getDescribeResults().getName();
      }
      return entityName;
    }
    set;
  }

  public String entityLabel {
    get {
      if(entityLabel == null && value != null) {
        entityLabel = getDescribeResults().getLabel();
      }
      return entityLabel;
    }
    set;
  }

  public String newURL {
    get {
      if(newURL == null && entityName != null) {
        newURL = '/'+getDescribeResults().getKeyPrefix()+'/e';
      }
      return newURL;
    }
    set;
  }

  public String editURL {
    get {
      if(editURL == null && value != null) {
        editURL = '/'+value.Id+'/e';
      }
      return editURL;
    }
    set;
  }

  public String subtitle {
    get {
      if(subtitle == null && value != null) {
        subtitle = (String)value.get('Name');
      }
      return subtitle;
    }
    set;
  }

  public String htmlText {
    get {
      if(htmlText == null) {
        PageReference pr;
        if(value.Id != null) {
          pr = new PageReference(editURL);
        }
        else {
          pr = new PageReference(newURL);
          pr.getParameters().put('ent',entityName);
          Id recordTypeId = (Id)value.get('RecordTypeId');
          if(recordTypeId != null) {
            pr.getParameters().put('RecordType',(String)recordTypeId);
          }
        }
        pr.getParameters().put('retURL',ApexPages.currentPage().getParameters().get('retURL'));
        pr.getParameters().put('nooverride','1');
        htmlText=getContents(pr);
      }
      return htmlText;
    }
    set {
      htmlText = value;
      pageContents = null;
    }
  }

  public String pageContents {
    get {
      if(pageContents == null) {
        // Strictly speaking, we could just return htmlText and it will work.
        // However, it is probably better to filter for just the conntents
        // we want.
        Integer contentStart=htmlText.indexOf(START_PAGE_CONTENT);
        Integer contentEnd = contentStart;
        if(contentStart >= 0) {
          contentStart+=START_PAGE_CONTENT.length();
          contentEnd=htmlText.lastIndexOf(END_PAGE_CONTENT);
        }
        if(contentEnd <= contentStart) {
          contentEnd = contentStart = htmlText.indexOf('<body');
          if(contentStart > 0) {
            contentStart = htmlText.indexOf('>',contentStart)+1;
            if(contentStart > 0) {
              contentEnd = htmlText.lastIndexOf('</body>');
            }
          }
        }
        if(contentEnd > contentStart) {
          pageContents = htmlText.substring(contentStart,contentEnd);
        }
        else {
          pageContents = htmlText;
        }
      }
      return pageContents;
    }
    set;
  }

  public PageReference saveAndNew() {
    ApexPages.Action saveAction = new ApexPages.Action('{!save}');
    PageReference retval = saveAction.invoke();
    if(! hasErrors()) {
      retval = new PageReference('/setup/ui/recordtypeselect.jsp');
      String retURL=ApexPages.currentPage().getParameters().get('retURL');
      retval.getParameters().put('retURL',retURL);
      retval.getParameters().put('save_new_url',newURL);
      retval.getParameters().put('ent',entityName);
      retval.setRedirect(true);
    }
    return retval;
  }
}

 

 

docbilldocbill

DetailsEditControllerTest.cls:

 

 

// <!-- Copyright Red Hat Inc, 2011.  All Rights Reserved. -->
public with sharing class DetailsEditController {
  public static final String START_PAGE_CONTENT = '<!-- Start page content -->';
  public static final String END_PAGE_CONTENT = '<!-- End page content -->';
  public static Blob testContents = null;

  public static Boolean hasMessages(ApexPages.Severity severity,List<ApexPages.Message> messages) {
    for(ApexPages.Message message : messages) {
      System.debug('hasMessages '+message);
      if (message.getSeverity() == severity) {
        return true;
      }
    }
    return false;
  }
  
  public static Boolean hasErrors() {
    return hasMessages(ApexPages.Severity.ERROR,ApexPages.getMessages());
  }

  private static String getContents(PageReference pr) {
    Blob content = testContents;
    if(content == null) {
      content = pr.getContent();
    }
    String retval = null;
    if(content != null) {
      retval = content.toString();
    }
    return retval;
  }

  public SObject value {
    get;
    set;
  }

  public Boolean missingFieldAlert {
    get {
      if(missingFieldAlert == null) {
        missingFieldAlert = true;
      }
      return missingFieldAlert;
    }
    set;
  }

  public String missingFieldText {
    get {
      if(missingFieldText == null) {
        missingFieldText = 'MISSING FIELD';
      }
      return missingFieldText;
    }
    set;
  }

  private transient Schema.DescribeSObjectResult valueDescribeResults;
  
  private Schema.DescribeSObjectResult getDescribeResults() {
    if(valueDescribeResults == null && value != null) {
      valueDescribeResults = value.getSObjectType().getDescribe();
    }
    return valueDescribeResults;
  }

  public String entityName {
    get {
      if(entityName == null && value != null) {
        entityName = getDescribeResults().getName();
      }
      return entityName;
    }
    set;
  }

  public String entityLabel {
    get {
      if(entityLabel == null && value != null) {
        entityLabel = getDescribeResults().getLabel();
      }
      return entityLabel;
    }
    set;
  }

  public String newURL {
    get {
      if(newURL == null && entityName != null) {
        newURL = '/'+getDescribeResults().getKeyPrefix()+'/e';
      }
      return newURL;
    }
    set;
  }

  public String editURL {
    get {
      if(editURL == null && value != null) {
        editURL = '/'+value.Id+'/e';
      }
      return editURL;
    }
    set;
  }

  public String subtitle {
    get {
      if(subtitle == null && value != null) {
        subtitle = (String)value.get('Name');
      }
      return subtitle;
    }
    set;
  }

  public String htmlText {
    get {
      if(htmlText == null) {
        PageReference pr;
        if(value.Id != null) {
          pr = new PageReference(editURL);
        }
        else {
          pr = new PageReference(newURL);
          pr.getParameters().put('ent',entityName);
          Id recordTypeId = (Id)value.get('RecordTypeId');
          if(recordTypeId != null) {
            pr.getParameters().put('RecordType',(String)recordTypeId);
          }
        }
        pr.getParameters().put('retURL',ApexPages.currentPage().getParameters().get('retURL'));
        pr.getParameters().put('nooverride','1');
        htmlText=getContents(pr);
      }
      return htmlText;
    }
    set {
      htmlText = value;
      pageContents = null;
    }
  }

  public String pageContents {
    get {
      if(pageContents == null) {
        // Strictly speaking, we could just return htmlText and it will work.
        // However, it is probably better to filter for just the conntents
        // we want.
        Integer contentStart=htmlText.indexOf(START_PAGE_CONTENT);
        Integer contentEnd = contentStart;
        if(contentStart >= 0) {
          contentStart+=START_PAGE_CONTENT.length();
          contentEnd=htmlText.lastIndexOf(END_PAGE_CONTENT);
        }
        if(contentEnd <= contentStart) {
          contentEnd = contentStart = htmlText.indexOf('<body');
          if(contentStart > 0) {
            contentStart = htmlText.indexOf('>',contentStart)+1;
            if(contentStart > 0) {
              contentEnd = htmlText.lastIndexOf('</body>');
            }
          }
        }
        if(contentEnd > contentStart) {
          pageContents = htmlText.substring(contentStart,contentEnd);
        }
        else {
          pageContents = htmlText;
        }
      }
      return pageContents;
    }
    set;
  }

  public PageReference saveAndNew() {
    ApexPages.Action saveAction = new ApexPages.Action('{!save}');
    PageReference retval = saveAction.invoke();
    if(! hasErrors()) {
      retval = new PageReference('/setup/ui/recordtypeselect.jsp');
      String retURL=ApexPages.currentPage().getParameters().get('retURL');
      retval.getParameters().put('retURL',retURL);
      retval.getParameters().put('save_new_url',newURL);
      retval.getParameters().put('ent',entityName);
      retval.setRedirect(true);
    }
    return retval;
  }
}

 

 

DanielJimenezDanielJimenez
Thanks so much for posting this! It worked great! I also noticed the class and the test class look the same? Mispost perhaps? Either way, thanks!!!

Daniel
docbilldocbill

Looks like you are right.  Here is the correct for for the DetailsEditControllerTest.cls:

 

// <!-- Copyright Red Hat Inc, 2011.  All Rights Reserved. -->
/**
 * This class contains unit tests for validating the behavior of Apex classes
 * and triggers.
 *
 * Unit tests are class methods that verify whether a particular piece
 * of code is working properly. Unit test methods take no arguments,
 * commit no data to the database, and are flagged with the testMethod
 * keyword in the method definition.
 *
 * All test methods in an organization are executed whenever Apex code is deployed
 * to a production organization to confirm correctness, ensure code
 * coverage, and prevent regressions. All Apex classes are
 * required to have at least 75% code coverage in order to be deployed
 * to a production organization. In addition, all triggers must have some code coverage.
 * 
 * The @isTest class annotation indicates this class only contains test
 * methods. Classes defined with the @isTest annotation do not count against
 * the organization size limit for all Apex scripts.
 *
 * See the Apex Language Reference for more information about Testing and Code Coverage.
 */
@isTest
private class DetailsEditControllerTest {
	static Account testAccount {
		get {
			if(testAccount == null) {
				try {
					Id recordTypeId = [
						select Id
						from RecordType
						where SObjectType = 'Account' limit 1].Id;
					testAccount = new Account(Name='test testAccount',RecordTypeId=recordTypeId);
					insert testAccount;
				}
				catch(Exception e) {
					for(Account a : [select RecordTypeId from Account where RecordTypeId != null limit 1]) {
						testAccount = a;
					}
				}
			}
			return testAccount;
		}
		set;
	}

	static testMethod void editTest() {
		DetailsEditController c = new DetailsEditController();
		c.value = testAccount;
		System.assertEquals('Account',c.entityName);
		System.assertEquals('/'+testAccount.Id+'/e',c.editURL);
		String bodyText='<p>this is body contents 1</p>';
		c.htmlText = null;
		DetailsEditController.testContents=Blob.valueOf('<html><head><title>foo</title></head><body>'+bodyText+'</body></html>');
		System.assertEquals(bodyText,c.pageContents);
	}

	static testMethod void createTest() {
		DetailsEditController c = new DetailsEditController();
		c.value = testAccount.clone(false,true);
		System.assertEquals('Account',c.entityName);
		System.assertEquals('/001/e',c.newURL);
		String bodyText2='<p>this is body contents 2</p>';
		c.htmlText = null;
		DetailsEditController.testContents=Blob.valueOf('<html><head><title>foo</title></head><body>'
			+ DetailsEditController.START_PAGE_CONTENT
			+ bodyText2
			+ DetailsEditController.END_PAGE_CONTENT
			+ '</body></html>');
		System.assertEquals(bodyText2,c.pageContents);
	}
}

 

Note: We decided not to use this in our organization, because we are worried what happens if salesforce changes the page format so that the javascript that formats the page suddenly stops working.