Search code examples
angularangular-materialag-gridag-grid-angularag-grid-ng2

Fitting a mat-accordion inside ag-grid


I have a fairly standard setup of ag-grid with a nested component as follows :

import { Component } from '@angular/core';
import * as agGrid from 'ag-grid-community';
import { NestedMatExpansionPanelComponent } from './nested-mat-expansion-panel/nested-mat-expansion-panel.component';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'aggrid-material-expansion-panel';

  dataToShow = [
    { name: 'name1', companyCountry: 'UK', reference: 'Ref 1' },
    { name: 'name2', companyCountry: 'Germany', reference: 'Ref 2' },
    { name: 'name3', companyCountry: 'France', reference: 'Ref 3' },
    { name: 'name4', companyCountry: 'UK', reference: 'Ref 4' },
    { name: 'name5', companyCountry: 'USA', reference: 'Ref 5' },
  ];

  columnDefs = [
    // tslint:disable-next-line: max-line-length
    { headerName: 'Name', colId: 'name', cellRenderer: 'nestedMatExpansionPanelRenderer', filter: false, sortable: false },
    { headerName: 'Country', field: 'companyCountry', sortable: true, filter: true },
    { headerName: 'Reference', field: 'reference', sortable: true, filter: true }
  ];

  // agG_rid
  gridApi: agGrid.GridApi;
  gridColumnApi: agGrid.ColumnApi;
  gridOptions: agGrid.GridOptions = {};
  public defaultColDef: any;
  public columnTypes: any;
  public context: any;
  public frameworkComponents: any;
  public sortingOrder: any;

  constructor() {
    this.initTable();
  }

  public onGridReady(params: any) {
    // this.gridApi = this.gridOptions.api;
    // this.gridColumnApi = this.gridOptions.columnApi;
    this.gridApi = params.api;
    this.gridColumnApi = params.columnApi;

    this.gridApi.sizeColumnsToFit();
  }

  public initTable(): void {
    this.defaultColDef = {
      flex: 1,
      autoHeight: true,
      editable: false,
      enableBrowserTooltips: true,
      resizable: true,
      filter: 'agTextColumnFilter',
      suppressMenu: true,
      floatingFilterComponentParams: { suppressFilterButton: true },
      filterParams: { newRowsAction: 'keep' },
      sortable: true,
    };
    this.columnTypes = {
      numericColumn: { filter: 'agnumericColumnFilter' },
      dateColumn: {
        filter: 'agDateColumnFilter',
        filterParams: {
          newRowsAction: 'keep',
          comparator(filterLocalDateAtMidnight, cellValue) {
            const dateParts = cellValue.split('/');
            const day = Number(dateParts[2]);
            const month = Number(dateParts[1]) - 1;
            const year = Number(dateParts[0]);
            const cellDate = new Date(day, month, year);
            if (cellDate < filterLocalDateAtMidnight) {
              return -1;
            } else if (cellDate > filterLocalDateAtMidnight) {
              return 1;
            } else {
              return 0;
            }
          }
        }
      }
    };
    this.sortingOrder = ['desc', 'asc'];
    this.context = { componentParent: this };
    this.frameworkComponents = {
      nestedMatExpansionPanelRenderer: NestedMatExpansionPanelComponent,
    };
  }

  public onRowClicked($event) {
    //
  }

}

As you can see I have

  autoHeight: true,

so each row should automatically get the correct height depending on the expansion state of the embedded accordion but the output height of the row is calculated incorrectly:

Redundant space

How can I automatically adjust the height of each row depending on the state of the embedded expansion panels so there wont be any empty redundant space when I open or close the expansion panels inside each row?

Link to the repository


Solution

  • Basically, the issues you are having is a combined issue of the MatExpansionPanel and how autoHeight is calculated in ag-grid.

    With autoHeight they take the contents from the cell, and put this in a temporary invisible element probably at the document.body element. The problem is that the proper styling is not applied to the element at this moment, and it shows the height you were facing. So, autoHeight is not the solution here and should be set to false.

    So how to implement the manual height calculation. Ag-grid has a setRowHeight property on the node. Which you can use to set the height. From there the grid API needs to be informed that all rows have altered their heights and can recalculate on that basis, by calling onRowHeightChanged(). One way to get this communication is to use a normal service:

    @Injectable()
    export class GridService {
      readonly updateHeight$ = new Subject<void>();
    }
    

    From within the component where you create the ag-grid in the template, you should add this server to the providers array and listen to the subject (with a debounce to make sure all rows are calculated initially):

    @Component({
      // ...,
      providers: [GridService]
    })
    export class GridComponent {
      gridApi: agGrid.GridApi;
    
      constructor(private gs: GridService) {
        this.gs.updateHeight$.pipe(
          debounceTime(1)
        ).subscribe(() => {
          this.gridApi?.onRowHeightChanged();
        });
      }
    
      onGridReady(params: any) {
        this.gridApi = params.api;
      }
    }
    

    Now we need the following logic in the cell renderer framework component:

    export class ExpansionPanelComponent implements ICellRendererAngularComp, AfterViewInit {
      public params: ICellRendererParams;
    
      constructor(private el: ElementRef<HTMLElement>, private gs: GridService) {}
    
      agInit(params: ICellRendererParams): void {
        this.params = params;
      }
    
      ngAfterViewInit(): void {
        // inside setTimeout because the accordion is not properly sized (bug) and it will be too big
        setTimeout(() => this.updateHeight());
      }
    
      updateHeight(): void {
        // inside setTimeout because the accordion is not properly sized (bug) and it will be too big
        setTimeout(() => {
          this.params?.node.setRowHeight(this.el.nativeElement.offsetHeight);
          this.gs.updateHeight$.next();
        });
      }
    
      refresh(params: any): boolean {
        return false;
      }
    }
    

    I've created a working pull request here


    To remove the padding on the left, you can add the following to your columnDefs array:

    columnDefs = [
      {
        //...,
        cellRenderer: 'nestedMatExpansionPanelRenderer', 
        cellStyle: { 'padding-left': 0 }
      },
      // ...
    ]