3
votes

I'm using ag-Grid with React to test their enterprise row grouping feature. I want to change the row grouping column name during runtime, but I haven't been able to do this.

When I make changes to AgGridReact's columnDefs prop, the changes are reflected in the table. But changes to the autoGroupColumnDef prop aren't rendered, even though the debug logs show that the change was detected. Here's an example in TypeScript (using React hooks for state):

import React, { FC, useState } from 'react';
import 'ag-grid-enterprise';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-balham.css';

const AgGridTest: FC = () => {
  const rowData = [
    { col1: 'a', col2: 0, col3: 0 },
    { col1: 'b', col2: 1, col3: 1 },
    { col1: 'c', col2: 2, col3: 0 },
    { col1: 'd', col2: 3, col3: 1 },
    { col1: 'a', col2: 4, col3: 0 },
    { col1: 'b', col2: 5, col3: 1 },
    { col1: 'c', col2: 6, col3: 0 },
    { col1: 'd', col2: 7, col3: 1 },
  ];
  const [columnDefs, setColumnDefs] = useState([
    { headerName: 'Column 1', field: 'col1', rowGroup: true }, // initial group column
    { headerName: 'Column 2', field: 'col2', rowGroup: false },
    { headerName: 'Column 3', field: 'col3', rowGroup: false },
  ]);

  const [autoGroupColumnDef, setAutoGroupColumnDef] = useState(
    {headerName: 'col1 Initial'} // auto group column name is 'col1 Initial' to start
  );

  const groupByColumn = (field: string): void => {
    // this successfully changes the grouping column...
    setColumnDefs(
      prevColumnDefs => prevColumnDefs.map(
        colDef => colDef.field === field ? 
          {...colDef, rowGroup: true} :
          {...colDef, rowGroup: false}
      )
    );
    // ...but this won't change the auto group column name!
    setAutoGroupColumnDef({
      headerName: `${field} Group Column`,
    });
  }

  return (
    <div className="ag-theme-balham" style={{ height: '300px' }}>
      <button onClick={() => groupByColumn('col1')}>Group by Column 1</button>
      <button onClick={() => groupByColumn('col2')}>Group by Column 2</button>
      <AgGridReact 
        rowData={rowData}
        columnDefs={columnDefs}
        autoGroupColumnDef={autoGroupColumnDef}
        debug // enable debug logs
      />
    </div>
  );
}

const App: React.FC = () => <AgGridTest />

export default App;

I tried calling the grid API's api.refreshHeader but that didn't work either:

const AgGridTest: FC = () => {
  // ...

  // store the api in an instance variable
  // when the on ready event makes it available
  const api = useRef<GridApi>();
  const onGridReady = (params: GridReadyEvent) => {
    api.current = params.api;
  }

  const groupByColumn = (field: string): void => {
    // ... re-assign the grouping cols ...
  }

  useEffect(() => {
    // run this after grouping changes have been rendered
    gridApi.current && gridApi.current.refreshHeader(); // doesn't work
  });

  return (
    <div>
      <AgGridReact
        onGridReady={onGridReady}
        // ...
      />
    </div>
  );
}

I tried looking for an API function to set the autoColumnGroupDef but I couldn't find one. The documentation only mentions setting properties through gridOptions. The API has a function for updating regular column definitions: api.setColumnDefs. This is what AgGridReact calls when the columnDefs prop changes (relevant code snippets here and here). But when the autoGroupColumnDef property is changed, AgGridReact simply overwrites the autoGroupColumnDef property in the gridOptions object (relevant code snippet). There doesn't seem to be any associated setter function.

There doesn't even seem to be a way to get at the auto group column objects. columnApi.getAllColumns/getColumnGroup only return regular columns. The auto group column list is kept separately in a private instance variable in columnController (relevant code snippet), which has a public getPrimaryAndSecondaryAndAutoColumns accessor exposed, except that we can't access the columnController API.

Any ideas on how can I modify the auto group column names at runtime? Or do I have to disable them and create my own group columns?

