Search code examples
ag-gridag-grid-angularag-grid-reactag-grid-vue

How can I autosize all the columns in ag-grid when column virtualization is switched on?


According to the ag-grid documentation (https://www.ag-grid.com/javascript-data-grid/column-sizing/)

"The choice is yours, whether you want column virtualisation working OR auto-size working using off-screen columns"

What this means is uf you have many columns that are off-screen, they do not physically exist and so will not be autosized.

Is there any way I can hack around the built-in limitation in the library, so that I can make use of both features?


Solution

  • I wrote a method that takes every virtual column and moves it just offscreen, autosizes it, and then moves it back to its original location. ag-grid remembers it's size thereafter.

    Depending on if the column is to the left or to the right of where the current visible columns are, it will be moved and sized on the left or right respectively. This is so that the grid does not jump around, and the user is none the wiser. So this can also be used if a hidden column is shown, but is in a virtualized location and thus needs to be autosized.

    For about 30 columns this takes about 1 second to do. Not ideal but this is the best I could do with this library's limitation. However the user is unaware that it is happening and by the time they scroll it should be done.

    This also handles left-pinned columns in the process. I did not need right-pinned columns to be handled but it shouldn't be too hard to add.

      import { Injectable } from "@angular/core";
    import { Column, ColumnApi } from "ag-grid-community";
    
    //after ag-grid refused to fix this I developed this solution and published this solution here: https://stackoverflow.com/questions/77016345/how-can-i-autosize-all-the-columns-in-ag-grid-when-column-virtualization-is-swit
    //Sizes columns including virtualized ones, which the library cannot do
    //It moves the virtualized columns just off-screen, sizes them, then moves them back to their original position
    //Although they become re-virtualized, the grid remembers what size they were set to
    
    //Usage of initial width:
    //We use the initial width when the column content is so small that the autosize makes the header too squashed.
    //We don't use the minWidth in the grid-column-service because then the user cannot shrink the columns manually.
    //And so we temporarily turn the initialWidth  intoa  minWidth to limit the autosize.
    //Ideally we should use a custom columnDef property called something like autoSizeMinWidth, instead of initialWidth
    @Injectable()
    export class VirtualColumnSizeService {
    
      sizeColumns(colIds: string[], columnApi: ColumnApi) {
        const allCols = columnApi.getAllGridColumns();//includes hidden cols and pinned cols. The indexes used to move columns includes hidden and pinned
    
        const renderedCols = columnApi.getAllDisplayedVirtualColumns().filter(x => { return !x.isPinnedLeft() });//excludes hidden cols
        const initialRenderedColsCount = renderedCols.length;
        let leftVirtualIndex = allCols.findIndex(c => c.getColId() == renderedCols[0].getColId());
        let rightVirtualIndex = allCols.findIndex(c => c.getColId() == renderedCols[renderedCols.length - 1].getColId());
    
        const leftColumns: { col: Column, originalIndex: number }[] = [];
        const rightColumns: { col: Column, originalIndex: number }[] = [];
        const visibleColumns: Column[] = [];
        colIds.forEach(
          (id) => {
            const originalIndex = allCols.findIndex((c) => c.getId() == id);
            const column = allCols[originalIndex];
            if (column.isPinnedLeft()) {
              visibleColumns.push(column);
              return;
            }
    
            if (originalIndex < leftVirtualIndex) {
              leftColumns.push({ col: column, originalIndex });
            } else if (originalIndex > rightVirtualIndex) {
              rightColumns.push({ col: column, originalIndex });
            } else {
              visibleColumns.push(column);
            }
          });
    
        leftVirtualIndex = leftVirtualIndex + 1; //we add 1 to compensate for when occasionally it moves to the edge it doesnt quite fit and gets virtualised
        rightVirtualIndex = rightVirtualIndex - 1;//we subtract 1 to compensate for when occasionally it moves to the edge it doesn't quite fit and gets virtualised
    
        visibleColumns.forEach(rc => {
          const colDef = rc.getColDef();
          let initialWidthSet = false
          const userDef = rc.getUserProvidedColDef();
          if (colDef && colDef.initialWidth) {
            initialWidthSet = true;
            colDef.minWidth = colDef.initialWidth;
            rc.setColDef(colDef, userDef);
          }
          columnApi.autoSizeColumn(rc.getColId(), true);
    
          if (initialWidthSet) {
            delete colDef.minWidth;
            rc.setColDef(colDef, userDef);
          }
        });
    
        leftColumns.forEach(rc => {
          const col = rc.col;
    
          const colDef = col.getColDef();
          let initialWidthSet = false;
          const userDef = col.getUserProvidedColDef();
          if (colDef && colDef.initialWidth) {
            initialWidthSet = true;
            colDef.minWidth = colDef.initialWidth;
            col.setColDef(colDef, userDef);
          }
          //the previous movement could have shrunk the non-virtualized window so we adjust in case
          const leftVirtualIndexAdjusted = leftVirtualIndex + (initialRenderedColsCount - columnApi.getAllDisplayedVirtualColumns().filter(x => { return !x.isPinnedLeft() }).length);
          columnApi.moveColumn(col, leftVirtualIndexAdjusted);
          columnApi.autoSizeColumn(col.getColId(), true);
          columnApi.moveColumn(col, rc.originalIndex);
    
          if (initialWidthSet) {
            delete colDef.minWidth;
            col.setColDef(colDef, userDef);
          }
        });
    
        rightColumns.forEach(rc => {
          const col = rc.col;
          const colDef = col.getColDef();
    
          let initialWidthSet = false;
          const userDef = col.getUserProvidedColDef();
          if (colDef && colDef.initialWidth) {
            initialWidthSet = true;
            colDef.minWidth = colDef.initialWidth;
            col.setColDef(colDef, userDef);
          }
          //the previous movement could have shrunk the non-virtualized window so we adjust in case.
          const rightVirtualIndexAdjusted = rightVirtualIndex - (initialRenderedColsCount - columnApi.getAllDisplayedVirtualColumns().filter(x => { return !x.isPinnedLeft() }).length);
          columnApi.moveColumn(col, rightVirtualIndexAdjusted);
          columnApi.autoSizeColumn(col.getColId(), true);
          columnApi.moveColumn(col, rc.originalIndex);
    
          if (initialWidthSet) {
            delete colDef.minWidth;
            col.setColDef(colDef, userDef);
          }
    
        });
      }
    }
    

    And to call this on the columns that are virtualized this can be done like so:

     onFirstDataRendered() {
       const unhiddenCols = this.columnApi.getAllGridColumns().filter(x => { return x.isVisible() }).map(x => x.getColId());
       this.columnSizeService.sizeColumns(unhiddenCols, this.columnApi);
       this.finishedLoading = true;
     }
    

    Similarly if you have just shown some hidden columns you can do something like:

      params.columnApi.setColumnsVisible(colIds, show);
      if (show) {
        this.columnSizeService.sizeShownColumns(colIds, params.columnApi);
      }
    

    where show is a boolean that indicates whether the columns are being shown or hidden.

    There is another solution: You can programmatically scroll across the grid. This is about 100ms faster for 30 columns. The problem is this becomes visible to the user so you have to cover up the grid with a large loading element or skeleton with an absolute position so that the user does not see this happening. And soit is perceived as slow by the user.