In-line Editing Visualforce Component for Any Object

19 May

This is a Visualforce component for in-line editing of an object’s child or related records. It will work for most Salesforce standard and custom objects. Pass in parameters to change what is displayed and if the user can add, edit, or delete records.

In-line Editing Visualforce Component for any object

Required Parameters

  • aParentRecId: Parent Record Id. This is used to select the related object’s records.
  • aRelationField: Field that refers back to the parent. This field is used in the query where condition with the Parent Record Id.
  • asObjectType: Type of child Object.
  • aFieldList: List of fields to display in the pageBlockTable.

Optional Parameters

  • aAllowAdd: Ability to add new records. The Add button will display.
  • aAllowEdit: Ability to edit records. The Edit link will display.
  • aAllowDelete: Ability to delete records. The Del link will display.
  • aLabelOverrideFieldList: List of fields with overridden labels.
  • aLabelOverrideTextList: List of text that overrides the field labels.
  • aDefaultValueFieldList: List of fields used to set default values on added records.
  • aDefaultValueTextList: List of text used to set default values on added records.
  • aBlockTitle: Page block title text.

MultiRecordComponent.component


   
   
   
   
   
   
   
   
   
   
   
   
   


   
      
   
   
      
   

   

   


      

         
            
               
                  
                  
                  
               
            
            
               
                  
                  
                  
               
            
         

      

      
         
            
               Remove
            
            
               
                  
               
                | 
               Del
            
         

         
            
               
                  {!cf.FieldLabel}
               
               
                  

                  
               
            
         
      

   

MultiRecordComponentController.cls

