Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added customBodyRenderLite / removed object deprecation for column values #1339

Merged
merged 3 commits into from
Jun 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ The component accepts the following props:
|:--:|:-----|:-----|
|**`title`**|array|Title used to caption table
|**`columns`**|array|Columns used to describe table. Must be either an array of simple strings or objects describing a column
|**`data`**|array|Data used to describe table. Must be either an array containing objects of key/value pairs with values that are strings or numbers, or arrays of strings or numbers (Ex: data: [{"Name": "Joe", "Job Title": "Plumber", "Age": 30}, {"Name": "Jane", "Job Title": "Electrician", "Age": 45}] or data: [["Joe", "Plumber", 30], ["Jane", "Electrician", 45]])
|**`data`**|array|Data used to describe table. Must be either an array containing objects of key/value pairs with values that are strings or numbers, or arrays of strings or numbers (Ex: data: [{"Name": "Joe", "Job Title": "Plumber", "Age": 30}, {"Name": "Jane", "Job Title": "Electrician", "Age": 45}] or data: [["Joe", "Plumber", 30], ["Jane", "Electrician", 45]]). The **customBodyRender** and **customBodyRenderLite** options can be used to control the data diaplay.
|**`options`**|object|Options used to describe table
|**`components`**|object|Custom components used to render the table

Expand Down Expand Up @@ -259,7 +259,8 @@ const columns = [
#### Column Options:
|Name|Type|Default|Description
|:--:|:-----|:--|:-----|
|**`customBodyRender`**|function||Function that returns a string or React component. Used as display data within all table cells of a given column. `function(value, tableMeta, updateValue) => string`|` React Component` [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/component/index.js)
|**`customBodyRender`**|function||Function that returns a string or React component. Used to display data within all table cells of a given column. The value returned from this function will be used for filtering in the filter dialog. If this isn't need, you may want to consider customBodyRenderLite instead. `function(value, tableMeta, updateValue) => string`|` React Component` [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/component/index.js)
|**`customBodyRenderLite`**|function||Function that returns a string or React component. Used to display data within all table cells of a given column. This method performs better than customBodyRender but has the following caveats: <p><ul><li>The value returned from this function is **not** used for filtering, so the filter dialog will use the raw data from the data array.</li><li>This method only gives you the dataIndex and rowIndex, leaving you to lookup the column value.</li></ul></p>`function(dataIndex, rowIndex) => string`&#124;` React Component` [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/large-data-set/index.js)
|**`customFilterListOptions`**|{render: function, update: function}|| (These options only affect the filter chips that display after filters are selected. To modify the filters themselves, see `filterOptions`) `render` returns a string or array of strings used as the chip label(s). `function(value) => string OR arrayOfStrings`, `update` returns a `filterList (see above)` allowing for custom filter updates when removing the filter chip [Example](https://github.com/gregnb/mui-datatables/blob/master/examples/column-filters/index.js)
|**`customHeadRender`**|function||Function that returns a string or React component. Used as display for column header. `function(columnMeta, handleToggleColumn, sortOrder) => string`&#124;` React Component`
|**`display`**|string|'true'|Display column in table. `enum('true', 'false', 'excluded')`
Expand Down
7 changes: 4 additions & 3 deletions examples/array-value-columns/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,14 @@ class Example extends React.Component {
options: {
filter: true,
filterType: 'multiselect',
customBodyRender: (value) => {
customBodyRenderLite: (dataIndex) => {
let value = data[dataIndex][5];
return value.map( (val, key) => {
return <Chip label={val} key={key} />;
});
}
},
}
}
},
];