This question was asked here and here but they never received answers (and I don't understand the comment in the second question).

3

3 Answers

3
votes

As you are probably aware, ag-grid is pure js, and the React and Angular versions are wrappers around that.

I've been using the Angular version for about a year, and customizing it heavily, and I've found that many of the gridOptions, even though they are bound properties, have no effect after initial instantiation of the grid. I suspect that autoGroupColumnDef is one of these.

So, I would try creating your own column groups, as per the link that you referenced.

If that doesn't work, the (ugly) alternative is to destroy the grid, and then recreate it with the new gridOptions reflecting your new autoGroupColumnDef

One other hard-won piece of advice: If you use the setColumnDefs API to update the column definitions, it works fine, BUT, if you later want save and restore the grid state, getting the grid state from the API yields column names with "_1" appended to them, and if you set the column state with that, you get an error about the columns not being found. The solution is to set the columnDefs to an empty array first, and then set your actual columnDefs.

2
votes

@GreyBeardedGeek mentioned that using the setColumnDefs API mutates the column names. This sent me down a rabbit hole which may prove useful to others.

tl;dr

If you want to change column order, width, sort state, filtering, row grouping, or pivoting at runtime through the API (e.g. api.setColumnDefs) instead of only through ag-Grid's UI controls (e.g. allowing the user to resize columns by clicking and dragging), you must:

  1. Specify a unique colId for each column.
  2. Set gridOptions.deltaColumnMode to true.
  3. If you want to also allow the user to modify these properties (e.g. click-n-drag resize), add an explicit handler that watches for these changes and updates the columnDefs accordingly.

I'll now explain why (sort of - some parts are still unclear to me).

If you provide colIds but leave deltaColumnMode false...

I made a plunkr where you can create a new column and update the new column and an existing column's sort order, width, and whether it's a row grouping column. The first thing to note is that the initial width in the columnDefs for column 1 is applied, but clicking "increase width" for either the existing or new column does nothing. Grouping doesn't work either - the cell renderer will change, centering the content, but no grouping will actually be performed.

This is because deltaColumnMode is false by default, and when deltaColumnMode is false, runtime changes to certain properties (which include width and row grouping) are ignored. From the docs:

By default when new columns are loaded into the grid, the following properties are not used:

  • Column Order
  • Aggregation Function (colDef.aggFunc)
  • Width (colDef.width)
  • Pivot (colDef.pivot or colDef.pivotIndex)
  • Row Group (colDef.rowGroup or colDef.rowGroupIndex)
  • Pinned (colDef.pinned)

This is done on purpose to avoid unexpected behaviour for the application user.

For example - suppose the application user rearranges the order of the columns. Then if the application sets new column definitions for the purposes of adding one extra column into the grid, then it would be a bad user experience to reset the order of all the columns.

Similarly, when we create the new column, we place it first in the list of columnDefs, but this order is ignored and it's instead created at the end. This is because column order is one of the properties ignored when deltaColumnMode is false.

The second thing to note is that sort order is preserved even when we make columnDefs updates. Because we provided colIds, ag-Grid knew that when we changed the columnDefs, we were updating existing columns and not creating new ones. ag-Grid preserves internal state like sort order and row grouping when the colIds are the same. From the docs:

Comparison of column definitions is done on 1) object reference comparison and 2) column ID eg colDef.colId. If either the object reference matches, or the column ID matches, then the grid treats the columns as the same column. For example if the grid has a column with ID 'country' and the user sets new columns, one of which also has ID of 'country', then the old country column is kept in place of the new one keeping it's internal state such as width, position, sort and filter.

If you don't provide any colIds and leave deltaColumnMode false...

If you don't provide colIds in the initial columnDefs, ag-Grid will assign them internally:

If the user provides colId in the column definition, then this is used, otherwise the field is used. If both colId and field then colId gets preference. If neither colId or field then numeric is provided. Then finally the ID ensured to be unique by appending '_[n]' where n is the first positive number that allows uniqueness.

Whenever columnDefs change, ag-Grid decides whether the changes are to an existing column or to a new column by checking the colIds (or column object references). From the docs:

If you are updating the columns (not replacing the entire set) then you must either provide column ID's or reuse the column definition object instances. Otherwise the grid will not know that the columns are in fact the same columns.

When we provided colIds, ag-Grid knew we were updating existing columns. But when we don't provide colIds, ag-Grid doesn't know what to do. Let's look at the same plunkr as before but with colIds removed. Now, when we click the button to increase column 1's width, three things happen: 1) column 1's width does increase, but 2) its order shifts so it's now the rightmost column, and 3) if we keep clicking "increase width", we see in the console that column 1's colId was initially undefined, becomes "col1" (the field name) after the first increase, and then alternates between "col1_1" and "col1" on every other increase.