public with sharing class MultiRecordComponentController {
    public Boolean AllowAdd {get; set;}
    public Boolean AllowEdit {get; set;}
    public Boolean AllowDelete {get; set;}
    public String ParentRecId {get; set;}
    public String RelationField {get; set;}
    public String OrderByField {get; set;}
    public list FieldList {get; set;}
    public String sObjectType {get; set;}
    public list LabelOverrideFieldList {get; set;}
    public list LabelOverrideTextList {get; set;}
    private map LabelOverrideMap;
    public list DefaultValueFieldList {get; set;}
    public list DefaultValueTextList {get; set;}
    private map DefaultValueMap;
    public Boolean DisableSave {get; set;}
    public Boolean DisableCancel {get; set;}
    public String ActionId {get; set;}
    public list ObjectList {get; set;}
    private list ColumnWrapList;
    public String ActionRowNumber {get; set;}
    private Integer AddedRowCount;
    
    public MultiRecordComponentController() {
        DisableSave = true;
        DisableCancel = true;
        AddedRowCount = 0;
    }

    /***
    * ColumnList - get/set methods. get initializes columns and list entries on first load
    ***/
    public list ColumnList {
        get {
            if (ColumnWrapList == null) {
                InitValues();
                // load fields for table columns
                ColumnWrapList = LoadColumnList(sObjectType, FieldList, LabelOverrideMap);
                // load records in the table
                ObjectList = LoadObjectList(ParentRecId, sObjectType, FieldList, RelationField, OrderByField);
            }
            return ColumnWrapList;
        }
        set;
    }

    /***
    * InitValues - initialize maps with list data
    ***/
    public void InitValues() {
        // convert field label override lists to a map for easier lookup
        // Salesforce apex:attribute of type map doesn't current work properly.
        // this can updated to a map when/if SF fixes the attribute for maps
        LabelOverrideMap = new map();
        if (LabelOverrideFieldList != null && LabelOverrideTextList != null) {
            system.debug(LabelOverrideFieldList + ':::' + LabelOverrideTextList);
            for (Integer i=0; i < LabelOverrideFieldList.size(); i++) {
                if (i < LabelOverrideTextList.size()) {
                    LabelOverrideMap.put(LabelOverrideFieldList[i], LabelOverrideTextList[i]);
                }
            }
        }
        system.debug('LabelOverrideMap' + LabelOverrideMap);
        
        DefaultValueMap = new map();
        if (DefaultValueFieldList != null && DefaultValueTextList != null) {
            system.debug(DefaultValueFieldList + ':::' + DefaultValueTextList);
            for (Integer i=0; i < DefaultValueFieldList.size(); i++) {
                if (i < DefaultValueTextList.size()) {
                    DefaultValueMap.put(DefaultValueFieldList[i], DefaultValueTextList[i]);
                }
            }
        }
        system.debug('DefaultValueMap' + DefaultValueMap);
    }
    
    /***
    * DoAdd - add a record to the list
    ***/
    public void DoAdd() {
        DisableSave = false;
        DisableCancel = false;
        ObjectWrapper TmpObjWrap = new ObjectWrapper(Schema.getGlobalDescribe().get(sObjectType).newSObject(), true);
        TmpObjWrap.obj.put(RelationField, ParentRecId);
        for (String s : DefaultValueMap.keySet()) {
            TmpObjWrap.obj.put(s, DefaultValueMap.get(s));
        }
        AddedRowCount += 1;
        TmpObjWrap.AddedRowNumber = String.valueOf(AddedRowCount);
        ObjectList.add( TmpObjWrap );
    }

    /***
    * DoCancel - remove added lines and change lines back to display mode
    ***/
    public void DoCancel() {
        DisableSave = true;
        DisableCancel = true;
        for (Integer i=0; i < ObjectList.size(); i++) {
            // remove added lines that were not saved
            if (ObjectList[i].obj.Id == null) {
                ObjectList.remove(i);
                i--;
                continue;
            }
            // change to display mode
            ObjectList[i].IsEditMode = false;
        }
    }
    
    /***
    * DoSave - Save edited and added records. then refresh/requery the list
    ***/
    public void DoSave() {
        DisableSave = true;
        DisableCancel = true;
        list UpdateList = new list();
        list InsertList = new list();
        for (ObjectWrapper o : ObjectList) {
            if (o.IsEditMode == true) {
                if (o.obj.Id == null) {
                    InsertList.add(o.obj);
                } else {
                    UpdateList.add(o.obj);
                }
            }
        }
        System.Savepoint sp1 = Database.setSavepoint();
        try {
            system.debug('UpdateList: ' + UpdateList);
            system.debug('InsertList: ' + InsertList);
            if (UpdateList.size() > 0) {
                update UpdateList;
            }
            if (InsertList.size() > 0) {
                insert InsertList;
            }
        } catch (System.DmlException e) {
            system.debug('error: ' + e);
            Database.rollback(sp1);
            for (Integer i=0; i < e.getNumDml(); i++) {
                ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.Error, e.getDmlMessage(i)));
            }
            return;
        } catch (exception e) {
            system.debug('error: ' + e);
            Database.rollback(sp1);
            ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.Error, 'An error updating the records: ' + e.getMessage()));
            return;
        }
        // requery in case field list contains fields referencing related objects
        if (UpdateList.size() > 0 || InsertList.size() > 0) {
            ObjectList.clear();
            ObjectList = LoadObjectList(ParentRecId, sObjectType, FieldList, RelationField, OrderByField);
        }
    }
    
    /***
    * DoDelete - delete the selected record
    ***/
    public void DoDelete() {
        if (ActionId == null || ActionId.trim().length() == 0) {
            return;
        }
        try {
            database.delete(ActionId);
            for (Integer i=0; i < ObjectList.size(); i++) {
                if (ActionId == ObjectList[i].obj.Id) {
                    ObjectList.remove(i);
                    break;
                }
            }
        } catch (exception e) {
            ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.Error, e.getMessage()));
        }
        ActionId = null;
        return;
    }

    /***
    * DoRemove - remove usaved added rows of the list
    ***/
    public void DoRemove() {
        if (ActionRowNumber == null || ActionRowNumber.trim().length() == 0) {
            return;
        }
        for (Integer i=0; i < ObjectList.size(); i++) {
            if (ActionRowNumber == ObjectList[i].AddedRowNumber) {
                ObjectList.remove(i);
                break;
            }
        }
        ActionRowNumber = null;
        return;
    }
    
    /***
    * DoEdit - dispaly a record with editable fields
    ***/
    public void DoEdit() {
        if (ActionId == null || ActionId.trim().length() == 0) {
            return;
        }
        DisableSave = false;
        DisableCancel = false;
        for (ObjectWrapper o : ObjectList) {
            if (o.obj.Id != null && ActionId == o.obj.Id) {
                o.IsEditMode = true;
                break;
            }
        }
        ActionId = null;
        return;
    }
    
    /***
    * LoadObjectList - query the object, and load results into the object wrapper list
    ***/
    public static list LoadObjectList(String InitRecId, String InitSObj, list InitFieldList, String InitRelField, String InitOrderByField) {
        list ObjWrapList = new list();
        list QueryFieldList = new list();
        set QueryFieldSet = new set();
        
        // add id to field
        QueryFieldList.addAll(InitFieldList);
        QueryFieldSet.addAll(InitFieldList);
        if (QueryFieldSet.contains('id')) {
            QueryFieldList.add('id');
        }
        
        if (InitOrderByField == null || InitOrderByField.trim().length() == 0) {
            InitOrderByField = 'CreatedDate';
        }
        String TmpQuery;
        TmpQuery = 'Select ' + String.escapeSingleQuotes( String.join(QueryFieldList,', ') )+ 
                    ' From ' + String.escapeSingleQuotes( InitSObj ) + 
                    ' Where ' + String.escapeSingleQuotes( InitRelField ) + '=\'' + String.escapeSingleQuotes( InitRecId ) + '\'' + 
                    ' Order by ' + String.escapeSingleQuotes( InitOrderByField ) +
                    ' limit 1000';
        system.debug('Query: ' + TmpQuery);
                
        list TmpObjectList = database.query(TmpQuery);
        for (sObject o : TmpObjectList) {
            ObjWrapList.add(new ObjectWrapper(o, false));
        }
        return ObjWrapList;
    }
    
    /***
    * LoadColumnList - load properties for columns to display into a list
    ***/
    public static list LoadColumnList(String InitSObj, list InitFieldList, map LabelOverrideMap) {
        list TmpColumnList = new list();
        system.debug('sObj:' + InitSObj);
        // map of fields for the object
        map FieldMap = Schema.getGlobalDescribe().get(InitSObj).getDescribe().fields.getMap();
        
        for (String s : InitFieldList) {
            Schema.sObjectField FieldObj;
            Schema.DescribeFieldResult DescField;
            String TmpLabel;
            Boolean TmpIsEditable;
            Boolean TmpIsObjField;
            
            // check override label
            
            // check read only ************************************************************ 
            // defaults
            TmpIsEditable = false;
            TmpIsObjField = false;
            TmpLabel = s;

            // fields of the object retrieve label and permissions, related object fields do not
            FieldObj = FieldMap.get(s);
            if (FieldObj != null) {
                DescField = FieldObj.getDescribe();
                if (DescField != null) {
                    if (DescField.isAccessible() == false) {
                        system.debug('Field: ' + s + ' is not accessable for the user. Field ignored.');
                        continue;
                    }
                
                    if (DescField.isUpdateable() == true && DescField.isCreateable() == true) {
                        TmpIsEditable = true;
                    }
                    TmpLabel = FieldObj.getDescribe().getLabel();
                    TmpIsObjField = true;
                }
            }
            
            // use override label when found
            if (LabelOverrideMap.containsKey(s) == true) {
                    TmpLabel = LabelOverrideMap.get(s);
            }
                
            TmpColumnList.add(new ColumnWrapper(s, TmpLabel, TmpIsEditable, TmpIsObjField));
        }
        system.debug('ColumnList: ' + TmpColumnList);
        return TmpColumnList;
    }
    
    /***
    * ColumnWrapper - subclass for field properties of columns that will be displayed in the list
    ***/
    public class ColumnWrapper { 
        public String FieldName {get; set;}
        public String FieldLabel {get; set;}
        public Boolean IsEditable {get; set;}
        public Boolean IsObjField {get; set;}
        
        public ColumnWrapper(String FieldName, String FieldLabel, Boolean IsEditable, Boolean IsObjField) {
            this.FieldName = FieldName; 
            this.FieldLabel = FieldLabel;
            this.IsEditable = IsEditable;
            this.IsObjField = IsObjField;
        }
    }
    
    /***
    * ObjectWrapper - subclass for the sObject record with additional properties
    ***/
    public class ObjectWrapper { 
        public sObject obj {get; set;}
        public Boolean IsEditMode {get; set;}
        public String AddedRowNumber {get; set;}
        
        public ObjectWrapper(sObject obj, Boolean IsEditMode) {
            this.obj = obj;
            this.IsEditMode = IsEditMode;
        }
    }
}

