AG Grid with dcc.Clipboard

Based on a request in another post, here is more info on using the dcc.Clipboad component with Dash Ag Grid.

If you want to copy text from the grid, the easiest way is to enable text selection . However if you would like to include a copy button, you can use the dcc.Clipboard in a cell renderer. . The nice thing about using the dcc.Clipboard component is that it’s possible to format the content that’s copied to the clipboard. :tada:

Note for AG Grid Enterprise users - see also: React Data Grid: Clipboard

Example 1 Copy a cell.

ag-grid-dcc-clipboard-cell

In this example, when you click on the copy button, the content of the cell is copied to the clipboard.

A couple of things to note in the JavaScript function

  • It creates a div that includes the content of the cell, props.value, and the dcc.Clipboard component.
  • Use the content prop to specify what is copied to the clipboard. Here we use content: props.value
  • it’s possible to format the content that’s copied to the clipboard. Try changing the content prop to
    content: "Ticker: " + props.value,
  • With dcc.Clipboard, it’s necessary to define setProps as a function in the cell renderer, even though it’s unused. And thanks for that tip @jinnyzor.

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

data = {
    "ticker": ["AAPL", "MSFT", "AMZN", "GOOGL"],
    "company": ["Apple", "Microsoft", "Amazon", "Alphabet"],
    "price": [154.99, 268.65, 100.47, 96.75],
}
df = pd.DataFrame(data)

columnDefs = [

    {"headerName": "Company", "field": "company", "filter": True},
    {
        "headerName": "Last Close Price",
        "type": "rightAligned",
        "field": "price",
        "valueFormatter": {"function": """d3.format("($,.2f")(params.value)"""},
        "editable": True,
    },
    {
        "headerName": "Stock Ticker",
        "field": "ticker",
        "cellRenderer": "DCC_Clipboard",
    },
]

defaultColDef = {
    "resizable": True,
    "sortable": True,
    "editable": False,
}


grid = dag.AgGrid(
    id="grid",
    columnDefs=columnDefs,
    rowData=df.to_dict("records"),
    columnSize="autoSize",
    defaultColDef=defaultColDef,
    dashGridOptions={"rowHeight": 48},
)


app = Dash(__name__)

app.layout = html.Div(
    [
        dcc.Markdown("Example of cellRenderer with custom dcc.Clipboard"),
        grid,
        dcc.Textarea(style={"width": 600})
    ],
    style={"margin": 20},
)



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


"""
Put the following in the dashAgGridComponentFunctions.js file in the assets folder

---------------

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

dagcomponentfuncs.DCC_Clipboard = function (props) {
  return React.createElement("div", {}, [
    props.value,
    React.createElement(window.dash_core_components.Clipboard, {
      content: props.value,
      title: "Copy cell",
      style: {
        display: "inline-block",
        verticalAlign: "top",
        paddingLeft: 10,
        cursor: "pointer",
      },
      setProps: () => {},
    }),
  ]);
};
"""

Example 2 Copy a row

In this example the dcc.Clipboard component is in a separate column. When you click on the icon it copies the entire row.

ag-grid-dcc-clipboard-row

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

data = {
    "ticker": ["AAPL", "MSFT", "AMZN", "GOOGL"],
    "company": ["Apple", "Microsoft", "Amazon", "Alphabet"],
    "price": [154.99, 268.65, 100.47, 96.75],
}
df = pd.DataFrame(data)

columnDefs = [
    {
        "headerName": "",
        "cellRenderer": "CopyRow",
        "lockPosition": "left",
        "maxWidth": 25,
        "filter": False,
        "cellStyle": {"paddingLeft": 5},
    },
    {
        "headerName": "Stock Ticker",
        "field": "ticker",
    },
    {"headerName": "Company", "field": "company", "filter": True},
    {
        "headerName": "Last Close Price",
        "type": "rightAligned",
        "field": "price",
        "valueFormatter": {"function": """d3.format("($,.2f")(params.value)"""},
        "editable": True,
    },
]


defaultColDef = {
    "resizable": True,
    "sortable": True,
    "editable": False,
}


grid = dag.AgGrid(
    id="grid",
    columnDefs=columnDefs,
    rowData=df.to_dict("records"),
    columnSize="autoSize",
    defaultColDef=defaultColDef,
    dashGridOptions={"rowHeight": 48},
)


app = Dash(__name__)

app.layout = html.Div(
    [
        dcc.Markdown("Example of cellRenderer with custom dcc.Clipboard"),
        grid,
        dcc.Textarea(style={"width": 600}),
    ],
    style={"margin": 20},
)


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


"""
Put the following in the dashAgGridComponentFunctions.js file in the assets folder

---------------

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

dagcomponentfuncs.CopyRow = function (props) {
  return React.createElement("div", {}, [
    React.createElement(window.dash_core_components.Clipboard, {
      content: JSON.stringify(props.data),
      title: "Copy Row",
      style: {
        display: "inline-block",        
        verticalAlign: "top",
        paddingLeft: 3,
        cursor: "pointer",
      },
      setProps: () => {},
    }),
  ]);
};

"""