I suspect that this strange behavior is due to the way ag-Grid resolves columnDefs updates when colIds aren't provided. Because no colId is provided, ag-Grid seems to think that we're creating a new column when we update column 1's width. Since ag-Grid thinks this is a new column, the width change is actually applied, even though deltaColumnMode is false (remember that in the previous example with colIds set, the width property was applied when the column was created, just not during runtime). But because deltaColumnMode is false, when we create a new column, its order is ignored - hence, column 1 is placed as the rightmost column. (Though, since the docs say that width/column order should be ignored "when new columns are loaded into the grid", I don't quite know why the initial width is applied but column order is not).

Similar things happen when we group by column 1, but 1) all colIds change for all columns, and 2) the grouping column is moved to be the leftmost column. I suspect the first is because all columns change when grouping is applied, and the second is because the grouping column is moved to the left by default, as when we create a new column and group by it, it'll be moved to the left, too.

You may notice that sometimes after making changes to either column 1 or the new column, you're not able to apply any sorting to them. Remember how ag-Grid alternates between appending and removing a "_1" to the colIds? Since api.setSortModel looks up columns by colIds, every time the colId has a "_1" appended to it, api.setSortModel won't be able to find the column to apply sorting (and will fail silently, apparently). We could fix this by getting the current colIds with columnApi.getAllColumns() and using those instead.

Also, since no colIds are provided, ag-Grid doesn't preserve column state (e.g. sort order) between columnDef changes. If you sort a column and then increase its width or group by it, the sorting will be reset. This is because sorting is attached to the internal state of the column object, but the column objects are destroyed when the columnDefs change.

If you provide colIds and set deltaColumnMode to true...

Here's a plunkr to demonstrate. Now, things work as we expect: runtime changes are applied, the new column is created in the leftmost position, and sort order is preserved between columnDef updates. However, there's a reason that deltaColumnMode is false by default. Again, from the docs (emphasis mine):

This is done on purpose to avoid unexpected behaviour for the application user.

For example - suppose the application user rearranges the order of the columns. Then if the application sets new column definitions for the purposes of adding one extra column into the grid, then it would be a bad user experience to reset the order of all the columns.

Likewise if the user changes an aggregation function, or the width of a column, or whether a column was pinned, all of these changes the user does should not get undone because the application decided to update the column definitions.

To change this behaviour and have column attributes above (order, width, row group etc) take effect each time the application updates the grid columns, then set gridOption.deltaColumnMode=true. The responsibility is then on your application to make sure the provided column definitions are in sync with what is in the grid if you don't want undesired visible changes - eg if the user changes the width of a column, the application should listen to the grid event columnWidthChanged and update the applications column definition with the new width - otherwise the width will reset back to the default after the application updates the column definitions into the grid.

Try dragging column 1 to a different order and then increasing its width. You'll see that its order gets reset to its original order in the columnDefs, just as the docs say. To preserve column order, you need to set a listener for the column drag event and update the columnDefs with the new order - see this plunkr and the onDragStopped handler for an example.

And if you want to allow the user to change aggregation functions, width, pivoting, row grouping, or pinning through the ag-Grid default UI controls, you'll need to add listeners for these changes, too.

0
votes

I've been tackling this issue for a while, and I finally found a solution (albeit a bit out of the box, but it works)

If the only thing you want to change is the header, you can use dom manipulation. You just have to get the exact element that holds the title that you wish to change. In my case, I wanted to show the list of selected aggregations in the order they were selected, and so I was able to accomplish it via:

 aggregateMyColumn() {
    this.gridColumnApi.setRowGroupColumns(this.orderedAggregationFields);
    if(this.orderedAggregationFields.length > 0) {
      const container = document.querySelector("#agGrid");
      const matches = container.querySelector("div[col-id='ag-Grid-AutoColumn']");
      const final = container.querySelector("span[role='columnheader']");
      this.renderer.setProperty(final, 'innerHTML', this.orderedAggregationFields.toString());
    }
 }

Note: there are reasons to use renderer2 to set properties/change innerhtml (something something security), here are some useful links though:

https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll https://www.ninjadevcorner.com/2019/04/dom-manipulation-using-angular-renderer2.html