ExamplePage1.page


   
      
   

ExamplePage1Controller

public with sharing class ExamplePage1Controller {
    public Boolean MyAllowAdd {get; set;}
    public Boolean MyAllowEdit {get; set;}
    public Boolean MyAllowDelete {get; set;}
    public String MysObj {get; set;}
    public String MyRecId {get; set;}
    public String MyRelationField {get; set;}
    public list MyFieldList {get; set;}
    public list MyLabelOverrideFieldList {get; set;}
    public list MyLabelOverrideTextList {get; set;}
    public list MyDefaultValueFieldList {get; set;}
    public list MyDefaultValueTextList {get; set;}
    public String MyBlockTitle {get; set;}
    
    public ExamplePage1Controller() {
        MyAllowAdd = true;
        MyAllowEdit = true;
        MyAllowDelete = true;
        MyBlockTitle = 'In-line Editing of Contacts';
        MysObj = 'Contact';
        MyRecId = '001i000000JqJAa';  // fill in your record Id here
        MyRelationField = 'AccountId';
        MyFieldList = new list {'FirstName',
                                        'LastName',
                                        'Email',
                                        'Phone',
                                        'Account.Name'};
        MyLabelOverrideFieldList = new list { 'Account.Name'};
        MyLabelOverrideTextList = new list {'Account Name'};
        MyDefaultValueFieldList = new list {'FirstName'};
        MyDefaultValueTextList = new list {''};
    }
}

7 Replies to “In-line Editing Visualforce Component for Any Object

  1. This works great! Do you happen to have a test class written for the MultiRecordComponentController.cls though?

    Keep up the great work!

    • Thanks! No test class for it yet. I don’t want to hog all of the fun. If someone finds the component useful and wants to share a test class, I would happily post it.

  2. Dave,
    This component is exactly what I have been looking for to use for several Related Lists on my Quote page. I am new to VF and dont know how to implement. Would you happen to have some instructions that cater to greenbeans?

    • My recommendation is to copy and past the code from the two apex classes and two visualforce pages above, and then experiment with the different parameters. I think the best way for greenbeans is to build knowledge is to dive right in and start creating/modifying applications.

  3. I see in the Apex you reference JS – now do you save this as a static resource or is this something that SFDC reads and doesn’t need to have written in addition to the VF/Apex Controller? I am working though it to get a working copy in our sandbox and just want to understand once it is in our sandbox and we have a test class I will share back the entire sample 😉

    • No static resource is necessary. The DoDeleteJS references an apex:actionFunction which is one way Salesforce provides to execute a controller method with JavaScript.

Comments are closed.