AG-Grid automaticcaly scroll to newly added row

Hello,
I have a ag-grid with pagination, and I would to automatically scroll to the newly added row when I add one.
I tried to get inspired from to Scroll To | Dash for Python Documentation | Plotly page, but I still have one error saying “Cannot read properties of undefined (reading ‘rowIndex’)” when I click on the add button. The row is well added but it does not scoll to the end page.
Here is my code :

import dash_ag_grid as dag
from dash import Dash, dcc, html, Input, Output, clientside_callback, MATCH, callback, State
import pandas as pd
import uuid
import json

app = Dash(__name__)

class ComponentAIO(html.Div):
    class ids:
        grid = lambda aio_id: {
            "component": "ComponentAIO",
            "subcomponent": "grid",
            "aio_id": aio_id,
        }
        entry = lambda aio_id: {
            "component": "ComponentAIO",
            "subcomponent": "entry",
            "aio_id": aio_id,
        }
        button = lambda aio_id: {
            "component": "ComponentAIO",
            "subcomponent": "button",
            "aio_id": aio_id,
        }       

        # # Make the ids class a public class
    ids = ids

    # Define the arguments of the All-in-One component
    def __init__(self,  aio_id=None):
        if aio_id is None:
            aio_id = str(uuid.uuid4())

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


        columnDefs = [
            {
                "headerName": "Row #",
                "valueGetter": {"function": "params.node.rowIndex"},
                "width": 100,
            },
            {"field": "id", "width": 75},
        ] + [{"field": c} for c in df.columns if c != "id"]

        super().__init__(
            [
                html.Label("Scroll to row id:"),
                dcc.Input(id=self.ids.entry(aio_id), min=0, max=df.shape[0], type="number"),
                html.Button( "Add ",id=self.ids.button(aio_id)),
                dag.AgGrid(
                    id=self.ids.grid(aio_id),
                    columnDefs=columnDefs,
                    rowData=df.to_dict("records"),
                    defaultColDef={"resizable": True, "sortable": True},
                    getRowId="params.data.id",
                    dashGridOptions={"pagination": True, "paginationPageSize": 50},
                ),
            ]
        )

    @callback(        
        Output(ids.grid(MATCH), component_property='rowTransaction',allow_duplicate=True),
        Output(ids.entry(MATCH), component_property='value',allow_duplicate=True),
        Input(ids.button(MATCH), "n_clicks"),
        State(ids.grid(MATCH), component_property='rowData'),
        prevent_initial_call=True,
    )
    def add_row(n_clicks:int,data):
        print(len(data))
        #print(data[0])
        last_id=data[-1].get("id")
        print("last id is ",data[-1].get("id"))
        empty_row={'id':last_id+1,'athlete': 'me', 'age': '0', 'coutry': 'Zimbabwe', 'year': 2023, 'sport':"ski"}     
        return  {"add": [empty_row]},last_id+1

    clientside_callback(
        """function (rowId) {
            if (rowId) {
                grid = dash_ag_grid.getApi("""+ json.dumps(ids.grid("go-to-pagination"))+""")        
                rowIndex = grid.getRowNode(rowId).rowIndex        
                pageTarget = Math.floor(rowIndex / grid.paginationGetPageSize())
                grid.paginationGoToPage(pageTarget)
            }
            return {"rowId": rowId.toString()}
        }""",
        Output(ids.grid("go-to-pagination"), "scrollTo"),
        Input(ids.entry("go-to-pagination"), "value"),
        prevent_initial_call=True,
    )

app.layout = ComponentAIO("go-to-pagination")
if __name__ == "__main__":
    app.run(debug=True)

Can someone help me ? Thanks :grinning:

Hello @eghza,

Welcome to the community!

We actually already have a function for you to use:

The issue I think you are running into is that you need to wait until the data has been populated before it scrolls to it.

If you are always using rowTransaction to add and nothing else, you could listen to rowTransaction in a clientside callback, if it isn’t empty, then you store the next id that you will scroll to, when rowTransaction clears, then you trigger your scroll to event by using your clientside variable id as the arguments to the above mentioned data point.

Also, another thing you can do is to pass ‘async’: False to the rowTransaction, this should make the grid send the data quicker because it is performing the update as soon as it receives the data from the server vs when it is an opportune moment for the grid.

Thank you for your answer and greetings :smiley:
I had already read attentively the doc your pointed and as it says that scrollTo cannot be used with pagination (or at least, when the scrolling target in not in the actual page) , I followed the example with pagination showedf on the doc.

I have tried this :

and it works perfectly :slight_smile:

