0

I'm trying to dynamically build a table for display and user interaction in lightning and I'm coming up against some difficulty when utilizing the $A.createComponents and trying to build multiple aura:html items option. What's frustrating is that I can pretty much get it to render exactly how I want if I use aura:unescapedHTML and just feed it a string of the markup, but I have some interactive features in my column entries which are supposed to trigger a controller function after an onchange event which, judging by previous message threads I've seen from people who have tried something similar, is not possible to work when building the table dynamically like this and trying to use controller.getReference() or regular markup to make up for the shortcomings.

I've never used aura:html before, and I'm finding the Salesforce example less than helpful, and other examples not much better, so I know I'm screwing up in regards to building the basic structure. If anyone could help in just getting the $A.createComponents down by pointing me at an example where someone actually built a table using this method (if it's possible) so I could see it in action, that would be appreciated. Or, even better, if someone does know a way to get the onchange markup below working in the unescaped version.

To give you an idea of whatI'm trying to end up with, this is what I have that works with the unescapedHTML, aside from the onchange:

Controller method

    @AuraEnabled 
global static String fetchParentRoles(String strSelectedUser, String strSelectedUserRole) {
    try {
        system.debug('strSelectedUser: ' + strSelectedUser);
        system.debug('strSelectedUserRole: ' + strSelectedUserRole);
        Integer rowCount = 0;              // Variable used for row count
        Id prevRoleSelected;
        String strHTMLToReturn = '';
        string open = '<table>';
        string close = '</table>';
        Id parentRoleId;
        if(strSelectedUserRole != '')
            parentRoleId = strSelectedUserRole;
        boolean isorgAllignment = false;

        //Gets the custom setting details based on the logged in User.
        S2_custom_permissions__c objS2custom = S2_custom_permissions__c.getInstance(UserInfo.getProfileId().substring(0,15));
        if(objS2custom == null)
            isorgAllignment = false;
        else
            isorgAllignment = objS2custom.Org_Alignment__c; 

        if(String.isNotBlank(strSelectedUser) && strSelectedUserRole == '')   {

            UserRole objEnterpriseUserRole = [Select ParentRoleId, Id From UserRole Where Name = 'Enterprise Sales' Limit 1];

            Map<Integer, String> mapOptionsHTML = new Map<Integer, String>();
            Map<Integer, String> mapUsersHTML = new Map<Integer, String>();

            String userNames = '';
            String strSelectOptionsHTML = '';
            for(UserRole objUserRole : [Select ParentRoleId,id, UserRole.Name From UserRole Where ParentRoleId =: objEnterpriseUserRole.Id]) {
                strSelectOptionsHTML += '<option value="' +objUserRole.Id+ '"' + 
                                            ((objUserRole.Id == objUserRole.Id) ? '  ' : '') + 
                                            '>' +objUserRole.Name+ '</option>';
            } 
            if(String.isNotBlank(strSelectOptionsHTML)) {

                mapOptionsHTML.put(rowCount, ('<option value="" selected="selected" disabled="disabled">--None Selected--</option>' + strSelectOptionsHTML));
                mapUsersHTML.put(rowCount, userNames);
            }
            rowCount++;

            for(Integer j = 0; j <= rowCount; j++) {

                if(mapOptionsHTML.containsKey(j)) {

                    if(j == (rowCount-1)) {

                        strHTMLToReturn = '<tr class="rowContainingPicklist" id="row_'+(rowCount-j-1)+'">' +
                                               '<td >' +
                                                   '<div class="form-group formElementsWidth">' +
                                                        '<label for="divisionPicklist">Division</label><br/>' +
                                                       '<select class="form-control formElementsWidth" size="1" onchange="component.getReference("c.fetchDependentRoles")" id="pickVal_'
                                                                +(rowCount-j-1)+'"' +(isorgAllignment ? '' : ' disabled ') + '>'+
                                                       mapOptionsHTML.get(j) +
                                                   '</select>'+
                                                   '</div>' + 
                                               '</td>' + strHTMLToReturn;
                    }   
                }
            }
            strHTMLToReturn = strHTMLToReturn + 
                            '</tr>';

        } 
        else if(String.isNotBlank(strSelectedUser) && String.isNotBlank(strSelectedUserRole)) {

            Boolean isNewResultsFound = false;
            UserRole objEnterpriseUserRole = [Select ParentRoleId From UserRole Where Name = 'Enterprise Sales' Limit 1];
            Map<Integer, String> mapOptionsHTML = new Map<Integer, String>();
            Map<Integer, String> mapUsersHTML = new Map<Integer, String>();

            do {

                isNewResultsFound = false;
                String userNames = '';
                String strSelectOptionsHTML = '';
                List<UserRole> lstUserRoles = [Select ParentRoleId, Id From UserRole Where Id = :strSelectedUserRole limit 1];

                // Iteraing over UserRole based on ParentRole to fetch the Role names of user  
                for(UserRole objUserRole : [Select Id, Name, ParentRoleId From UserRole Where ParentRoleId = :parentRoleId]) {

                    isNewResultsFound = true;
                    strSelectOptionsHTML += '<option value="' +objUserRole.Id+ '"' + 
                                                ((objUserRole.Id == prevRoleSelected) ? ' selected="selected" ' : '') + 
                                                '>' +objUserRole.Name+ '</option>';
                }

                if( !lstUserRoles.isEmpty() ) {  

                    isNewResultsFound = true;
                    parentRoleId = lstUserRoles[0].ParentRoleId;
                }
                else {

                    isNewResultsFound = false;
                }

                if(rowCount > 0) {

                    //Iterating over users to fetch the Usernames based on selected role
                    for(User objUser : [SELECT Id, Name,userrole.name  FROM User WHERE userroleId =:prevRoleSelected Order By Name]) {

                        if(userNames == '') {

                            userNames+= objUser.Name;
                        }
                        else {

                            userNames+= ', ' + objUser.Name;
                        }
                    }
                }

                if(String.isNotBlank(strSelectOptionsHTML)) {

                    mapOptionsHTML.put(rowCount, ('<option value="">--None Selected--</option>' + strSelectOptionsHTML));
                    mapUsersHTML.put(rowCount, userNames);
                } 

                // Assign the previous role in hierarchy for comparison
                prevRoleSelected = strSelectedUserRole;

                // Updating the parent role for further querying
                strSelectedUserRole = parentRoleId;

                rowCount++;

            }  while(parentRoleId != objEnterpriseUserRole.ParentRoleId && isNewResultsFound && rowCount < 25);

            for(Integer j = 0; j <= rowCount; j++) {

                if(mapUsersHTML.containsKey(j) && j>0) {

                    strHTMLToReturn = '<td width="100%">' +
                                               '<div class="mediumMarginTop marginLeft" id="userNames'+(rowCount-j-1)+'" ' +
                                                            ((j == (rowCount-1)) ? 'style="margin-top: 4% !important;"' : '') + '>' +
                                                  mapUsersHTML.get(j) +
                                               '</div>' + 
                                           '</td>' +
                                        '</tr>' + strHTMLToReturn;
                }
                if(mapOptionsHTML.containsKey(j)) {

                    if(j == (rowCount-1)) {

                        string strmapOptionsHTML = mapOptionsHTML.get(j);
                        String[] arrTest = strmapOptionsHTML.split('value=""');
                        string strmapOptionsHTMLDisabled = arrTest[0] + ' value="" disabled="disabled" '+arrTest[1];

                        strHTMLToReturn = '<tr class="rowContainingPicklist" id="row_'+(rowCount-j-1)+'">' +
                                               '<td >' +
                                                   '<div class="form-group formElementsWidth">' +
                                                        '<label for="divisionPicklist">Division</label><br/>' +
                                                       '<select class="form-control formElementsWidth" size="1" onchange="component.getReference("c.fetchDependentRoles")" id="pickVal_'
                                                                +(rowCount-j-1)+'"' +(isorgAllignment ? '' : ' disabled ') + '>'+
                                                       strmapOptionsHTMLDisabled +
                                                   '</select>'+
                                                   '</div>' + 
                                               '</td>' + strHTMLToReturn;
                    }   
                    else { 

                        strHTMLToReturn = '<tr class="rowContainingPicklist" id="row_'+(rowCount-j-1)+'">' +
                                               '<td >' +
                                                   '<div class="form-group formElementsWidth">' +
                                                       '<select class="form-control formElementsWidth mediumMarginTop" size="1" onchange="component.getReference("c.fetchDependentRoles")" id="pickVal_'
                                                                +(rowCount-j-1)+'"' +(isorgAllignment ? '' : ' disabled ') + '>'+
                                                       mapOptionsHTML.get(j) +
                                                   '</select>'+
                                                   '</div>' + 
                                               '</td>' + strHTMLToReturn;
                    }
                }
            }
            strHTMLToReturn = strHTMLToReturn + 
                            '</tr>';
        }
       strHTMLToReturn = strHTMLToReturn;
       system.debug('returnHTML: ' + strHTMLToReturn);
        return strHTMLToReturn;
    }
    catch(exception e){
        system.debug('Error: ' + e.getMessage() + ' Line: ' + e.getLineNumber());
        return null;
    }
}

