Hello,
I build an app Mantine, Ag Grid and dark/light support.
I thought I finally found a solution to neatly get dark/light on the AgGrid, but it seems it retriggering the load. AS you can see i used routing_callback_inputs={“theme”: Input(“theme_store”, “data”)}, in my app and then also the them in the layout.
Thats probably triggering to re-render the grid.
Does someone know a better way, to change the class of the grid, to support light/dark theme on switch? I used mutation observer before, but did not like that so much.
Here is a minimal example using pages.
app.py
import dash_mantine_components as dmc
from dash import (
Dash,
html,
dcc,
_dash_renderer,
callback,
Input,
Output,
State,
clientside_callback,
page_container,
register_page,
)
from dash_iconify import DashIconify
_dash_renderer._set_react_version("18.2.0")
import dash_ag_grid as dag
import pandas as pd
import datetime
from dash.exceptions import PreventUpdate
app = Dash(
__name__,
external_stylesheets=dmc.styles.ALL,
routing_callback_inputs={"theme": Input("theme_store", "data")},
suppress_callback_exceptions=True,
use_pages=True,
)
app.layout = dmc.MantineProvider(
[
dcc.Store(
id="theme_store",
storage_type="local",
data="light",
),
dmc.Group(
[
html.H1("Grid Page"),
dmc.Group(
[
dmc.ActionIcon(
[
DashIconify(
icon="radix-icons:sun",
width=25,
id="light-theme-icon",
),
DashIconify(
icon="radix-icons:moon",
width=25,
id="dark-theme-icon",
),
],
variant="transparent",
id="theme_switch",
size="lg",
),
]
),
]
),
page_container,
],
id="theme_provider",
forceColorScheme="light",
)
if __name__ == "__main__":
app.run(debug=True)
The page:
import dash_mantine_components as dmc
from dash import (
Dash,
html,
dcc,
_dash_renderer,
callback,
Input,
Output,
State,
clientside_callback,
page_container,
register_page,
)
from dash_iconify import DashIconify
_dash_renderer._set_react_version("18.2.0")
import dash_ag_grid as dag
import pandas as pd
import datetime
from dash.exceptions import PreventUpdate
now = datetime.datetime.now()
df = pd.DataFrame(
{
"Column 1": [1, 2, 3, 4, 5, 6],
"Column 2": ["A", "B", "C", "D", "E", "F"],
"Column 3": [now, now, now, now, now, now],
"Column 4": [["testing"]] * 6,
}
)
cols = [{"field": col} for col in df.columns]
cols[-1]["cellEditor"] = {"function": "AllFunctionalComponentEditors"}
cols[-1]["cellEditorParams"] = {"component": dmc.Select(data=["rawr", "testing"])}
cols[-1]["cellEditorPopup"] = True
cols[-1]["headerName"] = "dcc.Dropdown(multi)"
cols[-2]["cellEditor"] = {"function": "AllFunctionalComponentEditors"}
cols[-2]["cellEditorParams"] = {"component": dmc.DateInput()}
cols[-2]["cellEditorPopup"] = True
cols[-2]["headerName"] = "dmc.DateInput"
register_page(__name__, path="/", name="Test")
def layout(theme=None, **kwargs):
if theme == None:
raise PreventUpdate
return full_layout(theme)
def full_layout(theme):
className = "ag-theme-quartz-dark" if theme == "dark" else "ag-theme-quartz"
return html.Div(
[
dmc.Select(data=["rawr", "testing"], id="test_input"),
dag.AgGrid(
id="grid_id",
columnDefs=cols,
columnSize="autoSize",
defaultColDef={"editable": True},
className=className,
),
]
)
@callback(Output("grid_id", "rowData"), Input("test_input", "value"))
def update_data(data):
print("render")
return df.to_dict("records")
clientside_callback(
"""
function(data) {
return data
}
""",
Output("theme_provider", "forceColorScheme"),
Input("theme_store", "data"),
)
clientside_callback(
"""
function(n_clicks, data) {
return data === "dark" ? "light" : "dark";
}
""",
Output("theme_store", "data"),
Input("theme_switch", "n_clicks"),
State("theme_store", "data"),
prevent_initial_call=True,
)
the assets:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root[data-mantine-color-scheme="dark"] #dark-theme-icon {
display: none;
}
:root[data-mantine-color-scheme="light"] #light-theme-icon {
display: none;
}
:root[data-mantine-color-scheme="dark"] #logo_dark {
display: none;
}
:root[data-mantine-color-scheme="light"] #logo_light {
display: none;
}
#_pages_content {
height: 100%;
}
#react-entry-point {
height: 100vh;
}
#container {
height: 100vh;
}
/* the unnamed parent of _pages_content */
#container>div {
height: 100%;
}
html,
body {
height: 100vh;
}
.pydantic-form-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
dashAgGridFunctions.py
var dagfuncs = (window.dashAgGridFunctions = window.dashAgGridFunctions || {});
const {useRef, createElement, useEffect, useCallback, forwardRef, useMemo} = React
dagfuncs.AllFunctionalComponentEditors = forwardRef(({ value, ...params }, ref) => {
const comp = params.colDef.cellEditorParams.component;
const node = useRef(params.api.getRowNode(params.node.id));
const newValue = useRef(value);
const escaped = useRef(null);
const editorRef = useRef(null);
const handleStop = ({ event, ...params }) => {
setTimeout(() => {
if (!escaped.current) {
node.current.setDataValue(params.column.colId, newValue.current);
}
}, 1);
};
if (params.colDef.cellEditorPopup) {
comp['props']['style'] = { ...comp.props?.style, width: params.column.actualWidth - 2 };
}
const setProps = (propsToSet) => {
if ('value' in propsToSet) {
newValue.current = propsToSet.value;
}
};
const handleKeyDown = ({ key, ...event }) => {
if (key == "Escape") {
escaped.current = true;
}
};
useEffect(() => {
params.api.addEventListener('cellEditingStopped', handleStop);
document.addEventListener('keydown', handleKeyDown);
params.colDef.suppressKeyboardEvent = (params) => params.editing && params.event.key != 'Tab';
return () => {
document.removeEventListener('keydown', handleKeyDown);
delete params.colDef.suppressKeyboardEvent;
params.api.removeEventListener('cellEditingStopped', handleStop);
};
}, []);
useEffect(() => {
if (editorRef.current) {
if (editorRef.current.querySelector('input')) {
editorRef.current.querySelector('input').focus();
} else {
editorRef.current.focus()
}
}
}, [editorRef.current]);
const el = useMemo(() =>
createElement(
'div',
{ ref: editorRef, tabIndex: 0 },
createElement(window[comp['namespace']][comp['type']], { ...comp.props, setProps, value })
),
[]);
return el;
});
dagfuncs.AllComponentEditors = class {
// gets called once before the renderer is used
init(params) {
// create the cell
this.params = params;
this.eInput = document.createElement('div');
this.root = ReactDOM.createRoot(this.eInput)
// sets editor value to the value from the cell
this.value = params.value;
}
// gets called once when grid ready to insert the element
getGui() {
return this.eInput;
}
// focus and select can be done after the gui is attached
afterGuiAttached() {
const comp = this.params.colDef.cellEditorParams.component
this.params.colDef.suppressKeyboardEvent = (params) => params.editing && params.event.key != 'Tab'
if (this.params.colDef.cellEditorPopup) {
comp['props']['style'] = {...comp.props?.style, width: this.params.column.actualWidth-2}
}
const setProps = (propsToSet) => {
if ('value' in propsToSet) {
this.value = propsToSet.value
}
this.root.render(
createElement(window[comp.namespace][comp.type], {...comp.props, value: this.value, setProps})
)
}
this.root.render(
createElement(window[comp.namespace][comp.type], {...comp.props, value: this.value, setProps})
)
}
// returns the new value after editing
getValue() {
delete this.params.colDef.suppressKeyboardEvent
return this.value;
}
};