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.
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
<apex:component controller="MultiRecordComponentController" allowDML="true"> <style> .cmdLink { font-size: 89%; text-decoration: none; float: left; } .cmdLink:hover { text-decoration: underline; } </style> <apex:attribute name="aParentRecId" description="Parent Record Id" type="String" required="true" assignTo="{!ParentRecId}" /> <apex:attribute name="aRelationField" description="Field that will be assigned the Parent's Record Id" type="String" required="true" assignTo="{!RelationField}" /> <apex:attribute name="asObjectType" description="Type of child Object." type="String" required="true" assignTo="{!sObjectType}" /> <apex:attribute name="aFieldList" description="List of fields to display." type="string[]" required="true" assignTo="{!FieldList}" /> <apex:attribute name="aAllowAdd" description="Ability to add new records." type="Boolean" required="false" assignTo="{!AllowAdd}" /> <apex:attribute name="aAllowEdit" description="Ability to edit records" type="Boolean" required="false" assignTo="{!AllowEdit}" /> <apex:attribute name="aAllowDelete" description="Ability to delete records" type="Boolean" required="false" assignTo="{!AllowDelete}" /> <apex:attribute name="aLabelOverrideFieldList" description="List of fields with overridden labels" type="String[]" required="false" assignTo="{!LabelOverrideFieldList}" /> <apex:attribute name="aLabelOverrideTextList" description="List of text that overrides the field labels" type="String[]" required="false" assignTo="{!LabelOverrideTextList}" /> <apex:attribute name="aDefaultValueFieldList" description="List of fields used to set default values on added records" type="String[]" required="false" assignTo="{!DefaultValueFieldList}" /> <apex:attribute name="aDefaultValueTextList" description="List of text used to set default values on added records" type="String[]" required="false" assignTo="{!DefaultValueTextList}" /> <apex:attribute name="aBlockTitle" description="Page block title text" type="String" required="false" /> <apex:actionFunction name="DoDeleteJS" action="{!DoDelete}" rerender="pbContainer,msgs" immediate="true"> <apex:param name="ActionId" assignto="{!ActionId}" value="" /> </apex:actionFunction> <apex:actionFunction name="DoRemoveJS" action="{!DoRemove}" rerender="pbContainer,msgs" immediate="true"> <apex:param name="ActionRowNumber" assignto="{!ActionRowNumber}" value="" /> </apex:actionFunction> <apex:pageMessages id="msgs" /> <apex:pageBlock id="pbContainer" title="{!aBlockTitle}"> <apex:pageBlockButtons location="top"> <apex:actionStatus id="ButtonStatus"> <apex:facet name="stop"> <apex:outputPanel > <apex:commandButton rerender="pbContainer,msgs" status="ButtonStatus" value="Add" action="{!DoAdd}" immediate="true" rendered="{!AllowAdd}" /> <apex:commandButton rerender="pbContainer,msgs" status="ButtonStatus" value="Save" action="{!DoSave}" rendered="{!OR(AllowEdit,AllowAdd)}" disabled="{!DisableSave}" /> <apex:commandButton rerender="pbContainer,msgs" status="ButtonStatus" value="Cancel" action="{!DoCancel}" immediate="true" rendered="{!OR(AllowEdit,AllowAdd)}" disabled="{!DisableCancel}" /> </apex:outputPanel> </apex:facet> <apex:facet name="start"> <apex:outputPanel > <apex:commandButton value="Processing..." disabled="true" rendered="{!AllowAdd}" /> <apex:commandButton value="Processing..." disabled="true" rendered="{!OR(AllowEdit,AllowAdd)}" /> <apex:commandButton value="Processing..." disabled="true" rendered="{!OR(AllowEdit,AllowAdd)}" /> </apex:outputPanel> </apex:facet> </apex:actionStatus> </apex:pageBlockButtons> <apex:pageBlockTable id="pbTable" value="{!ObjectList}" var="ow"> <apex:column headerValue="Action" width="71px" rendered="{!OR(AllowEdit,AllowDelete,AllowAdd)}"> <apex:outputPanel rendered="{!ISBLANK(ow.obj.Id)}"> <apex:outputLink style="color: #015BA7;" styleClass="cmdLink" value="javascript:DoRemoveJS('{!JSENCODE(ow.AddedRowNumber)}');">Remove</apex:outputLink> </apex:outputPanel> <apex:outputPanel rendered="{!!ISBLANK(ow.obj.Id)}"> <apex:commandLink style="color: #015BA7;" styleClass="cmdLink" value="Edit" action="{!DoEdit}" immediate="true" rendered="{!AllowEdit}" rerender="pbContainer,msgs"> <apex:param name="RecId" value="{!ow.obj.Id}" assignTo="{!ActionId}" /> </apex:commandLink> <apex:outputPanel style="display:inline;float: left; margin: 0 2px 0 2px" rendered="{!AND(AllowEdit,AllowDelete)}"> | </apex:outputPanel> <apex:outputLink style="color: #015BA7;" styleClass="cmdLink" value="javascript:if (window.confirm('Are you sure?')) DoDeleteJS('{!JSENCODE(ow.obj.Id)}');" rendered="{!AllowDelete}">Del</apex:outputLink> </apex:outputPanel> </apex:column> <apex:repeat value="{!ColumnList}" var="cf"> <apex:column width="200"> <apex:facet name="header"> <span>{!cf.FieldLabel}</span> </apex:facet> <apex:outputPanel rendered="{!OR(!ISBLANK(ow.obj['Id']), AND(ISBLANK(ow.obj['Id']),cf.IsObjField))}"> <apex:outputField value="{!ow.obj[cf.FieldName]}" rendered="{!!AND(ow.IsEditMode,cf.IsEditable)}" /> <apex:InputField value="{!ow.obj[cf.FieldName]}" rendered="{!AND(ow.IsEditMode,cf.IsEditable)}" /> </apex:outputPanel> </apex:column> </apex:repeat> </apex:pageBlockTable> </apex:pageBlock> </apex:component> |
MultiRecordComponentController.cls
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 |
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<String> FieldList {get; set;} public String sObjectType {get; set;} public list<String> LabelOverrideFieldList {get; set;} public list<String> LabelOverrideTextList {get; set;} private map<String, String> LabelOverrideMap; public list<String> DefaultValueFieldList {get; set;} public list<String> DefaultValueTextList {get; set;} private map<String, String> DefaultValueMap; public Boolean DisableSave {get; set;} public Boolean DisableCancel {get; set;} public String ActionId {get; set;} public list<ObjectWrapper> ObjectList {get; set;} private list<ColumnWrapper> 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<ColumnWrapper> 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<String, String>(); 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<String, String>(); 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<sObject> UpdateList = new list<sObject>(); list<sObject> InsertList = new list<sObject>(); 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<ObjectWrapper> LoadObjectList(String InitRecId, String InitSObj, list<String> InitFieldList, String InitRelField, String InitOrderByField) { list<ObjectWrapper> ObjWrapList = new list<ObjectWrapper>(); list<String> QueryFieldList = new list<String>(); set<String> QueryFieldSet = new set<String>(); // 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<sObject> 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<ColumnWrapper> LoadColumnList(String InitSObj, list<String> InitFieldList, map<String, String> LabelOverrideMap) { list<ColumnWrapper> TmpColumnList = new list<ColumnWrapper>(); system.debug('sObj:' + InitSObj); // map of fields for the object map<String, Schema.sObjectField> 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
1 2 3 4 5 6 7 8 9 10 11 |
<apex:page controller="ExamplePage1Controller"> <apex:form > <c:MultiRecordComponent aParentRecId="{!MyRecId}" asObjectType="{!MysObj}" aFieldList="{!MyFieldList}" aRelationField="{!MyRelationField}" aLabelOverrideFieldList="{!MyLabelOverrideFieldList}" aLabelOverrideTextList="{!MyLabelOverrideTextList}" aDefaultValueFieldList="{!MyDefaultValueFieldList}" aDefaultValueTextList="{!MyDefaultValueTextList}" aBlockTitle="{!MyBlockTitle}" aAllowAdd="{!MyAllowAdd}" aAllowEdit="{!MyAllowEdit}" aAllowDelete="{!MyAllowDelete}" /> </apex:form> </apex:page> |
ExamplePage1Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
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<String> MyFieldList {get; set;} public list<String> MyLabelOverrideFieldList {get; set;} public list<String> MyLabelOverrideTextList {get; set;} public list<String> MyDefaultValueFieldList {get; set;} public list<String> 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<String> {'FirstName', 'LastName', 'Email', 'Phone', 'Account.Name'}; MyLabelOverrideFieldList = new list<String> { 'Account.Name'}; MyLabelOverrideTextList = new list<String> {'Account Name'}; MyDefaultValueFieldList = new list<String> {'FirstName'}; MyDefaultValueTextList = new list<String> {'<default value>'}; } } |
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.
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.
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.
OMG, this is so awesome! Wow! .. Tinkering now. More later.. Thanks!!