Pro tip:
To see what’s included with the props in the cell renderer function, include a console.log() before the return statement, then you can see it in the browser console.

dagcomponentfuncs.DCC_Clipboard = function (props) {
  console.log("props", props)
  return React.createElement("div", {}, [
     
9 Likes

Thank you @AnnMarieW for those interesting examples! :rainbow:
You should tag “dag-docs” :wink:

2 Likes

Huge thanks, @AnnMarieW. I think the latest bit I was missing was setProps in the JS code. I had the content and title set to props.value and could verify on hovering that the values are correct, but clicking the clipboard icon was not doing anything.

1 Like

Nice example. I have a bit of another case: after pushing dcc.Clipboard, a user should receive a generated string (exactly URL) from the backend (callback I guess). How can I do that? In your example, the custom component manipulates the existing info. I usually use a dash-table for data frames and tried to add dcc.Clipboard to each row of dash-table but it doesn’t work)

Hi @ilnur
Can you say more about what you want copied with the clipboard?

Let’s imagine that we have the next app (I leave some details):

app.layout = html.Div([
    "Generate URL to the   file", 
    dcc.Clipboard(id='clipboard'),
    dash_table.DataTable(id='data-table2', ...)
])

@app.callback(
    Output('clipboard', 'content', allow_duplicate=True),
    Input('clipboard', 'n_clicks'),
    State('data-table2', 'active_cell'),
    State('data-table2', 'data'),
    prevent_initial_call=True
)
def generate_url(_, a_c, data):
    file_id = data[a_c['row_id']]['id']
    URL = generate_url_to_file_on_s3(file_id)
    return URL

@app.callback(
    Output('data-table2', 'data'),
    Input("tabs", "active_tab"),
    prevent_initial_call=True
)
def get_data(a_t):
    if a_t == 'output_layout':
        data = get_data_for_the_table_from_db()
        return data

As you can see here, the clipboard is outside of the data table. I would like to add a clipboard component to each row and after the user has clicked on it, the callback will return the generated URL to the corresponding row in the table. Seems that I couldn’t make it with dash.data-table, and I saw this example, where you use dag. I tried to figure it out by myself but I’m not so experienced in js and don’t see any example of how to interact with common dash callback from dag.

The easiest way would be to create the link in the cell renderer. When you click on the clipboard button, you have access to all the data in the row (and more). So it would basically be moving the generate_url_to_file_on_s3 functionality to the clientside.

return React.createElement("div", {}, [
  React.createElement(window.dash_core_components.Clipboard, {
      content: "/path-to-file/" + props.data.id,

If that’s not possible, another option is to have a regular button with a clipboard icon. It could save the id to a dcc.Store, which can trigger the callback to create the URL and update a hidden clipboard button. That’s a little more convoluted though, and I can help with the example if the first method doesn’t work for you.

URL is generated through the aws sdk. For Python that is a boto3 package and I don’t know whether it exists for js. Even if it is, I should distance from js as much as possible, because my team and I are mostly Python developers. Therefore second approach is more appropriate in this case. I’ll to figure it out by myself and if I couldn’t, I’ll contact you)

@ilnur Just a quick update - you don’t need to use a dcc.Store if you are putting a button in the row. You can add extra data , so when you click the button, it will include the file id from the row and will be available in the callback using the cellRendererData prop. The button might look something like this: (this assumes you are using dash-bootstrap-components


dagcomponentfuncs.CopyButton = function (props) {
    const {setData, data} = props;

    function onClick() {
        setData(data.id);
    }
    return React.createElement(
        'button',
        {
            onClick: onClick,
            className: 'btn',
        },
        React.createElement("i", {className: 'bi bi-clipboard'})
    );
};


1 Like

Hello Ann, I spent some time and make the next example that works great:

import json
from datetime import datetime

from dash import Dash, dcc, html
from dash.dependencies import Input, Output, State
from dash.dash_table import DataTable
import dash_bootstrap_components as dbc
import dash_ag_grid as dag
import pandas as pd

app = Dash(
    __name__,
    external_stylesheets=[
        dbc.themes.BOOTSTRAP,
        dbc.icons.BOOTSTRAP
    ],
)

column_names = ["ticker", "company", "price"]
dt_columns = [{'id': v, 'name': v.capitalize()} for v in column_names]
dag_columns = [{'field': v, 'headerName': v.capitalize()} for v in column_names]

data = {
    "ticker": ["AAPL", "MSFT", "AMZN", "GOOGL"],
    "company": ["Apple", "Microsoft", "Amazon", "Alphabet"],
    "price": [154.99, 268.65, 100.47, 96.75],
    "link": ['Link' for _ in range(4)]
}

df = pd.DataFrame(data)

data_table = DataTable(
        id="data-table",
        data=df.to_dict("records"),
        columns=dt_columns + [{'id': 'link', 'name': 'Link'}],
        markdown_options={'html': True},
        editable=False,
        style_cell={'textAlign': 'left'},
    )

dag_table = dag.AgGrid(
        id="dag-table",
        columnDefs=dag_columns + [
            {
                'headerName': 'Link',
                "field": "link",
                "cellRenderer": "CopyButton",
                # "cellRendererParams": {"className": "btn btn-success"},
                "maxWidth": 80,
                "filter": False,
                "cellStyle": {"display": "block", "margin": "0px auto"}
            },
        ],
        rowData=df.to_dict("records"),
        # className="ag-theme-alpine-dark",
        columnSize="sizeToFit",
    )

tabs = dbc.Tabs(
    [
        dbc.Tab(data_table, label="Data Table", tab_id="data_table"),
        dbc.Tab(dag_table, label="DAG Table", tab_id="dag_table"),
    ],
    id="tabs",
    active_tab="data_table",
    class_name="nav-fill nav-pills",
    # class_name="nav-pills nav-justified",
)


app.layout = html.Div([
    html.H1("Dash App"),
    dcc.Clipboard(id='clipboard', n_clicks=0, style={"display": "none"}),
    tabs
    # data_table,
    # dag_table
])


@app.callback(
    Output("clipboard", 'content', allow_duplicate=True),
    Output("clipboard", 'n_clicks', allow_duplicate=True),
    Input('data-table', 'active_cell'),
    State("data-table", "data"),
    State('clipboard', 'n_clicks'),
    prevent_initial_call=True
)
def to_clipboard_from_data_table(a_c, rows, n):
    row = rows[a_c['row']]
    return json.dumps(row), n + 1


@app.callback(
    Output("clipboard", 'content', allow_duplicate=True),
    Output("clipboard", 'n_clicks', allow_duplicate=True),
    Input("dag-table", "cellRendererData"),
    State('dag-table', 'rowData'),
    State('clipboard', 'n_clicks'),
    prevent_initial_call=True
)
def to_clipboard_from_dag(row_data, rows, n):
    row = rows[int(row_data['rowId'])]
    return json.dumps(row), n + 1


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

"""
var dagcomponentfuncs = (window.dashAgGridComponentFunctions = window.dashAgGridComponentFunctions || {});

dagcomponentfuncs.CopyButton = function (props) {
    const {setData, data} = props;

    function onClick() {
        setData(data.id);
    }
    return React.createElement(
        'button',
        {
            onClick: onClick,
            // className: props.className,
            // className: 'btn btn-primary',
            className: 'btn',

        },
        React.createElement("i", {className: 'bi bi-clipboard-data', style: {color: "black"}})
    );
};
"""

As you can see, I made examples with a dash-data-table and a dag-table. Dash table is more functional, but we cannot put buttons into it, instead, we can manipulate it with an active-cell prop. Unfortunately, both examples weren’t working at my main app, corresponding response is returning from the server but after ctrl+v, it doesn’t paste str that should be in the clipboard. And today, when I was looking into the browser’s dev-tools I saw that there was some problem with CSS property, when I corrected it, the clipboard’s logic started working correctly. Or maybe because I update dash, dbc and dag packages…

THANK YOU VERY MUCH!

P.S. There was a little mistake with class names in React.createElement(“i”, {className: ‘bi bi-clipboard-data’, style: {color: “black”}}

P.P.S. Is there any active-cell property in dag-table?

P.P.P.S. Also, there is some problem with dag-table “AG Grid: tried to call sizeColumnsToFit() but the grid is coming back with zero width, maybe the grid is not visible yet on the screen?”. How fit the dag table to the screen, if it is in another tab that isn’t default?

Hello @ilnur,

DAG copy and paste is available with their Enterprise, which isnt too expensive when considering a lot of the functionality that it brings over.

There are work arounds for different things, but range selection is something you cant really workaround.

You can add an event listener for cellFocused and send the same data that the cellClicked property has to the grid, which it pretty similar to how the focus works with DataTable.

Column sizes are based upon the grid being visible, if the grid isnt visible then this will not work properly. However, please note that your example seemed to work properly for me.

Hello @jinnyzor . Does it work like this for you?

Yes, that’s what mine looked like.

But it shouldn’t look this way, If you change active_tab=“dag_table” prop value, you can see how it should look.

Ah, it you want to use sizeToFit, that renders based upon the grid being ready, since the grid is ready behind the scenes, it cant determine how to adjust the size of the columns.

For this purpose, I recommend using and initialFlex on the column def of 1. If you want sizeToFit, if you wanted to use responsiveSizeToFit, then just pass flex: 1.

1 Like