Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Links "DE#nnn" prior to version 2.0 point to the Dash Enterprise closed-source D
## [unreleased]
### Added
- [#453](https://github.com/plotly/dash-ag-grid/pull/453) Test for changelog entry
- [#455](https://github.com/plotly/dash-ag-grid/pull/455) Added support for dynamic `detailCellRendererParams` in Master/Detail, including dynamic detail-grid column definitions.

### Changed
- [#452](https://github.com/plotly/dash-ag-grid/pull/452)
Expand Down
30 changes: 30 additions & 0 deletions docs/examples/enterprise/master_detail.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
{"headerName": "Pop. (Metro area)", "field": "population_metro"},
]

detailColumnDefsSimple = [
{"headerName": "City", "field": "city"},
{"headerName": "Pop. (City proper)", "field": "population_city"},
]

rowData = [
{
"country": "China",
Expand Down Expand Up @@ -164,6 +169,31 @@
),
body=True,
),
html.Hr(),
dbc.Card(
dcc.Markdown(
"Use a JavaScript function in `detailCellRendererParams` to dynamically define detail columns per expanded row."
),
body=True,
),
dbc.Card(
dag.AgGrid(
id="master-detail-table-dynamic-columns",
columnDefs=masterColumnDefs,
rowData=rowData,
columnSize="sizeToFit",
enableEnterpriseModules=True,
masterDetail=True,
detailCellRendererParams={
"function": """params.data.region === "Asia"
? {detailGridOptions: {columnDefs: %s}, detailColName: "cities", suppressCallback: true}
: {detailGridOptions: {columnDefs: %s}, detailColName: "cities", suppressCallback: true}"""
% (detailColumnDefsSimple, detailColumnDefs)
},
dashGridOptions={"detailRowAutoHeight": True},
),
body=True,
),
]
)

