[SalesForce] Help building a dynamic table component in lightning

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

Best Answer

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.