Expand Down
5 changes: 2 additions & 3 deletions examples/column-filters/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,8 @@ class Example extends React.Component {
{
name: "Age",
options: {
customBodyRender: (val, tableMeta) => {
console.log(val);
console.dir(tableMeta);
customBodyRenderLite: (dataIndex) => {
let val = data[dataIndex][3];
return val;
},
filter: true,
Expand Down
5 changes: 4 additions & 1 deletion examples/csv-export/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ class Example extends React.Component {
{
name: 'Age',
options: {
customBodyRender: value => <div><span>{value}</span></div>
customBodyRenderLite: (dataIndex) => {
let value = data[dataIndex][3];
return <div><span>{value}</span></div>;
}
}
},
{
Expand Down
9 changes: 4 additions & 5 deletions examples/custom-action-columns/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class Example extends React.Component {
filter: false,
sort: false,
empty: true,
customBodyRender: (value, tableMeta, updateValue) => {
customBodyRenderLite: (dataIndex) => {
return (
<button onClick={() => {
const { data } = this.state;
Expand All @@ -69,9 +69,9 @@ class Example extends React.Component {
filter: false,
sort: false,
empty: true,
customBodyRender: (value, tableMeta, updateValue) => {
customBodyRenderLite: (dataIndex, rowIndex) => {
return (
<button onClick={() => window.alert(`Clicked "Edit" for row ${tableMeta.rowIndex}`)}>
<button onClick={() => window.alert(`Clicked "Edit" for row ${rowIndex} with dataIndex of ${dataIndex}`)}>
Edit
</button>
);
Expand Down Expand Up @@ -116,7 +116,7 @@ class Example extends React.Component {
filter: false,
sort: false,
empty: true,
customBodyRender: (value, tableMeta, updateValue) => {
customBodyRenderLite: (dataIndex) => {
return (
<button onClick={() => {
const { data } = this.state;
Expand Down Expand Up @@ -168,7 +168,6 @@ class Example extends React.Component {
filter: true,
filterType: 'dropdown',
responsive: 'vertical',
page: 2,
onColumnSortChange: (changedColumn, direction) => console.log('changedColumn: ', changedColumn, 'direction: ', direction),
onChangeRowsPerPage: numberOfRows => console.log('numberOfRows: ', numberOfRows),
onChangePage: currentPage => console.log('currentPage: ', currentPage)
Expand Down
17 changes: 11 additions & 6 deletions examples/large-data-set/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ class Example extends React.Component {
label: "Name",
options: {
filter: true,
customBodyRender: (val) => {
customBodyRenderLite: (dataIndex) => {
let val = this.state.data[dataIndex].name;
return val;
}
}
Expand All @@ -61,7 +62,8 @@ class Example extends React.Component {
label: "Modified Title Label",
options: {
filter: true,
customBodyRender: (val, tableMeta) => {
customBodyRenderLite: (dataIndex) => {
let val = this.state.data[dataIndex].title;
return val;
}
}
Expand All @@ -86,7 +88,8 @@ class Example extends React.Component {
options: {
filter: true,
sort: false,
customBodyRender: (val) => {
customBodyRenderLite: (dataIndex) => {
let val = this.state.data[dataIndex].salary;
return val;
}
}
Expand All @@ -97,7 +100,8 @@ class Example extends React.Component {
options: {
filter: true,
sort: false,
customBodyRender: (val) => {
customBodyRenderLite: (dataIndex) => {
let val = this.state.data[dataIndex].phone;
return val;
}
}
Expand All @@ -108,7 +112,8 @@ class Example extends React.Component {
options: {
filter: true,
sort: false,
customBodyRender: (val) => {
customBodyRenderLite: (dataIndex) => {
let val = this.state.data[dataIndex].email;
return val;
}
}
Expand Down Expand Up @@ -144,4 +149,4 @@ class Example extends React.Component {
}
}

export default Example;
export default Example;
70 changes: 40 additions & 30 deletions src/MUIDataTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ class MUIDataTable extends React.Component {
filterType: PropTypes.oneOf(['dropdown', 'checkbox', 'multiselect', 'textField', 'custom']),
customHeadRender: PropTypes.func,
customBodyRender: PropTypes.func,
customBodyRenderLite: PropTypes.func,
customFilterListOptions: PropTypes.oneOfType([
PropTypes.shape({
render: PropTypes.func,
Expand Down Expand Up @@ -671,16 +672,6 @@ class MUIDataTable extends React.Component {
})
: data.map(row => columns.map(col => leaf(row, col.name)));

// We need to determine if object data exists in the transformed structure, as this is currently not allowed and will cause errors if not handled by a custom renderer
const hasInvalidData =
transformedData.filter(
data => data.filter(d => typeof d === 'object' && d !== null && !Array.isArray(d)).length > 0,
).length > 0;
if (hasInvalidData)
this.warnDep(
'Passing objects in as data is not supported. Consider using ids in your data and linking it to external objects if you want to access object data from custom render functions.',
);

return transformedData;
};

Expand Down Expand Up @@ -726,26 +717,41 @@ class MUIDataTable extends React.Component {
});
}

if (typeof column.customBodyRender === 'function') {
const rowData = tableData[rowIndex].data;
tableMeta = this.getTableMeta(rowIndex, colIndex, rowData, column, data, this.state);
const funcResult = column.customBodyRender(value, tableMeta);
if (column.filter !== false) {
if (typeof column.customBodyRender === 'function') {
const rowData = tableData[rowIndex].data;
tableMeta = this.getTableMeta(rowIndex, colIndex, rowData, column, data, this.state);
const funcResult = column.customBodyRender(value, tableMeta);

if (React.isValidElement(funcResult) && funcResult.props.value) {
value = funcResult.props.value;
} else if (typeof funcResult === 'string') {
value = funcResult;
if (React.isValidElement(funcResult) && funcResult.props.value) {
value = funcResult.props.value;
} else if (typeof funcResult === 'string') {
value = funcResult;
}
}
}

if (filterData[colIndex].indexOf(value) < 0 && !Array.isArray(value)) {
filterData[colIndex].push(value);
} else if (Array.isArray(value)) {
value.forEach(element => {
if (filterData[colIndex].indexOf(element) < 0) {
filterData[colIndex].push(element);
}
});
if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
// it's extremely rare but possible to create an object without a toString method, ex: var x = Object.create(null);
// so this check has to be made
value = value.toString ? value.toString() : '';
}

if (filterData[colIndex].indexOf(value) < 0 && !Array.isArray(value)) {
filterData[colIndex].push(value);
} else if (Array.isArray(value)) {
value.forEach(element => {
let elmVal;
if ((typeof element === 'object' && element !== null) || typeof element === 'function') {
elmVal = element.toString ? element.toString() : '';
} else {
elmVal = element;
}

if (filterData[colIndex].indexOf(elmVal) < 0) {
filterData[colIndex].push(elmVal);
}
});
}
}
}

Expand Down Expand Up @@ -895,7 +901,9 @@ class MUIDataTable extends React.Component {
let columnValue = row[index];
let column = columns[index];

if (column.customBodyRender) {
if (column.customBodyRenderLite) {
displayRow.push(column.customBodyRenderLite);
} else if (column.customBodyRender) {
const tableMeta = this.getTableMeta(rowIndex, index, row, column, dataForTableMeta, {
...this.state,
filterList: filterList,
Expand All @@ -916,9 +924,11 @@ class MUIDataTable extends React.Component {
: funcResult.props && funcResult.props.value
? funcResult.props.value
: columnValue;
}

displayRow.push(columnDisplay);
displayRow.push(columnDisplay);
} else {
displayRow.push(columnDisplay);
}

const columnVal = columnValue === null || columnValue === undefined ? '' : columnValue.toString();

Expand Down
2 changes: 1 addition & 1 deletion src/components/TableBodyCell.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ function TableBodyCell(props) {
className,
)}
{...otherProps}>
{children}
{typeof children === 'function' ? children(dataIndex, rowIndex) : children}
Copy link
Collaborator

@wdh2100 wdh2100 Jun 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I never knew that children could be a function. Interesting.
I thought it was only a react node(element)

Does children mean columns?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's a pattern called "Function as Child Component". It's useful in certain situations. Here the children equal either the value that will be displayed in the column, or the customBodyRenderLite function, which will generate the value for the column.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@patorjk thanks!

</div>,
];

Expand Down
77 changes: 77 additions & 0 deletions test/MUIDataTable.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1938,4 +1938,81 @@ describe('<MUIDataTable />', function() {
expect(getCollatorComparator()('testStringA', 'testStringB')).to.equal(-1);
});
});

it('should correctly execute customBodyRender methods based on filtering and data', () => {
let filteredData = [];
let customBodyRenderCb = spy();
let customBodyRenderLiteCb = spy();
let customBodyRenderNoFilterCb = spy();
const options = {
rowsPerPage: 5,
rowsPerPageOptions: [5]
};

const data = [
['a','b'],
['a','b'],
['a','b'],
['a','b'],
['a','b'],

['a','b'],
['a','b'],
['a','b'],
['a','b'],
['a','b'],

['a','b'],
['a','b'],
['a','b'],
['a','b'],
['a','b'],
];

const columns = [
{
name: 'firstName',
label: 'First Name',
options: {
customBodyRender: () => {
customBodyRenderCb();
return '';
}
}
},
{
name: 'lastName',
label: 'Last Name',
options: {
customBodyRenderLite: () => {
customBodyRenderLiteCb();
return '';
}
},
},
{
name: 'phone',
label: 'Phone',
options: {
filter: false,
customBodyRender: () => {
customBodyRenderNoFilterCb();
return '';
}
},
}
];

const fullWrapper = mount(<MUIDataTable columns={columns} data={data} options={options} />);
fullWrapper.unmount();

// lite only gets executed for the 5 entries shown
assert.strictEqual(customBodyRenderLiteCb.callCount, 5);

// regular gets executed 15 times for filtering, and 15 more times for display
assert.strictEqual(customBodyRenderCb.callCount, 30);

// regular with no filtering gets executed 15 times for display
assert.strictEqual(customBodyRenderNoFilterCb.callCount, 15);
});
});