How to use fieldsets with Lightning?

  • Been trying to figure out how to create a salesforce Lightning input form, based on fields from a fieldset. As an aura enabled apex method is not allowed to pass an entire fieldset I tried by reforming it to a JSON string myself.

    It kinda works but doesnt feel like I am doing it the right way at all. Anyone can tell what the proper way to display fields from a fieldset in Lightning is ?

    Component:

    < aura:component controller="ns.myController" >
      < aura:handler name="init" value="{!this}" action="{!c.getForm}" />
        <div class="fields">        
            <form aura:id="myForm">
            </form>
        </div>    
    < /aura:component>
    

    Clientside controller:

    getForm : function(component) {
        var action = component.get("c.getFieldSet");  
        var self = this;
        var inputOutput = 'input';
        action.setCallback(this, function(a) {
            var jsonFieldset = JSON.parse(a.getReturnValue());
            for(var x=0,xlang=jsonFieldset.length;x<xlang;x++){
                fieldDef = jsonFieldset[x]; 
                this.createField(component,fieldDef.label,inputOutput+fieldDef.type,fieldDef.name,fieldDef.required);
            }
        });
        $A.enqueueAction(action);
    },
    
    createField : function(component,fieldName,fieldType,fieldId,fieldRequired) {
        $A.componentService.newComponentAsync(this,
           function(newField){                                                                        
                myForm = component.find('myForm');
                var body = myForm.get("v.body");  
                body.push(newField);                                
                myForm.set("v.body",body);
            },
            {
                "componentDef": "markup://ui:"+fieldType,
                "localId": "fieldId",
                "attributes": {
                    "values": { label: fieldName,
                               displayDatePicker:true,
                               required:fieldRequired
                              }
                }
            }
        );
    }
    

    Serverside controller:

    static Map<String,String> auraTypes {get; set;}
    
    public static Map<String,String> getAuraTypes() {
        if(auraTypes!=null) {
            return auraTypes;
        }
        else {
            auraTypes = new Map<String,String>();
            auraTypes.put('BOOLEAN','Checkbox');
            auraTypes.put('DATE','Date');
            auraTypes.put('DATETIME','DateTime');
            auraTypes.put('EMAIL','Email');
            auraTypes.put('NUMBER','Number');
            auraTypes.put('PHONE','Phone');
            auraTypes.put('STRING','Text');            
        }
        return auraTypes;
    }
    
    
    @AuraEnabled    
    public static String getFieldSet() {
        String result = '';
        List<Schema.FieldSetMember> fieldset =  SObjectType.Kandidaat__c.FieldSets.formulier.getFields();
        for(Schema.FieldSetMember f : fieldset) {
            if(result!=''){
                result += ',';
            }
            String jsonPart = '{';
            jsonPart += '"label":"'+f.getLabel()+'",';
            jsonPart += '"required":"'+(f.getDBRequired() || f.getRequired())+'",';
            jsonPart += '"type":"'+getAuraTypes().get((f.getType()+'')) +'",';
            jsonPart += '"name":"'+f.getFieldPath()+'"';
            jsonPart += '}';
            result +=jsonPart;
        }
        return '['+result+']';
    }
    

    I know Skip Sauls was looking into this - building out an example etc - for you last night - not sure on an ETA but wanted to let you know he is actively researching this.

  • Skip Sauls

    Skip Sauls Correct answer

    7 years ago

    As Doug states, I started looking into this, and ended up with a bigger test/demo/etc. than I originally planned. I took the "wrapper" class approach, starting with an AuraEnabled version of FieldSetMember in FieldSetMember.apx:

    public class FieldSetMember {
    
        public FieldSetMember(Schema.FieldSetMember f) {
            this.DBRequired = f.DBRequired;
            this.fieldPath = f.fieldPath;
            this.label = f.label;
            this.required = f.required;
            this.type = '' + f.getType();
        }
    
        public FieldSetMember(Boolean DBRequired) {
            this.DBRequired = DBRequired;
        }
    
        @AuraEnabled
        public Boolean DBRequired { get;set; }
    
        @AuraEnabled
        public String fieldPath { get;set; }
    
        @AuraEnabled
        public String label { get;set; }
    
        @AuraEnabled
        public Boolean required { get;set; }
    
        @AuraEnabled
        public String type { get; set; }
    }
    

    This is used in FieldSetController.apxc, which can do things like a) get the names of object types that have field sets and b) get the fields for the field set as a list of FieldSetMember:

    public class FieldSetController {
    
        @AuraEnabled
        public static List<String> getTypeNames() {
            Map<String, Schema.SObjectType> types = Schema.getGlobalDescribe();
            List<String> typeNames = new List<String>();
            String typeName = null;
            List<String> fsNames;
            for (String name : types.keySet()) {
                if (hasFieldSets(name)) {
                    typeNames.add(name);        
                }
            }
            return typeNames;
        }
    
        @AuraEnabled
        public static Boolean hasFieldSets(String typeName) {
            Schema.SObjectType targetType = Schema.getGlobalDescribe().get(typeName);
            Schema.DescribeSObjectResult describe = targetType.getDescribe();
            Map<String, Schema.FieldSet> fsMap = describe.fieldSets.getMap();
            return !fsMap.isEmpty();
        }
    
        @AuraEnabled
        public static List<String> getFieldSetNames(String typeName) {
            Schema.SObjectType targetType = Schema.getGlobalDescribe().get(typeName);
            Schema.DescribeSObjectResult describe = targetType.getDescribe();
            Map<String, Schema.FieldSet> fsMap = describe.fieldSets.getMap();
            List<String> fsNames = new List<String>();
            for (String name : fsMap.keySet()) {
                fsNames.add(name);
            }
            return fsNames;
        }
    
        @AuraEnabled
        public static List<FieldSetMember> getFields(String typeName, String fsName) {
            Schema.SObjectType targetType = Schema.getGlobalDescribe().get(typeName);
            Schema.DescribeSObjectResult describe = targetType.getDescribe();
            Map<String, Schema.FieldSet> fsMap = describe.fieldSets.getMap();
            Schema.FieldSet fs = fsMap.get(fsName);
            List<Schema.FieldSetMember> fieldSet = fs.getFields();
            List<FieldSetMember> fset = new List<FieldSetMember>();
            for (Schema.FieldSetMember f: fieldSet) {
                fset.add(new FieldSetMember(f));
            }
            return fset;
        }
    }
    

    The controller is used in fsTest.cmp

    <aura:component implements="force:appHostable" controller="aotp1.FieldSetController">
        <aura:attribute name="values" type="Object[]"/>
        <aura:attribute name="form" type="Aura.Component[]"/>
        <aura:attribute name="types" type="String[]"/>
        <aura:attribute name="type" type="String" default="NA"/>
        <aura:attribute name="fsNames" type="String[]"/>
        <aura:attribute name="fsName" type="String" default="NA"/>
        <aura:handler name="init" value="{!this}" action="{!c.doInit}" />
    
        <div class="container">
            <div class="row">
                <div class="section">
                    <div class="title">
                        <a href="javascript:void(0);" onclick="{!c.doGetTypeNames}" class="refresh">&#x21bb;</a>
                        SObjects with FieldSets
                    </div>
                    <dl class="cell list">
                        <aura:iteration items="{!v.types}" var="type">
                            <dd><a href="javscript:void(0);" onclick="{!c.doSelectType}" name="{!type}">{!type}</a></dd>
                        </aura:iteration>
                    </dl>
                </div>
                <div class="section">
                    <div class="title">
                        FieldSet Names for {!v.type}
                    </div>
                    <dl class="cell list">
                        <aura:iteration items="{!v.fsNames}" var="name">
                            <dd><a href="javscript:void(0);" onclick="{!c.doSelectFieldSet}" name="{!name}">{!name}</a></dd>
                        </aura:iteration>
                    </dl>
                </div>
            </div>
            <div class="row">
                <div class="section">
                    <div class="title">
                        Form for {!v.type} with {!v.fsName} fieldset
                    </div>
                    <div class="controls">
                        <ui:button label="Test Submit" press="{!c.doSubmit}"/>
                    </div>
                    <div class="cell form">
                        {!v.form}                    
                    </div>
                </div>
                <div class="section">
                    <div class="title">
                        Data Binding for {!v.type} with {!v.fsName} fieldset
                    </div>
                    <div class="cell test">
                        <aura:iteration items="{!v.values}" var="item">
                            <div>
                                <span>{!item.name}</span>: <span>{!item.value}</span>
                            </div>
                        </aura:iteration>
                    </div>
                </div>
            </div>        
        </div>
    </aura:component>
    

    The associated controller, fsTesdtController.js, mainly handles events:

    ({
        doInit: function(component, event, helper) {
            //helper.getFields(component, event);
        },
    
        doGetTypeNames: function(component, event, helper) {
            helper.getTypeNames(component, event);
        },
    
        doSelectType: function(component, event, helper) {
            var type = event.target.getAttribute("name");
            helper.selectType(component, type);
        },
    
        doSelectFieldSet: function(component, event, helper) {
            var fsName = event.target.getAttribute("name");
            helper.selectFieldSet(component, fsName);
        },
    
        doSubmit: function(component, event, helper) {
            helper.submitForm(component, event);
        }
    })
    

    The helper, fsTestHelper.js, constructs the UI to display the objects with fields sets, the field sets for the selected object, a generated form, and a test area to demonstrate data-binding:

    ({
    
        /*
         *  Map the Schema.FieldSetMember to the desired component config, including specific attribute values
         *  Source: https://www.salesforce.com/us/developer/docs/apexcode/index_Left.htm#CSHID=apex_class_Schema_FieldSetMember.htm|StartTopic=Content%2Fapex_class_Schema_FieldSetMember.htm|SkinName=webhelp
         *
         *  Change the componentDef and attributes as needed for other components
         */
        configMap: {
            "anytype": { componentDef: "markup://ui:inputText" },
            "base64": { componentDef: "markup://ui:inputText" },
            "boolean": {componentDef: "markup://ui:inputCheckbox" },
            "combobox": { componentDef: "markup://ui:inputText" },
            "currency": { componentDef: "markup://ui:inputText" },
            "datacategorygroupreference": { componentDef: "markup://ui:inputText" },
            "date": { componentDef: "markup://ui:inputDate" },
            "datetime": { componentDef: "markup://ui:inputDateTime" },
            "double": { componentDef: "markup://ui:inputNumber", attributes: { values: { format: "0.00"} } },
            "email": { componentDef: "markup://ui:inputEmail" },
            "encryptedstring": { componentDef: "markup://ui:inputText" },
            "id": { componentDef: "markup://ui:inputText" },
            "integer": { componentDef: "markup://ui:inputNumber", attributes: { values: { format: "0"} } },
            "multipicklist": { componentDef: "markup://ui:inputText" },
            "percent": { componentDef: "markup://ui:inputNumber", attributes: { values: { format: "0"} } },
            "picklist": { componentDef: "markup://ui:inputText" },
            "reference": { componentDef: "markup://ui:inputText" },
            "string": { componentDef: "markup://ui:inputText" },
            "textarea": { componentDef: "markup://ui:inputText" },
            "time": { componentDef: "markup://ui:inputDateTime", attributes: { values: { format: "h:mm a"} } },
            "url": { componentDef: "markup://ui:inputText" }
        },
    
        // Adds the component via newComponentAsync and sets the value handler
        addComponent: function(component, facet, config, fieldPath) {
            $A.componentService.newComponentAsync(this, function(cmp) {
                cmp.addValueHandler({
                    value: "v.value",
                    event: "change",
                    globalId: component.getGlobalId(),
                    method: function(event) {
                        var values = component.get("v.values");
                        for (var i = 0; i < values.length; i++) {
                            if (values[i].name === fieldPath) {
                                values[i].value = event.getParams().value;
                            }
                        }
                        component.set("v.values", values);
                    }
                });
    
                facet.push(cmp);
            }, config);
        },
    
        // Create a form given the set of fields
        createForm: function(component, fields) {
            var field = null;
            var cmp = null;
            var def = null;
            var config = null;
            var self = this;
    
            // Clear any existing components in the form facet
            component.set("v.form", []);
    
            var facet = component.getValue("v.form");
            var values = [];
            for (var i = 0; i < fields.length; i++) {
                field = fields[i];
                // Copy the config, note that this type of copy may not work on all browsers!
                config = JSON.parse(JSON.stringify(this.configMap[field.type.toLowerCase()]));
                // Add attributes if needed
                config.attributes = config.attributes || {};
                // Add attributes.values if needed
                config.attributes.values = config.attributes.values || {};
    
                // Set the required and label attributes
                config.attributes.values.required = field.required;
                config.attributes.values.label = field.label;
    
                // Add the value for each field as a name/value            
                values.push({name: field.fieldPath, value: undefined});
    
                // Add the component to the facet and configure it
                self.addComponent(component, facet, config, field.fieldPath);
            }
            component.set("v.values", values);
        },
    
        getTypeNames: function(component, event) {
            var action = component.get("c.getTypeNames");
            action.setParams({})
            action.setCallback(this, function(a) {
                var types = a.getReturnValue();
                component.set("v.types", types);
            });
            $A.enqueueAction(action);        
        },
    
        selectType: function(component, type) {
            component.set("v.type", type);
            this.getFieldSetNames(component, type);
        },
    
        getFieldSetNames: function(component, typeName) {
            var action = component.get("c.getFieldSetNames");
            action.setParams({typeName: typeName});
            action.setCallback(this, function(a) {
                var fsNames = a.getReturnValue();
                component.set("v.fsNames", fsNames);
            });
            $A.enqueueAction(action);        
        },
    
        selectFieldSet: function(component, fsName) {
            component.set("v.fsName", fsName);
            this.getFields(component);
        },
    
        getFields: function(component) {
            var action = component.get("c.getFields");
            var self = this;
            var typeName = component.get("v.type");
            var fsName = component.get("v.fsName");
            action.setParams({typeName: typeName, fsName: fsName});
            action.setCallback(this, function(a) {
                var fields = a.getReturnValue();
                component.set("v.fields", fields);
                self.createForm(component, fields);
            });
            $A.enqueueAction(action);        
        },
    
        submitForm: function(component, event) {
            var values = component.get("v.values");
            var s = JSON.stringify(values, undefined, 2);
            alert(s);
        }
    })
    

    And a bit of CSS to make it a bit nicer in fsTest.css:

    .THIS.container {
        margin: 10px auto;
        width: 100%;
        outline: 1px solid #C0C0C0;
    }
    
    .THIS .row {
        width: 100%;
        white-space: nowrap;
    }
    
    .THIS .section {
        outline: 1px solid #A0A0A0;
        width: 50%;
        display: inline-block;
        vertical-align: top;
        overflow: none;
        position: relative;
    }
    
    .THIS .section .title {
        padding: 4px;
        border-bottom: 1px solid #A0A0A0;
        background: #F0F0F0;
    }
    
    .THIS .section .list dd {
        padding: 4px;
        border-bottom: 1px solid #A0A0A0;
        background: #FAFAFA;
    }
    
    .THIS .section .cell {
        height: 200px;
        overflow: auto;
    }
    
    .THIS .section .controls {
        position: absolute;
        right: 0px;
        top: 0px;
    }
    
    .THIS .section .controls .uiButton {
        margin: 1px 5px;
        padding: 2px;
        font-size: 8pt;
    }
    
    .THIS .form label {
        width: 160px;
        display: inline-block;
        text-align: right;
        margin: 2px 4px;
    }
    

    The component implements force:appHostable, so it can be used in the S1 Mobile App. To use it standalone, here's fsTestApp.app:

    <aura:application>
        <aotp1:fsTest/>
    </aura:application>
    

    When run, the UI looks like this:

    fsTestApp.png

    To use it, click on the refresh/reload icon in the upper left. This can take a few seconds as the number of sobjects on a typical org is huge. If you don't have any field sets, you won't see anything in the list. If you do, any object types with field sets are listed. Clicking on the link for the object type will fetch the field sets from the server and display them on the list on the upper right. Clicking on a field set link will generate the form and test listing. Enter values and tab out/hit return to see the values change. Click the Test Submit button to see the values.

    Note that there is a bug in ui:inputDate that can occur when using dynamic creation. You'll get a spinner and be blocked.

    You can change the DisplayType->component mapping in the configMap in fsTestHelper.js. You could do the mapping otherwise, via code, metadata, etc., if desired.

    Let me know if you have any questions. It's not a robust app, but it might provide some ideas on how to approach this.

    Thanks alot for digging into this. Using a fieldSet object is indeed a much better idea to pass the member data than building my own JSON string.

    I should be able to push the picklist options towards the client to use them it into a selectbox. Using the same class.

    Funny that you mention this. I based the sample code here on my original picklist sample, where I had used an AuraEnabled version of SelectOption. It has the label, value, disabled, and escapeItem member variables with accessors, and a couple of constructors. Let me know if you would like the Apex code and JS that demonstrates it.

    I cannot get the field set names list. In line 112 of helper.js, it fires the controller action, but never gets to the callback. Using chrome developer tools can see the response fine. Any idea?

    Is there a new version of this using createComponents?

    can we prepopulate values in this dynamic way?

    Wow, this might be the longest answer I've ever seen. +1 just for all that work!

    Is there any way to add the class attribute within the helper? I'm having problems because class is a reserved word and lightning won't let me save when using it as an attribute. Ultimately, I want to dynamically add the appropriate SLDS classes.

    Nevermind, just needed to enclose class in quotation marks.

License under CC-BY-SA with attribution


Content dated before 7/24/2021 11:53 AM