You should try Lightning:DataTable with standard table features. You can use this generic component as well.
CmAp_LightningDataTable.Cmp
<aura:component >
<!-- Attrubutes Definations Start here -->
<aura:attribute type="boolean"
name="enableSearchonRow"
description="This attribute is used to show hide search option in tabel"/>
<aura:attribute type="Integer"
name="maxRowSelection"
default="1"
description="This attribute is used to define number of row can be selected"/>
<aura:attribute type="string"
name="tableHeaderTitle"
default=""
description="This attribute is used to define title for table"/>
<aura:attribute type="List"
name="selectedRows"
description="This attribute is used to store preselected record Ids"
/>
<aura:attribute type="string"
name="panelHeightinPx"
default="170px"
description="This attribute is used to define container Height"
/>
<aura:attribute type="string"
name="headerIcon"
default="standard:empty"
description="This attribute is used to define icon in Header"
/>
<aura:attribute type="boolean"
name="showRowNumber"
default="false"
description="This attribute is used to show hide row number"
/>
<aura:attribute type="boolean"
name="showHeaderButton"
default="false"
description="This attribute is used to show hide button in Header"
/>
<aura:attribute type="string"
name="buttonLabel"
default="New"
description="This attribute is used to show label on Button"
/>
<aura:attribute type="string"
name="searchString"
default="false"
description="This attribute store search fields for row filter. Fields must be comma saperated."
/>
<aura:attribute name="recordSearchKey"
type="string"
default=""
description="this attribute store search string for row in table"
access="Private"/>
<aura:attribute name="unFilteredData"
type="List"
description="this attribute store all data for table without filter"
access="Private"
/>
<aura:attribute name="lstDataRows"
type="List"
Description="attribute store Data Rows list of sobject"/>
<aura:attribute name="lstColumns"
type="List"
Description="store Header columns property list"/>
<aura:attribute name="defaultSelectedRow"
type="string"
Description="store default selected row id in table"/>
<aura:attribute name="showSpinner"
type="Boolean"
default ="false"
access="private"/>
<aura:attribute name="sortDirection"
type="String"
default="desc" />
<aura:attribute name="sortedBy"
type="String" />
<!-- Attrubutes Definations ends here -->
<!-- Aura Handeler Section -->
<aura:handler name="init" value="{!this}" action="{!c.doinit}"/>
<aura:method name="setPredefinedValues" action="{!c.setPredefinedValues}" access="public"/>
<!--<aura:handler name="change" value="{!v.lstDataRows}" action="{!c.setPredefinedValues}"/>-->
<!-- Aura Handeler Section Ends here -->
<!-- Events Regstration Sections Defiend here -->
<aura:registerEvent name="onclickActionEvent" type="c:CmAp_LightningDataTableActionEvent"/>
<aura:registerEvent name="onRowChangeEvent" type="c:CmAp_LightningDataTableSelectionEvent"/>
<aura:registerEvent name="onClickRowActionEvent" type="c:CmAp_LightningDataTableRowActionEvent"/>
<!-- Events Regstration Sections Ends here -->
<lightning:card title="{!v.tableHeaderTitle+' ('+if(not(empty(v.lstDataRows)),v.lstDataRows.length,0)+')'}" iconName="{!v.headerIcon}" variant="base">
<aura:set attribute="actions">
<aura:if isTrue="{!v.enableSearchonRow}">
<lightning:input type="search" variant=" label-hidden" class="slds-m-left_small slds-m-right_small" placeholder="{!'Search '+v.tableHeaderTitle}" value="{!v.recordSearchKey}" onchange="{!c.searchInRows}" />
</aura:if>
<aura:if isTrue="{!v.showHeaderButton}">
<lightning:button class="slds-theme_neutral" variant="brand" label="{!v.buttonLabel}" onclick="{!c.handelBttonClickAction}"></lightning:button>
</aura:if>
</aura:set>
<aura:set attribute="footer"> </aura:set>
<div style="{!if(not(empty(v.lstDataRows)),'','display:none')}">
<div style="{!if((v.lstDataRows.length> $Label.c.CmAp_AutoHeightMaxRowLimit),'height:'+v.panelHeightinPx,'')}">
<!-- Spinner Section -->
<aura:if isTrue="{!v.showSpinner}">
<div class="slds-is-relative">
<lightning:spinner variant="brand" alternativeText="Loading..." />
</div>
</aura:if>
<!-- Spinner Ends Here -->
<lightning:datatable aura:id="recordTabel"
columns="{!v.lstColumns}"
data="{!v.lstDataRows}"
keyField="Id"
selectedRows="{!v.selectedRows}"
maxRowSelection="{! v.maxRowSelection }"
onrowselection="{!c.updateSelectedText}"
showRowNumberColumn="{!v.showRowNumber}"
sortedDirection="{!v.sortDirection}"
sortedBy="{!v.sortedBy}"
onsort="{!c.handleSort}"
wrapTextMaxLines="2"
hideCheckboxColumn="{!if(v.maxRowSelection>0,false,true)}"
onrowaction="{!c.rowOnClickActions}"
/>
</div>
</div>
<div class="slds-box slds-theme_default slds-align_absolute-center" style="{!if(not(empty(v.lstDataRows)),'display:none','')}">
<p><b>No data to display.</b></p>
</div>
</lightning:card>
</aura:component>
CmAp_LightningDataTableController.Js
({
doinit: function(component, event, helper){
component.set("v.showSpinner",true);
},
setPredefinedValues : function(component, event, helper) {
helper.setPredefinedValues(component, event, helper);
},
updateSelectedText: function (component, event, helper) {
helper.handleSelect(component, event, helper);
},
searchInRows: function (component, event, helper) {
helper.searchInRows(component, event, helper);
},
handleSort: function(component, event, helper) {
helper.handleSort(component, event);
} ,
handelBttonClickAction: function (component, event, helper) {
helper.handelBttonClickAction(component, event, helper)
},
rowOnClickActions: function (component, event, helper) {
helper.rowOnClickActions(component, event, helper)
}
})
**CmAp_LightningDataTableHelper.js**
({
setPredefinedValues : function(component, helper) {
let lstData =component.get("v.lstDataRows");
let lstUnFilteredData = component.get("v.unFilteredData");
// if($A.util.isUndefined(lstUnFilteredData) || $A.util.isEmpty(lstUnFilteredData)){
component.set("v.unFilteredData",lstData);
//}
this.handelDefaultRowSelection(component, helper);
component.set("v.showSpinner",false);
},
handelDefaultRowSelection: function(component, helper){
let defaultSelectedRow = component.get("v.defaultSelectedRow");
if(!$A.util.isUndefined(defaultSelectedRow) && !$A.util.isEmpty(defaultSelectedRow)){
let temp =[];
temp.push(defaultSelectedRow);
let recordTabel = component.find("recordTabel");
if(recordTabel){
recordTabel.set("v.selectedRows", temp);
}
}
},
searchInRows: function (component, event, helper) {
let data = component.get("v.lstDataRows");
let allData = component.get("v.unFilteredData");
let strfieldlist = component.get('v.searchString');
if(!$A.util.isUndefined(strfieldlist) && !$A.util.isEmpty(strfieldlist)){
let fieldlList = strfieldlist.split(',');
let searchKey = component.get("v.recordSearchKey");
if(!$A.util.isUndefined(data) || !$A.util.isEmpty(data)){
let filtereddata = allData.filter(checkFieldsValidity);
function checkFieldsValidity(word){
let isValid=false;
for(let fieldname of fieldlList) {
if(word.hasOwnProperty(fieldname) &&
(!searchKey || word[fieldname].toLowerCase().indexOf(searchKey.toLowerCase()) > -1)){
isValid= true;
break;
}
}
return isValid;
}
component.set("v.lstDataRows", filtereddata);
this.handelDefaultRowSelection(component, helper);
}
if(searchKey==''){
component.set("v.lstDataRows",component.get("v.unFilteredData"));
}
}
},
sortBy: function(field, reverse, primer) {
var key = primer
? function(x) {
return primer(x[field]);
}
: function(x) {
return x[field];
};
return function(a, b) {
a = key(a);
b = key(b);
return reverse * ((a > b) - (b > a));
};
},
handleSort: function(component, event) {debugger;
var sortedBy = event.getParam('fieldName');
var sortDirection = event.getParam('sortDirection');
let data = component.get('v.lstDataRows');
var cloneData = data.slice(0);
cloneData.sort((this.sortBy(sortedBy, sortDirection === 'asc' ? 1 : -1)));
component.set('v.lstDataRows', cloneData);
component.set('v.sortDirection', sortDirection);
component.set('v.sortedBy', sortedBy);
},
handelBttonClickAction: function(component, event, helper){
let compEvent = component.getEvent("onclickActionEvent");
let tableUniqueName = component.get("v.tableDataType");
compEvent.setParams({"tableUniqueName" : tableUniqueName });
compEvent.fire();
},
handleSelect: function(component, event, helper){
let compEvent = component.getEvent("onRowChangeEvent");
let tableUniqueName = component.get("v.tableHeaderTitle");
let selectedRows = event.getParam('selectedRows');
compEvent.setParams({"tableUniqueName" : tableUniqueName,
"selectedRows":selectedRows});
compEvent.fire();
},
rowOnClickActions: function (component, event, helper) {debugger;
let compEvent = component.getEvent("onClickRowActionEvent");
let tableUniqueName = component.get("v.tableHeaderTitle");
let objRecord = event.getParam('row');
let actionName = event.getParam('action').name;
compEvent.setParams({"tableUniqueName" : tableUniqueName,
"selectedRowActionName":actionName,
"selectedRowData":objRecord});
compEvent.fire();
}
})
How To Use
<c:CmAp_LightningDataTable aura:id="genericTable"
tableHeaderTitle ="{!$Label.c.CmAp_DevicesHeader}"
panelHeightinPx="350px"
headerIcon="standard:planogram"
maxRowSelection="1"
showRowNumber="{!v.objWrapDevice.showRowNumber}"
showHeaderButton="false"
enableSearchonRow="{!v.objWrapDevice.enableSearchonRow}"
searchString="{!$Label.c.CmAp_DeviceSearchString}"
lstDataRows="{!v.objWrapDevice.dataForTable}"
lstColumns ="{!v.objWrapDevice.columnForTable}"
defaultSelectedRow="{!v.SelectedDeviceId}"/>
sortAction
call. The example is for data table but the same dynamics will apply to your component.