AG Grid - saguoran/snipets GitHub Wiki

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

Clipboard

 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??"";
  }

columnDefs

{
  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'   }),
      }
    };
  },
},

Suppress Keyboard Event

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;
  }
}

Samples

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>`
        );
    }
};

checkbox columnDef

{
    lockPosition: true,
    suppressMenu: true,
    suppressColumnsToolPanel: true,
    field: '',
    width: 40,
    checkboxSelection: true,
    resizable: false,
    lockVisible: true,
    headerCheckboxSelection: false,
    sortable: false,
},

group grid column

 // chevron column
{
  valueGetter: _ => '',
  showRowGroup: true,
  cellRenderer: 'agGroupCellRenderer',
  suppressColumnsToolPanel: true,
  suppressMenu: true,
  maxWidth: 40,
  lockVisible: true,
  lockPosition: true,
  resizable: false,
  sortable: false,
},

parentGrid onRowSelected

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;
    }
  });
}

⚠️ **GitHub.com Fallback** ⚠️