Component markup:

<aura:unescapedHTML value="{!v.bodyText}"/>

Helper class:

fetchParentRoles : function(component, event, helper) {
            var action = component.get("c.fetchParentRoles");
    action.setParams({
        "strSelectedUser" : component.get("v.strSelectedUser", "v.value"),
        "strSelectedUserRole" : component.get("v.selectedUserRoleID", "v.value")
        });
    return new Promise(function(resolve, reject){
    action.setCallback(this, function(a){
        var response = a.getReturnValue();
        if (response == null || response == "" || response == "[]" || response == "{}"){
            var msgId = component.find("uiMessageid");
            $A.util.addClass(msgId, 'toggle');
            //Show toast Error
            return;
            } 
        else{
            console.log('Fetch response: ' + response);
            component.set("v.bodyText", response);
            resolve("Resolved");
            }
        });
        $A.enqueueAction(action);
        });
    },

And the result: enter image description here

2

There's simply too much code in your question to reasonably fully fix in an answer; it'd probably take me a few hours to fully deconstruct and rebuild this. But, generally speaking, you need to separate your data from your layout. Your method should look something like this:

public class Response {
  @AuraEnabled public UserRole[] roles;
  @AuraEnabled public User[] users;
  ...
}
@AuraEnabled public static Response getDataFromServer() {
  Response res = new Response();
  // Populate data
  return res;
}