Expand Down
36 changes: 22 additions & 14 deletions src/lib/components/AgGrid.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -600,22 +600,30 @@ DashAgGrid.propTypes = {
* Specifies the params to be used by the default detail Cell Renderer. See Detail
* Grids.
*/
detailCellRendererParams: PropTypes.shape({
/**
* Grid options for detail grid in master-detail view.
*/
detailGridOptions: PropTypes.any,
detailCellRendererParams: PropTypes.oneOfType([
PropTypes.shape({
/**
* Grid options for detail grid in master-detail view.
*/
detailGridOptions: PropTypes.any,

/**
* Column name where detail grid data is located in main dataset, for master-detail view.
*/
detailColName: PropTypes.string,
/**
* Column name where detail grid data is located in main dataset, for master-detail view.
*/
detailColName: PropTypes.string,

/**
* Default: true. If true, suppresses the Dash callback in favor of using the data embedded in rowData at the given detailColName.
*/
suppressCallback: PropTypes.bool,
}),
/**
* Default: true. If true, suppresses the Dash callback in favor of using the data embedded in rowData at the given detailColName.
*/
suppressCallback: PropTypes.bool,
}),
PropTypes.shape({
/**
* JavaScript function that receives detail row params and returns detailCellRendererParams.
*/
function: PropTypes.string,
}),
]),

/**
* The style to give a particular row. See Row Style.
Expand Down
63 changes: 47 additions & 16 deletions src/lib/fragments/AgGrid.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,33 @@ export function DashAgGrid(props) {
const convertOneRef = useRef();
const convertAllPropsRef = useRef();

const normalizeDetailCellRendererParams = useCallback(
(value) => {
if (!value || typeof value !== 'object') {
return value;
}

let adjustedVal = value;
if ('suppressCallback' in value) {
adjustedVal = {
...adjustedVal,
getDetailRowData: value.suppressCallback
? suppressGetDetail(value.detailColName)
: callbackGetDetail,
};
}
if ('detailGridOptions' in value) {
adjustedVal = assocPath(
['detailGridOptions', 'components'],
components,
adjustedVal
);
}
return convertAllPropsRef.current(adjustedVal);
},
[suppressGetDetail, callbackGetDetail, components]
);

const convertOne = useCallback(
(value, target) => {
if (value) {
Expand Down Expand Up @@ -617,29 +644,32 @@ export function DashAgGrid(props) {
}, value);
}
if (GRID_NESTED_FUNCTIONS[target]) {
let adjustedVal = value;
if (
target === 'rowSelection' &&
typeof value === 'string'
) {
// to still support rowSelection='single' | 'multiple' deprecated in v32.3.4
return value;
}
if ('suppressCallback' in value) {
adjustedVal = {
...adjustedVal,
getDetailRowData: value.suppressCallback
? suppressGetDetail(value.detailColName)
: callbackGetDetail,
};
}
if ('detailGridOptions' in value) {
adjustedVal = assocPath(
['detailGridOptions', 'components'],
components,
adjustedVal
);
if (target === 'detailCellRendererParams') {
if (has('function', value)) {
const dynamicDetailParams =
convertMaybeFunction(value);
if (typeof dynamicDetailParams === 'function') {
return (params) =>
normalizeDetailCellRendererParams(
dynamicDetailParams(params)
Comment thread
BSd3v marked this conversation as resolved.
);
}
return normalizeDetailCellRendererParams(
dynamicDetailParams
);
}
}
const adjustedVal =
target === 'detailCellRendererParams'
? normalizeDetailCellRendererParams(value)
: value;
return convertAllPropsRef.current(adjustedVal);
}
if (GRID_DANGEROUS_FUNCTIONS[target]) {
Expand Down Expand Up @@ -675,13 +705,14 @@ export function DashAgGrid(props) {
[
convertCol,
convertMaybeFunctionNoParams,
convertMaybeFunction,
normalizeDetailCellRendererParams,
Comment thread
BSd3v marked this conversation as resolved.
suppressGetDetail,
callbackGetDetail,
components,
convertAllPropsRef.current,
convertFunction,
handleDynamicStyle,
convertMaybeFunction,
]
);

Expand Down
107 changes: 106 additions & 1 deletion tests/test_recursive_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def test_rf001_recursive_functions(dash_duo):
{
"city": "Shanghai",
"population_city": 24870895,
"population_metro": "NA",
"population_metro": 0,
},
{
"city": "Beijing",
Expand Down Expand Up @@ -268,6 +268,111 @@ def test_rf001_recursive_functions(dash_duo):
).get_attribute("style")


def test_rf003_master_detail_dynamic_columns(dash_duo):
app = Dash(__name__)
masterColumnDefs = [
{
"headerName": "Country",
"field": "country",
"cellRenderer": "agGroupCellRenderer",
},
{"headerName": "Region", "field": "region"},
]

detailColumnDefsSimple = [
{"headerName": "City", "field": "city"},
{"headerName": "Pop. (City proper)", "field": "population_city"},
]
detailColumnDefs = detailColumnDefsSimple + [
{"headerName": "Pop. (Metro area)", "field": "population_metro"},
]

rowData = [
{
"country": "China",
"region": "Asia",
"cities": [
{
"city": "Shanghai",
"population_city": 24870895,
"population_metro": 0,
},
],
},
{
"country": "United States",
"region": "Americas",
"cities": [
{
"city": "New York",
"population_city": 8398748,
"population_metro": 19303808,
},
],
},
]

app.layout = html.Div(
[
dag.AgGrid(
id="grid",
columnDefs=masterColumnDefs,
rowData=rowData,
columnSize="sizeToFit",
enableEnterpriseModules=True,
masterDetail=True,
detailCellRendererParams={
"function": """params.data.region === "Asia"
? {detailGridOptions: {columnDefs: %s}, detailColName: "cities", suppressCallback: true}
: {detailGridOptions: {columnDefs: %s}, detailColName: "cities", suppressCallback: true}"""
% (detailColumnDefsSimple, detailColumnDefs)
},
dashGridOptions={"detailRowAutoHeight": True},
)
]
)

dash_duo.start_server(app)

grid = utils.Grid(dash_duo, "grid")
grid.wait_for_cell_text(0, 0, "China")

grid.get_cell_expandable(0, 0).click()
until(
lambda: [
e.text
for e in dash_duo.find_elements(
'#grid .ag-details-grid [aria-rowindex="1"] .ag-header-cell-text'
)
]
== ["City", "Pop. (City proper)"],
timeout=3,
)
dash_duo.wait_for_text_to_equal(
'#grid .ag-details-grid [row-index="0"] [aria-colindex="2"]', "24870895"
)

grid.get_cell_collapsable(0, 0).click()
until(
lambda: len(dash_duo.find_elements("#grid .ag-details-grid")) == 0,
timeout=3,
)
grid.get_cell_expandable(1, 0).click()
until(
lambda: [
e.text
for e in dash_duo.find_elements(
'#grid .ag-details-grid [aria-rowindex="1"] .ag-header-cell-text'
)
]
== ["City", "Pop. (City proper)", "Pop. (Metro area)"],
timeout=3,
)
dash_duo.wait_for_text_to_equal(
'#grid .ag-details-grid [row-index="0"] [aria-colindex="3"]', "19303808"
)


def test_rf002_recursive_functions_server(dash_duo):
app = Dash(__name__)
masterColumnDefs = [
Expand Down
Loading