Ag Grid Helper/abstract class
export abstract class InfiniteGridExtension {
private _gridOptions : GridOptions = {
defaultColDef : {
minWidth : 100 ,
resizable : true ,
sortable : true ,
autoHeight : true ,
menuTabs : [ 'filterMenuTab' , 'columnsMenuTab' ] ,
} ,
detailCellRendererParams : null as IDetailCellRendererParams ,
overlayLoadingTemplate : '<span class="ag-overlay-loading-center">Loading...</span>' ,
overlayNoRowsTemplate : '<span class="ag-overlay-loading-center">No rows to show</span>' ,
tooltipShowDelay : 100 ,
enableBrowserTooltips : true ,
animateRows : true ,
unSortIcon : true ,
} ;
get gridOptions ( ) : GridOptions {
return this . _gridOptions ;
}
/**
* set/overwrite default gridOptions
*/
set gridOptions ( value : GridOptions ) {
this . _gridOptions = { ...this . _gridOptions , ...value } ;
}
get rows ( ) : any [ ] {
let row = [ ] ;
this . gridApi ?. forEachNode ( ( rowNode , index ) => row . push ( rowNode . data ) ) ;
return row ;
}
get gridApi ( ) : GridApi {
return this . gridOptions . api ;
}
get selectedRows ( ) {
return this . gridApi ?. getSelectedRows ( ) ;
}
/**
* @returns first selected row
*/
get selectedRow ( ) : any {
return this . selectedRows ?. length ? this . selectedRows [ 0 ] : undefined ;
}
theme = "ag-theme-balham" ;
}
### gridOptions
``` TypeScript
this.gridOptions = {
tooltipShowDelay: 100,
enableBrowserTooltips: true,
defaultColDef: {
resizable: true,
sortable: true,
autoHeight: true, // <-- & HERE
menuTabs: ['filterMenuTab', 'columnsMenuTab'],
},
};
processCellForClipboard ( params ) {
if ( [ "string" , "number" ] . includes ( typeof params . value ) ) {
return params . value ;
} else if ( Array . isArray ( params . value ) ) {
return params . value . map ( x => x . codeName ) . join ( "\n" ) ;
}
return params . value ?? "" ;
}
{
headerName : "Bundle ID" ,
field : "name" ,
wrapText : false ,
flex : 3 ,
maxWidth : 200 ,
tooltipValueGetter : params => {
let users = params . data . suggestions . reduce ( ( a , b ) => ( { ...a , [ b . createdBy . email ] : b . createdBy } ) , { } ) ;
return Object . keys ( users ) . sort ( ( k1 , k2 ) => users [ k2 ] . displayName - users [ k1 ] . displayName ) . join ( ", " ) ;
} ,
valueGetter : params => {
let users = params . data . suggestions . reduce ( ( a , b ) => ( { ...a , [ b . createdBy . email ] : b . createdBy } ) , { } ) ;
return Object . keys ( users ) . sort ( ( k1 , k2 ) => users [ k2 ] . displayName - users [ k1 ] . displayName ) . map ( x => users [ x ] . displayName ) . join ( ", " ) ;
} ,
cellStyle : { 'white-space' : 'normal' , "word-break" : "break-word" } ,
suppressKeyboardEvent : suppressKeyboardFunc . atag ( 'a' ) ,
cellRenderer : 'userEmailRenderer' ,
cellRendererParams :{
title : 'email' ,
key : 'displayName'
} ,
cellRendererSelector : params => {
// no renderer if suggestion is new milestone
if ( params . data . suggestions [ 0 ] . metaData . parentType === 'Milestone' && params . data . suggestions [ 0 ] . metaData . key === "dtoType" ) {
return undefined ;
}
return {
component : 'linkRenderer' ,
params : {
routerLink : [ ] ,
queryParams : ( p ) => ( { [ selectedSuggestGroupTab === "Key Activities" ? "keyActivityId" : "Risks" === selectedSuggestGroupTab ? "keyActivityId" : 'milestoneId' ] :
selectedSuggestGroupTab !== "Key Activities" ?p . data . kaId :p . data . parentId , tab : 'suggestions' , subtab :'submitted' } ) ,
}
} ;
} ,
} ,
function suppressAtag ( controlElementTag : 'select' | 'input' | 'textarea' | 'a' | 'button' ) {
return params => {
const event : KeyboardEvent = params . event ;
const eventEle = params . event . target ;
let gridCell = eventEle ;
while ( gridCell . role !== 'gridcell' ) {
gridCell = gridCell . parentElement ;
}
const controlElement = gridCell . querySelector ( controlElementTag ) ;
if ( ! controlElement || event . ctrlKey ) {
return false ;
}
if ( event . key === "Tab" && eventEle === gridCell ) {
controlElement . focus ( ) ;
// console.log(controlElement);
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
return true ;
}
if ( event . key === "Enter" && eventEle === gridCell ) {
controlElement . click ( ) ;
return true ;
}
return false ;
} ;
}
function suppressAtags ( controlElementTag : 'select' | 'input' | 'textarea' | 'a' | 'button' , parentNodeName : string ) {
return params => {
const event : KeyboardEvent = params . event ;
const eventEle = params . event . target ;
const eventEleIsAtag = eventEle . nodeName == controlElementTag . toUpperCase ( ) ;
let gridCell = eventEle ;
while ( gridCell . role !== 'gridcell' ) {
gridCell = gridCell . parentElement ;
}
let atagArray = [ ] ;
if ( ! eventEleIsAtag ) {
atagArray = eventEle . querySelectorAll ( controlElementTag ) ;
} else {
let parentTag = eventEle . parentElement ;
while ( parentTag . nodeName != parentNodeName . toUpperCase ( ) ) {
parentTag = parentTag . parentElement ;
}
atagArray = parentTag . querySelectorAll ( controlElementTag ) ;
}
if ( params . event . ctrlKey || atagArray . length === 0 ) {
return false ;
}
if ( params . event . key === "Tab" ) {
if ( eventEle === gridCell ) {
if ( event . shiftKey ) {
atagArray [ atagArray . length - 1 ] . focus ( ) ;
} else {
atagArray [ 0 ] . focus ( ) ;
}
}
if ( eventEleIsAtag ) {
let index = Array . prototype . indexOf . call ( atagArray , eventEle ) ;
if ( event . shiftKey ) {
if ( index < 1 ) {
return false ;
} else {
atagArray [ index - 1 ] . focus ( ) ;
}
} else {
if ( index + 1 === atagArray . length ) {
return false ;
} else {
atagArray [ index + 1 ] . focus ( ) ;
}
}
}
event . stopPropagation ( ) ;
event . preventDefault ( ) ;
return true ;
}
return false ;
}
}
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { InfiniteGridExtension, deepCopy } from 'ng-on-lightning';
import { MeetingsApiService } from 'ng-on-meeting';
import { lastValueFrom } from 'rxjs';
import { ReportsApiService } from 'src/app/services/reports/report-api.service';
@Component({
selector: 'select-bundles-grid',
template: `<ag-grid-angular
style="width: 100%; height: calc(100vh - 200px);"
class="{{theme}}"
[gridOptions]="gridOptions">
</ag-grid-angular>`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: SelectBundlesGridComponent
},
]
})
export class SelectBundlesGridComponent extends InfiniteGridExtension implements OnInit, ControlValueAccessor, OnDestroy {
@Output() select = new EventEmitter();
@Output() unselect = new EventEmitter();
@Input() set loading(loading) {
if (loading) {
if (this.gridApi) {
this.gridApi.showLoadingOverlay();
}
}else{
// this.initCheckbox(this.gridApi);
}
}
@Input('rowData') set setRowData(rowData) {
if(!rowData)return;
if (this.gridApi) {
this.gridApi.setRowData(rowData);
} else {
this.gridOptions.rowData = rowData;
}
}
ngOnInit(): void {
}
payload;
gridSelectedRows = [];
detailGridSelectedRows = [];
initCheckbox(api){
let selectIds = this.selectedData.map(x=>x.businessId);
// console.log(api, selectIds);
api?.forEachNode((rowNode, index) => {
if(selectIds.includes(rowNode.data.businessId)){
rowNode.setSelected(true);
}else{
rowNode.setSelected(false);
}
});
}
constructor(
) {
super()
let defaultDetailGroupOptions = deepCopy(this.gridOptions);
this.gridOptions = {
onRowDataUpdated: params=>{
if (!this.rows.length) {
this.gridApi.showNoRowsOverlay();
} else {
this.gridApi.hideOverlay();
}
this.initCheckbox(params.api);
},
rowMultiSelectWithClick: true,
onSelectionChanged: params=> this.initCheckbox(params.api),
onRowSelected: params => {
// console.log(params.node.isSelected());
// if row selected
if (params.node.isSelected()){
this.select.emit(params.data);
this.selectedData = this.selectedData.concat(this.rows.filter(x=>x.businessId===params.data.businessId));
// console.log(this.selectedData);
} else {
// if row unselected
this.selectedData = this.selectedData.filter(x=>x.businessId!==params.data.businessId);
this.unselect.emit(params.data);
}
this.onChange(this.selectedData );
},
masterDetail: true,
rowSelection: 'multiple',
isRowMaster: data => data.submissions?.length,
columnDefs: [
// checkbox column
{
checkboxSelection: true,
headerCheckboxSelection: true,
suppressColumnsToolPanel: true,
maxWidth: 40,
suppressMenu: true,
lockPosition: true,
lockVisible: true,
resizable: false,
sortable: false,
},
// chevron/agGroupCellRenderer column
{
valueGetter: _ => '',
showRowGroup: true,
cellRenderer: 'agGroupCellRenderer',
suppressColumnsToolPanel: true,
suppressMenu: true,
maxWidth: 40,
lockVisible: true,
lockPosition: true,
resizable: false,
sortable: false,
},
{
headerName: "Bundle ID",
field: "businessId",
flex: 2,
minWidth: 100,
maxWidth: 200,
},
{
headerName: "Bundle Title",
field: "title",
flex: 3,
},
{
headerName: "Bundle Description",
field: "description",
// render html
cellRenderer: params => params.value,
flex: 5,
},
],
detailCellRendererParams: {
detailGridOptions: {
...defaultDetailGroupOptions,
// onRowSelected: params => { },
suppressContextMenu: true,
rowSelection: 'multiple',
detailRowAutoHeight: true,
columnDefs: [
{
headerName: "Business ID",
field: "submissionNumber",
flex: 2,
minWidth: 200,
},
{
headerName: "Title",
field: "title",
flex: 3,
},
],
},
getDetailRowData: (params) => {
params.successCallback(params.data.submissions);
},
template: (params) => {
return (
`<div class="ag-details-row ag-details-row-auto-height" style="background-color: #e8f1f9; padding-top: 5px; padding-right: 15px; padding-left: 15px; box-sizing: border-box;">
<h3 class="slds-text-heading_small" style="height: 25px;">Items</h3>
<div ref="eDetailGrid" style="height: 250px; width: 100%; border-bottom-style: solid; border-bottom-width:15px; border-bottom-color: #e8f1f9"; box-sizing: border-box; ></div>
</div>`
);
},
},
};
}
ngOnDestroy(): void {
}
selectedData = [];
writeValue(obj: any): void {
this.selectedData = obj;
this.initCheckbox(this.gridApi);
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
onChange = (x) => {
// this.initCheckbox(this.gridApi);
};
onTouched = () => { };;
validatorChange;
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
touched = false;
markAsTouched() {
if (!this.touched) {
this.onTouched();
this.touched = true;
}
}
}
Don't blow are still working or not.
< ag-grid-angular
style ="width: 100%, height: 100% "
class ="{{ theme }} "
[rowData] ="rowData "
[columnDefs] ="gridColumns "
[gridOptions] ="gridOptions "
[rowSelection] ="rowSelection "
[masterDetail] ="true "
[isRowMaster] ="isRowMaster "
[detailCellRendererParams] ="detailCellRendererParams "
[suppressRowClickSelection] ="true "
>
</ ag-grid-angular >
gridOptions with selection
this . gridOptions = {
onRowGroupOpened : ( params => {
if ( ! params . node . expanded ) return ;
const detailGridInfo = params . api . getDetailGridInfo ( 'detail_' + params . node . id ) ;
let detailGridNodes = [ ] ;
if ( detailGridInfo ) {
detailGridInfo . api . forEachNode ( ( x , i ) => detailGridNodes . push ( x ) ) ;
if ( params . node . selected && detailGridNodes . length ) {
const detailGridFirstNode = detailGridNodes [ 0 ] ;
if ( this . childSelectedRows ?. length ) {
const selectedIndex = detailGridNodes . findIndex ( x => x . data . id === this . childSelectedRows [ 0 ] . id ) ;
if ( selectedIndex > - 1 ) {
detailGridNodes [ selectedIndex ] . setSelected ( true ) ;
}
} else if ( detailGridNodes . every ( x => ! x . selected ) ) {
detailGridFirstNode . setSelected ( params . node . selected ) ;
}
}
// console.log(this.childSelectedRows);
}
} ) . bind ( this ) ,
onRowSelected :this . parentGridCheckChange . bind ( this ) ,
} ;
detailCellRendererParams with custom template
this . detailCellRendererParams = {
detailGridOptions : {
onRowSelected : this . childGridOnRowSelected . bind ( this ) ,
suppressContextMenu : true ,
// suppressPropertyNamesCheck : true, // suppress irritating warnings
domLayout : 'autoHeight' ,
rowSelection : Selection . SINGLE ,
columnDefs : [
{
lockPosition : true ,
suppressMenu : true ,
suppressColumnsToolPanel : true ,
field : '' ,
width : 40 ,
checkboxSelection : true ,
resizable : false ,
lockVisible : true ,
headerCheckboxSelection : false ,
sortable : false ,
} ,
...
] ,
getDetailRowData : ( params ) => {
params . successCallback ( params . data . packageMeetings ) ;
} ,
template : ( params ) => {
return (
`<div class="ag-details-row ag-details-row-auto-height" style="background-color: #e8f1f9; padding-top: 5px; padding-right: 15px; padding-left: 15px; box-sizing: border-box;">
<h3 class="slds-text-heading_small" style="height: 25px;">Meetings</h3>
<div ref="eDetailGrid" style="height: 250px; width: 100%; border-bottom-style: solid; border-bottom-width:15px; border-bottom-color: #e8f1f9"; box-sizing: border-box; ></div>
</div>`
) ;
}
} ;
{
lockPosition : true ,
suppressMenu : true ,
suppressColumnsToolPanel : true ,
field : '' ,
width : 40 ,
checkboxSelection : true ,
resizable : false ,
lockVisible : true ,
headerCheckboxSelection : false ,
sortable : false ,
} ,
// chevron column
{
valueGetter : _ => '' ,
showRowGroup : true ,
cellRenderer : 'agGroupCellRenderer' ,
suppressColumnsToolPanel : true ,
suppressMenu : true ,
maxWidth : 40 ,
lockVisible : true ,
lockPosition : true ,
resizable : false ,
sortable : false ,
} ,
onRowSelected ( params ) {
const expendedDetailGrids = [ ] ;
params . api . forEachDetailGridInfo ( ( x , i ) => {
expendedDetailGrids . push ( x ) ;
} ) ;
const detailGridInfo = params . api . getDetailGridInfo ( 'detail_' + params . node . id ) ;
let detailGridNodes = [ ] ;
if ( detailGridInfo ) {
detailGridInfo . api . forEachNode ( ( x , i ) => detailGridNodes . push ( x ) ) ;
}
if ( params . node . selected ) {
if ( detailGridNodes . length ) {
const detailGridFirstNode = detailGridNodes [ 0 ] ;
if ( params . node . selected && detailGridNodes . every ( x => ! x . selected ) ) {
detailGridFirstNode . setSelected ( params . node . selected ) ;
}
} else if ( ! this . childSelectedRows ?. length || params . node . data . packageMeetings . some ( x => this . childSelectedRows [ 0 ] . id !== x . id ) ) {
this . childSelectedRows = [ params . node . data . packageMeetings [ 0 ] ] ;
}
} else {
this . childSelectedRows = [ ] ;
if ( detailGridNodes . length ) {
detailGridNodes . forEach ( x => x . setSelected ( false ) ) ;
}
}
}
detail grid onRowSelected
detailGridOnRowSelected ( params ) {
const expendedDetailGrids = [ ] ;
this . gridOptions . api . forEachDetailGridInfo ( ( x , i ) => {
expendedDetailGrids . push ( x ) ;
} ) ;
let parentNode ;
this . gridOptions . api . forEachNode ( ( node , i ) => {
if ( node . data . packageMeetings . some ( x => x . id === params . data . id ) ) {
// select or unselect parent node base detail grid selection
const selectedRows = params . api . getSelectedNodes ( ) ;
if ( ! ( selectedRows . length && node . selected ) ) {
node . setSelected ( params . node . selected ) ;
}
if ( params . node . selected ) {
this . childSelectedRows = [ params . data ] ;
} else if ( selectedRows ?. length === 0 ) {
this . childSelectedRows = [ ] ;
}
parentNode = node ;
}
} ) ;
}