That is all your Apex Code should be doing. Getting data from the server. The rest is handled client-side. First, build your component:

<aura:component controller="XYZ">
  <aura:attribute name="userList" type="List" />
  ...

  <aura:handler name="init" value="{!this}" action="{!c.doInit}" />

  <table>
  <thead><tr> ... </tr></thead>
  <tbody>
  <aura:iteration items="{!v.userList}" var="userRecord">
    <tr> ... </tr>
  </aura:iteration>
  </tbody>
  </table>
</aura:component>

All of that fancy footwork stuff you're doing should be in your client-side controller and helper logic. I see absolutely no reason why you'd want to render this in Apex Code and expect everything to magically work. Further, rendering on the server is slow, rendering on the client is fast. Do you want to be the one to give your users a slow experience?

This tactic "worked" in Visualforce because we were allowed to freely mix HTML and data in all sorts of unusual and interesting ways, and the Visualforce runtime was slow enough that it barely mattered if you rendered everything on the server, but in Lightning, aura:unescapedHTML is really only meant as a last-ditch effort to shoehorn in older/unorthodox code. The only real solution is to move to a proper Model-View-Controller design.

  • Damn, you are a Legend for a reason. I don't know anything about this question/answer, but please keep up the Awesome work!! SFSE is blessed with a diamond(like you). – Arnold Jr. Apr 3 '18 at 3:04
  • Yes, sfdcfox is quite the legend, always very helpful. Glad for users like him and Bob Buzzard in the community to provide some needed guidance and perspective. Definitely going to reconfigure as suggested, especially as the structure above is a holdover from a legacy of this UI we're now converting over to lightning, and I will admit I was hoping we could still rely on based on the complexity you can see it contains. But your point is a valid one, it will be much smoother when we can bring the bulk of the processing client-side. Thanks. – CMielczarski Apr 3 '18 at 13:18
0

Ok, so, in the event that anyone else needs to do something crazy like this, following the suggestion from @sfdcFox, I rewrote pretty much everything and got the display to work the way I wanted it so my users can now interact with the list as needed.

Controller Methods:

public class Response {
    @AuraEnabled public list<String> roles;
    @AuraEnabled public string users;

    public Response(list<String> roles, String users){
        this.roles = roles;
        this.users = users;
        }
    }

