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+']';
}

Answer

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.

Attribution
Source : Link , Question Author : JD Dingenen , Answer Author : Skip Sauls

Leave a Comment