I would have another question , in the clientside callback, the example accesses the grid like this

grid = dash_ag_grid.getApi("""+ json.dumps(ids.grid("go-to-pagination"))+""")    

But I would like to put the code into an AIO component, so the callback input and output would be

Output(ids.grid(MATCH), "scrollTo"),
        Input(ids.entry(MATCH), "value"),
        prevent_initial_call=True,

but

grid = dash_ag_grid.getApi("""+ json.dumps(ids.grid(MATCH))+""")  

does not work. Would you know I could access the grid without knowing its exact id ?

Ok, so here is a working an example of what I was talking about, you needed to wait for the rowTransaction to clear and the rowTransaction needed to be async: False in order to have the grid populate immediately.

import dash_ag_grid as dag
from dash import Dash, dcc, html, Input, Output, clientside_callback, MATCH, callback, State
import pandas as pd
import uuid
import json

app = Dash(__name__)

class ComponentAIO(html.Div):
    class ids:
        grid = lambda aio_id: {
            "component": "ComponentAIO",
            "subcomponent": "grid",
            "aio_id": aio_id,
        }
        entry = lambda aio_id: {
            "component": "ComponentAIO",
            "subcomponent": "entry",
            "aio_id": aio_id,
        }
        button = lambda aio_id: {
            "component": "ComponentAIO",
            "subcomponent": "button",
            "aio_id": aio_id,
        }

        # # Make the ids class a public class
    ids = ids

    # Define the arguments of the All-in-One component
    def __init__(self,  aio_id=None):
        if aio_id is None:
            aio_id = str(uuid.uuid4())

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


        columnDefs = [
            {
                "headerName": "Row #",
                "valueGetter": {"function": "params.node.rowIndex"},
                "width": 100,
            },
            {"field": "id", "width": 75},
        ] + [{"field": c} for c in df.columns if c != "id"]

        super().__init__(
            [
                html.Label("Scroll to row id:"),
                dcc.Input(id=self.ids.entry(aio_id), min=0, max=df.shape[0], type="number",
                          debounce=True),
                html.Button( "Add ",id=self.ids.button(aio_id)),
                dag.AgGrid(
                    id=self.ids.grid(aio_id),
                    columnDefs=columnDefs,
                    rowData=df.to_dict("records"),
                    defaultColDef={"resizable": True, "sortable": True},
                    getRowId="params.data.id",
                    dashGridOptions={"pagination": True, "paginationPageSize": 50},
                ),
            ]
        )

    @callback(
        Output(ids.grid(MATCH), component_property='rowTransaction',allow_duplicate=True),
        Output(ids.entry(MATCH), component_property='value',allow_duplicate=True),
        Input(ids.button(MATCH), "n_clicks"),
        State(ids.grid(MATCH), component_property='rowData'),
        prevent_initial_call=True,
    )
    def add_row(n_clicks:int,data):
        print(len(data))
        #print(data[0])
        last_id=data[-1].get("id")
        print("last id is ",data[-1].get("id"))
        empty_row={'id':last_id+1,'athlete': 'me', 'age': '0', 'country': 'Zimbabwe', 'year': 2023, 'sport':"ski"}
        return  {"add": [empty_row], 'async': False},last_id+1

    clientside_callback(
        """function (rt, rowId, id) {
            if (!rt) {
                if (rowId) {
                    grid = dash_ag_grid.getApi(id)     
                    rowIndex = grid.getRowNode(rowId).rowIndex        
                    pageTarget = Math.floor(rowIndex / grid.paginationGetPageSize())
                    if (pageTarget != grid.paginationGetCurrentPage()) {
                        grid.paginationGoToPage(pageTarget)
                    }
                    return {"rowId": rowId.toString()}
                }
            }
            return window.dash_clientside.no_update
        }""",
        Output(ids.grid("go-to-pagination"), "scrollTo"),
        Input(ids.grid('go-to-pagination'), 'rowTransaction'),
        Input(ids.entry("go-to-pagination"), "value"),
        State(ids.grid('go-to-pagination'), 'id'),
        prevent_initial_call=True,
    )

app.layout = ComponentAIO("go-to-pagination")
if __name__ == "__main__":
    app.run(debug=True)

Now… for the id, Plotly does some formatting on the id, so its easier to pass it as a state from the component, especially from AIO/Pattern-Matching components.

I adjusted your dcc.Input to have debounce, as typing in there was causing it to jump all over the grid when it didnt need to. :slight_smile:

1 Like

Thank you so much for your example. It all works perfectly :slight_smile:
Have a nice day

1 Like