@AuraEnabled public static list<Response> fetchParentRoles(String strSelectedUser, String strSelectedUserRole){
    list<Response> resList = new list<Response>(); 
    try{
        system.debug('strSelectedUser: ' + strSelectedUser);
        system.debug('strSelectedUserRole: ' + strSelectedUserRole);
        Integer rowCount = 0;              // Variable used for row count
        Id prevRoleSelected;
        Id parentRoleId;
        if(strSelectedUserRole != ''){
            parentRoleId = strSelectedUserRole;
            }
        if(String.isNotBlank(strSelectedUser) && String.isNotBlank(strSelectedUserRole)){
            Boolean isNewResultsFound = false;
            UserRole objEnterpriseUserRole = [Select ParentRoleId From UserRole Where Name = 'Enterprise Sales' Limit 1];
            map<Integer, list<String>> mapOptions = new map<Integer, list<String>>();
            map<Integer, String> mapUsersHTML = new map<Integer, String>();
            map<integer, string> roleOrder = new map<integer, string>();
            list<String> roleSelectOptions = new list<String>();
            integer counter = 0;
            string firstRole = '';
            do{
               isNewResultsFound = false;
               String userNames = '';
               List<UserRole> lstUserRoles = [Select ParentRoleId, Id From UserRole Where Id = :strSelectedUserRole limit 1];
                // Iterating over UserRole based on ParentRole to fetch the Role names of user  
                for(UserRole objUserRole : [Select Id, Name, ParentRoleId From UserRole Where ParentRoleId = :parentRoleId]) {
                    isNewResultsFound = true;
                    roleOrder.put(counter, objUserRole.Name);
                    //roleSelectOptions.add(objUserRole.Name);
                    counter++;
                    }
                if(!lstUserRoles.isEmpty()){  
                    isNewResultsFound = true;
                    parentRoleId = lstUserRoles[0].ParentRoleId;
                    }
                else{
                    isNewResultsFound = false;
                    }
                if(rowCount > 0){
                    //Iterating over users to fetch the Usernames based on selected role
                    for(User objUser : [SELECT Id, Name, userRole.name  FROM User WHERE userroleId =:prevRoleSelected Order By Name]) {
                        if(userNames == ''){
                            userNames+= objUser.Name;
                            }
                        else{
                            userNames+= ', ' + objUser.Name;
                            }
                            firstRole = objUser.userRole.name;
                        }
                    }
                if(roleOrder.size() > 0) {
                    system.debug('roleOrder: ' + roleOrder);
                    if(firstRole != ''){
                        roleSelectOptions.add(firstRole);
                        }
                    for(integer i = 0; i<= counter; i++){
                        roleSelectOptions.add(roleOrder.get(i));
                        }
                    mapOptions.put(rowCount, roleSelectOptions);
                    mapUsersHTML.put(rowCount, userNames);
                    roleSelectOptions = new list<String>();
                    counter = 0;
                    firstRole = '';
                    }
                system.debug('mapOptions2: ' + mapOptions);
                system.debug('mapUsersHTML2: ' + mapUsersHTML);  
                // Assign the previous role in hierarchy for comparison
                prevRoleSelected = strSelectedUserRole;
                // Updating the parent role for further querying
                strSelectedUserRole = parentRoleId;
                rowCount++;
                }while(parentRoleId != objEnterpriseUserRole.ParentRoleId && isNewResultsFound && rowCount < 25);
            for(Integer j = rowCount; j >= (rowCount * -1); j--){
                if(mapUsersHTML.containsKey(j)){
                if(mapOptions.containsKey(j)) {
                    resList.add(new Response(mapOptions.get(j), mapUsersHTML.get(j)));
                    }
                }
            }
        }
       system.debug('ResList: ' + resList); 
       return resList;
    }
    catch(exception e){
        system.debug('Error: ' + e.getMessage() + ' Line: ' + e.getLineNumber());
        return null;
    }
    }

Component:

<div>
                            <table>
                            <thead><tr>Division</tr></thead>
                            <tbody>
                                    <aura:iteration items="{!v.userRoleList}" var="userRecord">
                                    <tr>
                                        <ui:inputSelect class="slds-select" aura:id="inputSelectRole" change="{!c.fetchDependentRoles}">
                                            <aura:iteration items="{!userRecord.roles}" var="roleLst">
                                                <ui:inputSelectOption text="{!roleLst}" />
                                            </aura:iteration>
                                        </ui:inputSelect> 
                                    </tr>
                                    <tr>
                                        {!userRecord.users} 
                                    </tr>    
                                    </aura:iteration>
                            </tbody>
                            </table>
                        </div>

Helper:

fetchParentRoles : function(component, event, helper){
            var action = component.get("c.fetchParentRoles");
    action.setParams({
        "strSelectedUser" : component.get("v.strSelectedUser", "v.value"),
        "strSelectedUserRole" : component.get("v.selectedUserRoleID", "v.value")
        });
    return new Promise(function(resolve, reject){
    action.setCallback(this, function(a){
        var response = a.getReturnValue();
        if (response == null || response == "" || response == "[]" || response == "{}"){
            var msgId = component.find("uiMessageid");
            $A.util.addClass(msgId, 'toggle');
            //Show toast Error
            return;
            } 
        else{
            console.log('Fetch response: ' + response);
            component.set("v.userRoleList", response);
            resolve("Resolved");
            }
        });
        $A.enqueueAction(action);
        });
    },

Result: enter image description here

Still a bit of other work to do on it, but now my users have their interactive table.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.