AG Grid - Invoke callbacks from custom header component

I would like to add a button to a column header in AG grid, and invoke callbacks when the button is clicked. Following the docs, I created a .js file in the assets folder, where I registered a custom component,

var dagcomponentfuncs = window.dashAgGridComponentFunctions = window.dashAgGridComponentFunctions || {};

dagcomponentfuncs.ButtonHeader = function (props) {
    return React.createElement(
        'button',
        {
            onClick: () => props.setData(),
            className: props.className,
            style: props.style,
        },
        props.displayName)
}

Next, I selected the custom component in the columns defition,

import dash_ag_grid as dag
from dash import Dash, html
import pandas as pd

df = pd.read_csv(
    "https://raw.githubusercontent.com/plotly/datasets/master/ag-grid/olympic-winners.csv"
)

app = Dash(__name__)
app.layout = html.Div(
    [
        dag.AgGrid(
            id="column-definition-ID",
            rowData=df.to_dict("records"),
            columnDefs={
                "field": "athlete",
                "headerName": "Athlete",
                "headerComponent": "ButtonHeader"  # select custom component
            },
            columnSize="sizeToFit",
        ),
    ]
)

if __name__ == "__main__":
    app.run(debug=True, port=7777)

Now, the component renders correctly,

but I get an error, when I click the button,

react-dom@16.v2_9_3m1693901393.14.0.js:321 Uncaught TypeError: Cannot read properties of undefined (reading 'id')

I guess it might be because the ‘setData’ function was designed for rows, and when invoked for columns, the ‘node’ property is undefined, which yields an error when trying to acces the id here,

Now my question is - is there any workaround to invoke a callback from a column? And/or is there any way to access the ‘setProps’ property of the grid, so that I can set (arbitrary) props myself? That would be neat :slight_smile:

Maybe @jinnyzor or @AnnMarieW knows? :smiley:

Hello @Emil,

Yes, there are a few workarounds.

The props here gives you access to the grid’s api, so you can use it to trigger another event that you can access.

You can pass the clicks to the sorting:

You’d listen to columnState or virtualRowData for this.

You could also update a cell value on some hidden column in the first row.

You’d listen to cellValueChanged for this.

You also have access to the gridOptions, from which you can access some of the grid event listeners:

dagcomponentfuncs.ButtonHeader = function (props) {
    return React.createElement(
        'button',
        {
            onClick: () => {
            props.api.gridOptionsService.gridOptions.onCellClicked({value: {'headerClicked': props.column.colId},
            column: props.column.colId,
            rowIndex: null, node: []})},
            className: props.className,
            style: props.style,
        },
        props.displayName)
}

As far as setting arbitrary props, this isnt a supported way from the grid, this was discussed a little, but the idea was given that Dash would eventually support this. XD

But… I have a workaround for that too:

function get_setProps(dom, traverseUp = 0) {
    const key = Object.keys(dom).find(key=>{
        return key.startsWith("__reactFiber$") // react 17+
            || key.startsWith("__reactInternalInstance$"); // react <17
    });
    const domFiber = dom[key];
    if (domFiber == null) return null;

    // react <16
    if (domFiber._currentElement) {
        let compFiber = domFiber._currentElement._owner;
        for (let i = 0; i < traverseUp; i++) {
            compFiber = compFiber._currentElement._owner;
        }
        return compFiber;
    }

    // react 16+
    const GetCompFiber = fiber=>{
        //return fiber._debugOwner; // this also works, but is __DEV__ only
        let parentFiber = fiber.return;
        while (typeof parentFiber.type == "string") {
            parentFiber = parentFiber.return;
        }
        return parentFiber;
    };
    let compFiber = GetCompFiber(domFiber);
    for (let i = 0; i < traverseUp; i++) {
        compFiber = GetCompFiber(compFiber);
    }
    if (compFiber.stateNode) {
        if (Object.keys(compFiber.stateNode.props).includes('setProps')) {
            return compFiber.stateNode.props
        }
        if (Object.keys(compFiber.stateNode).includes('setProps')) {
            return compFiber.stateNode
        }
    }
    for (let i = 0; i < 30; i++) {
        compFiber = GetCompFiber(compFiber);
        if (compFiber.stateNode) {
            if (Object.keys(compFiber.stateNode.props).includes('setProps')) {
                return compFiber.stateNode.props
            }
            if (Object.keys(compFiber.stateNode).includes('setProps')) {
                return compFiber.stateNode
            }
        }
    }
    throw new Error("this is not the dom you are looking for")
}

Whole thing together:

function get_setProps(dom, traverseUp = 0) {
    const key = Object.keys(dom).find(key=>{
        return key.startsWith("__reactFiber$") // react 17+
            || key.startsWith("__reactInternalInstance$"); // react <17
    });
    const domFiber = dom[key];
    if (domFiber == null) return null;

    // react <16
    if (domFiber._currentElement) {
        let compFiber = domFiber._currentElement._owner;
        for (let i = 0; i < traverseUp; i++) {
            compFiber = compFiber._currentElement._owner;
        }
        return compFiber;
    }

    // react 16+
    const GetCompFiber = fiber=>{
        //return fiber._debugOwner; // this also works, but is __DEV__ only
        let parentFiber = fiber.return;
        while (typeof parentFiber.type == "string") {
            parentFiber = parentFiber.return;
        }
        return parentFiber;
    };
    let compFiber = GetCompFiber(domFiber);
    for (let i = 0; i < traverseUp; i++) {
        compFiber = GetCompFiber(compFiber);
    }
    if (compFiber.stateNode) {
        if (Object.keys(compFiber.stateNode.props).includes('setProps')) {
            return compFiber.stateNode.props
        }
        if (Object.keys(compFiber.stateNode).includes('setProps')) {
            return compFiber.stateNode
        }
    }
    for (let i = 0; i < 30; i++) {
        compFiber = GetCompFiber(compFiber);
        if (compFiber.stateNode) {
            if (Object.keys(compFiber.stateNode.props).includes('setProps')) {
                return compFiber.stateNode.props
            }
            if (Object.keys(compFiber.stateNode).includes('setProps')) {
                return compFiber.stateNode
            }
        }
    }
    throw new Error("this is not the dom you are looking for")
}

dagcomponentfuncs.ButtonHeader = function (props) {
    return React.createElement(
        'button',
        {
            onClick: () => {get_setProps(event.target.closest('div.ag-theme-alpine')).setProps({'cellClicked': {value: `header clicked ${props.column.colId}`}})},
            className: props.className,
            style: props.style,
        },
        props.displayName)
}

app:

import dash_ag_grid as dag
from dash import Dash, html, Input, Output
import pandas as pd
import json

df = pd.read_csv(
    "https://raw.githubusercontent.com/plotly/datasets/master/ag-grid/olympic-winners.csv"
)

app = Dash(__name__)
app.layout = html.Div(
    [
        dag.AgGrid(
            id="column-definition-ID",
            rowData=df.to_dict("records"),
            columnDefs=[{
                "field": "athlete",
                "headerName": "Athlete",
                "headerComponent": "ButtonHeader"  # select custom component
            }],
            columnSize="sizeToFit",
        ),
        html.Div(id='output')
    ]
)

@app.callback(Output('output', 'children'),
              Input('column-definition-ID', 'cellClicked'))
def sorting(s):
    return json.dumps(s)

if __name__ == "__main__":
    app.run(debug=True, port=7777)

Now, with all this said, this should technically be an issue on github because it shouldnt be dependent if you have the component in a row. :smiley:

2 Likes

@jinnyzor thanks for a great and elaborate answer! :smiley:

I love your (crazy) function for looking up the ‘setProps’ function, but I think I’ll use the ‘props.api.gridOptionsService.gridOptions.onCellClicked’ approach - at least for now :wink:

1 Like

The nice thing about the get_setProps is that you arent confined to props that the grid has, completely arbitrary. :slight_smile:

Heads up on this @Emil, in v30+ your solution will actually go away. Hopefully by the time of the release, Plotly will have released their version of setProps.

nudge @alexcjohnson :smiley:

1 Like

Thanks for the heads up! Could you elaborate on which part of the solution stops working? :upside_down_face:

This one:

dagcomponentfuncs.ButtonHeader = function (props) {
    return React.createElement(
        'button',
        {
            onClick: () => {
            props.api.gridOptionsService.gridOptions.onCellClicked({value: {'headerClicked': props.column.colId},
            column: props.column.colId,
            rowIndex: null, node: []})},
            className: props.className,
            style: props.style,
        },
        props.displayName)
}

props.api.gridOptionsServer doesnt exist anymore.

You can do a workaround that I posted here:

1 Like

Have you considered exposing grid events directly, similar to dash-leaflet? :upside_down_face:

https://www.dash-leaflet.com/docs/events

The events are already exposed using the underlying grid api. You can add your own event listeners with it. XD

Once the setProps is available, you can directly interact with Dash components without the need for workarounds.

And yes, I have thought about the ability to do something similar, but that’s why we exposed the api, its more than an event engine. :slight_smile:

The main difference being how you define the listeners I guess, with the grid you can give it like this:

 app.clientside_callback(
    """function (id) {dash_ag_grid.getApiAsync(id).then((grid) => {assignListeners(grid)})
return window.dash_clientside.no_update}""",
Output("grid","id"), Input("grid","id")
)
1 Like

If I register listeners this way, will they be cleaned up automatically, when the component is unmounted?

Yes, they should be.

1 Like