From 5f699bfdd6770aeb2af13e78dfe3d89f7ca5df7e Mon Sep 17 00:00:00 2001 From: SergeyAlexeev Date: Mon, 21 Aug 2017 09:51:20 +0300 Subject: [PATCH] feat(react-grid): provide the custom data accessors capability (#264) Fixes #176 --- .../src/plugins/editing-state/computeds.js | 12 ++ .../plugins/editing-state/computeds.test.js | 28 +++++ .../src/plugins/filtering-state/computeds.js | 8 +- .../plugins/filtering-state/computeds.test.js | 16 +-- .../src/plugins/grouping-local/computeds.js | 6 +- .../plugins/grouping-local/computeds.test.js | 5 +- .../src/plugins/sorting-state/computeds.js | 14 ++- .../plugins/sorting-state/computeds.test.js | 14 ++- .../custom-data-accessors-in-columns.jsx | 110 +++++++++++++++++ .../custom-data-accessors-in-columns.test.jsx | 11 ++ .../data-accessors/custom-data-accessors.jsx | 105 ++++++++++++++++ .../custom-data-accessors.test.jsx | 11 ++ .../editing/edit-row-controlled.test.jsx | 2 +- .../src/bootstrap3/editing/edit-row.test.jsx | 2 +- .../dx-react-demos/src/demo-data/generator.js | 37 ++++-- packages/dx-react-demos/src/demo-registry.js | 10 ++ .../custom-data-accessors-in-columns.jsx | 110 +++++++++++++++++ .../custom-data-accessors-in-columns.test.jsx | 14 +++ .../data-accessors/custom-data-accessors.jsx | 105 ++++++++++++++++ .../custom-data-accessors.test.jsx | 14 +++ .../src/templates/table-cell.jsx | 8 +- .../src/templates/table-cell.test.jsx | 32 ++--- .../src/templates/table-cell.jsx | 8 +- .../src/templates/table-cell.test.jsx | 31 ++--- .../docs/guides/data-accessors.md | 114 ++++++++++++++++++ .../docs/reference/editing-state.md | 7 +- packages/dx-react-grid/docs/reference/grid.md | 3 + .../docs/reference/local-filtering.md | 1 + .../docs/reference/local-grouping.md | 1 + .../docs/reference/local-sorting.md | 1 + .../docs/reference/table-edit-row.md | 2 + .../docs/reference/table-view.md | 4 +- packages/dx-react-grid/src/grid.jsx | 22 +++- packages/dx-react-grid/src/grid.test.jsx | 76 ++++++++++++ .../src/plugins/editing-state.jsx | 13 ++ .../src/plugins/editing-state.test.jsx | 52 ++++++++ .../src/plugins/local-filtering.jsx | 3 +- .../src/plugins/local-grouping.jsx | 4 +- .../src/plugins/local-sorting.jsx | 2 +- .../src/plugins/table-edit-row.jsx | 41 +++++-- .../src/plugins/table-edit-row.test.jsx | 41 ++++++- .../dx-react-grid/src/plugins/table-view.jsx | 6 +- .../src/plugins/table-view.test.jsx | 8 +- site/_data/docs/navigation.yml | 2 + 44 files changed, 1019 insertions(+), 97 deletions(-) create mode 100644 packages/dx-react-demos/src/bootstrap3/data-accessors/custom-data-accessors-in-columns.jsx create mode 100644 packages/dx-react-demos/src/bootstrap3/data-accessors/custom-data-accessors-in-columns.test.jsx create mode 100644 packages/dx-react-demos/src/bootstrap3/data-accessors/custom-data-accessors.jsx create mode 100644 packages/dx-react-demos/src/bootstrap3/data-accessors/custom-data-accessors.test.jsx create mode 100644 packages/dx-react-demos/src/material-ui/data-accessors/custom-data-accessors-in-columns.jsx create mode 100644 packages/dx-react-demos/src/material-ui/data-accessors/custom-data-accessors-in-columns.test.jsx create mode 100644 packages/dx-react-demos/src/material-ui/data-accessors/custom-data-accessors.jsx create mode 100644 packages/dx-react-demos/src/material-ui/data-accessors/custom-data-accessors.test.jsx create mode 100644 packages/dx-react-grid/docs/guides/data-accessors.md create mode 100644 packages/dx-react-grid/src/plugins/editing-state.test.jsx diff --git a/packages/dx-grid-core/src/plugins/editing-state/computeds.js b/packages/dx-grid-core/src/plugins/editing-state/computeds.js index 5a0f601ce7..69f513bbb7 100644 --- a/packages/dx-grid-core/src/plugins/editing-state/computeds.js +++ b/packages/dx-grid-core/src/plugins/editing-state/computeds.js @@ -16,3 +16,15 @@ export const addedRowsByIds = (addedRows, rowIds) => { }); return result; }; + +export const computedCreateRowChange = (columns) => { + const map = columns.reduce((acc, column) => { + if (column.createRowChange) { + acc[column.name] = column.createRowChange; + } + return acc; + }, {}); + + return (row, columnName, value) => + (map[columnName] ? map[columnName](row, value, columnName) : { [columnName]: value }); +}; diff --git a/packages/dx-grid-core/src/plugins/editing-state/computeds.test.js b/packages/dx-grid-core/src/plugins/editing-state/computeds.test.js index 5dee88f6b3..5cdf91c30e 100644 --- a/packages/dx-grid-core/src/plugins/editing-state/computeds.test.js +++ b/packages/dx-grid-core/src/plugins/editing-state/computeds.test.js @@ -1,6 +1,7 @@ import { changedRowsByIds, addedRowsByIds, + computedCreateRowChange, } from './computeds'; describe('EditingState computeds', () => { @@ -33,4 +34,31 @@ describe('EditingState computeds', () => { ]); }); }); + describe('#computedCreateRowChange', () => { + it('should create a row change', () => { + const rows = [ + { a: 1, b: 1 }, + { a: 2, b: 2 }, + ]; + const columns = [ + { name: 'a' }, + { name: 'b' }, + ]; + const createRowChange = computedCreateRowChange(columns); + const change = createRowChange(rows[1], columns[1].name, 3); + + expect(change).toEqual({ b: 3 }); + }); + + it('should create a row change by using a custom function within column config', () => { + const rows = [{ a: 1 }]; + const createRowChangeMock = jest.fn(); + const columns = [{ name: 'a', createRowChange: createRowChangeMock }]; + + const createRowChange = computedCreateRowChange(columns); + createRowChange(rows[0], columns[0].name, 3); + + expect(createRowChangeMock).toBeCalledWith(rows[0], 3, columns[0].name); + }); + }); }); diff --git a/packages/dx-grid-core/src/plugins/filtering-state/computeds.js b/packages/dx-grid-core/src/plugins/filtering-state/computeds.js index f17d381ad4..1ea4e6871e 100644 --- a/packages/dx-grid-core/src/plugins/filtering-state/computeds.js +++ b/packages/dx-grid-core/src/plugins/filtering-state/computeds.js @@ -1,11 +1,13 @@ const toString = value => String(value).toLowerCase(); -const applyRowFilter = (row, filter) => - toString(row[filter.columnName]).indexOf(toString(filter.value)) > -1; +const applyFilter = (filter, value) => (toString(value).indexOf(toString(filter.value)) > -1); -export const filteredRows = (rows, filters, filterFn = applyRowFilter) => { +export const filteredRows = (rows, filters, getCellData, userFilterFn) => { if (!filters.length) return rows; + const filterFn = userFilterFn || + ((row, filter) => applyFilter(filter, getCellData(row, filter.columnName))); + return rows.filter( row => filters.reduce( (accumulator, filter) => diff --git a/packages/dx-grid-core/src/plugins/filtering-state/computeds.test.js b/packages/dx-grid-core/src/plugins/filtering-state/computeds.test.js index e6e13a60af..5b4de90b50 100644 --- a/packages/dx-grid-core/src/plugins/filtering-state/computeds.test.js +++ b/packages/dx-grid-core/src/plugins/filtering-state/computeds.test.js @@ -5,23 +5,25 @@ import { describe('FilteringState computeds', () => { describe('#filteredRows', () => { const rows = [ - { a: 1, b: 1 }, - { a: 1, b: 2 }, - { a: 2, b: 1 }, - { a: 2, b: 2 }, + { a: 1, b: 1 }, + { a: 1, b: 2 }, + { a: 2, b: 1 }, + { a: 2, b: 2 }, ]; + const getCellData = (row, columnName) => row[columnName]; + it('should not touch rows if no filters specified', () => { const filters = []; - const filtered = filteredRows(rows, filters); + const filtered = filteredRows(rows, filters, getCellData); expect(filtered).toBe(rows); }); it('can filter by one field', () => { const filters = [{ columnName: 'a', value: 1 }]; - const filtered = filteredRows(rows, filters); + const filtered = filteredRows(rows, filters, getCellData); expect(filtered).toEqual([ { a: 1, b: 1 }, { a: 1, b: 2 }, @@ -31,7 +33,7 @@ describe('FilteringState computeds', () => { it('can filter by several fields', () => { const filters = [{ columnName: 'a', value: 1 }, { columnName: 'b', value: 2 }]; - const filtered = filteredRows(rows, filters); + const filtered = filteredRows(rows, filters, getCellData); expect(filtered).toEqual([ { a: 1, b: 2 }, ]); diff --git a/packages/dx-grid-core/src/plugins/grouping-local/computeds.js b/packages/dx-grid-core/src/plugins/grouping-local/computeds.js index 85fab990dd..4f6021cc81 100644 --- a/packages/dx-grid-core/src/plugins/grouping-local/computeds.js +++ b/packages/dx-grid-core/src/plugins/grouping-local/computeds.js @@ -1,11 +1,11 @@ import { GROUP_KEY_SEPARATOR } from '../grouping-state/constants'; -export const groupedRows = (rows, grouping) => { +export const groupedRows = (rows, grouping, getCellData) => { if (!grouping.length) return rows; const groups = rows .reduce((acc, row) => { - const value = row[grouping[0].columnName]; + const value = getCellData(row, grouping[0].columnName); const key = String(value); const sameKeyItems = acc.get(key); if (!sameKeyItems) { @@ -20,7 +20,7 @@ export const groupedRows = (rows, grouping) => { return [...groups.values()] .map(([value, items]) => ({ value, - items: groupedRows(items, nestedGrouping), + items: groupedRows(items, nestedGrouping, getCellData), })); }; diff --git a/packages/dx-grid-core/src/plugins/grouping-local/computeds.test.js b/packages/dx-grid-core/src/plugins/grouping-local/computeds.test.js index c50fc2b443..402f9ede66 100644 --- a/packages/dx-grid-core/src/plugins/grouping-local/computeds.test.js +++ b/packages/dx-grid-core/src/plugins/grouping-local/computeds.test.js @@ -10,6 +10,7 @@ describe('GroupingPlugin computeds', () => { { a: 2, b: 1 }, { a: 2, b: 2 }, ]; + const getCellData = (row, columnName) => row[columnName]; const firstLevelGroupings = [{ columnName: 'a' }]; const firstLevelGroupedRows = [{ @@ -57,12 +58,12 @@ describe('GroupingPlugin computeds', () => { describe('#groupedRows', () => { it('can group by one column', () => { - expect(groupedRows(rowsSource, firstLevelGroupings)) + expect(groupedRows(rowsSource, firstLevelGroupings, getCellData)) .toEqual(firstLevelGroupedRows); }); it('can group by several columns', () => { - expect(groupedRows(rowsSource, secondLevelGroupings)) + expect(groupedRows(rowsSource, secondLevelGroupings, getCellData)) .toEqual(secondLevelGroupedRows); }); }); diff --git a/packages/dx-grid-core/src/plugins/sorting-state/computeds.js b/packages/dx-grid-core/src/plugins/sorting-state/computeds.js index ab50f37a23..0e6f0c7cb4 100644 --- a/packages/dx-grid-core/src/plugins/sorting-state/computeds.js +++ b/packages/dx-grid-core/src/plugins/sorting-state/computeds.js @@ -1,23 +1,25 @@ import mergeSort from '../../utils/merge-sort'; -const createSortingCompare = (sorting, compareEqual) => (a, b) => { - const sortColumn = sorting.columnName; +const createSortingCompare = (sorting, compareEqual, getCellData) => (a, b) => { const inverse = sorting.direction === 'desc'; + const columnName = sorting.columnName; + const aValue = getCellData(a, columnName); + const bValue = getCellData(b, columnName); - if (a[sortColumn] === b[sortColumn]) { + if (aValue === bValue) { return (compareEqual && compareEqual(a, b)) || 0; } - return (a[sortColumn] < b[sortColumn]) ^ inverse ? -1 : 1; // eslint-disable-line no-bitwise + return (aValue < bValue) ^ inverse ? -1 : 1; // eslint-disable-line no-bitwise }; -export const sortedRows = (rows, sorting) => { +export const sortedRows = (rows, sorting, getCellData) => { if (!sorting.length) return rows; const compare = Array.from(sorting) .reverse() .reduce((prevCompare, columnSorting) => - createSortingCompare(columnSorting, prevCompare), () => 0); + createSortingCompare(columnSorting, prevCompare, getCellData), () => 0); return mergeSort(Array.from(rows), compare); }; diff --git a/packages/dx-grid-core/src/plugins/sorting-state/computeds.test.js b/packages/dx-grid-core/src/plugins/sorting-state/computeds.test.js index 7911954dea..5d51f44568 100644 --- a/packages/dx-grid-core/src/plugins/sorting-state/computeds.test.js +++ b/packages/dx-grid-core/src/plugins/sorting-state/computeds.test.js @@ -13,17 +13,19 @@ describe('SortingState computeds', () => { { a: 1, b: 2 }, ]; + const getCellData = (row, columnName) => row[columnName]; + it('does not mutate rows if no sorting specified', () => { const sorting = []; - const sorted = sortedRows(rows, sorting); + const sorted = sortedRows(rows, sorting, getCellData); expect(sorted).toBe(rows); }); it('can sort ascending by one column', () => { const sorting = [{ columnName: 'a', direction: 'asc' }]; - const sorted = sortedRows(rows, sorting); + const sorted = sortedRows(rows, sorting, getCellData); expect(sorted).toEqual([ { a: 1, b: 1 }, { a: 1, b: 2 }, @@ -35,7 +37,7 @@ describe('SortingState computeds', () => { it('can sort descending by one column', () => { const sorting = [{ columnName: 'a', direction: 'desc' }]; - const sorted = sortedRows(rows, sorting); + const sorted = sortedRows(rows, sorting, getCellData); expect(sorted).toEqual([ { a: 2, b: 2 }, { a: 2, b: 1 }, @@ -47,7 +49,7 @@ describe('SortingState computeds', () => { it('can sort by several columns', () => { const sorting = [{ columnName: 'a', direction: 'asc' }, { columnName: 'b', direction: 'asc' }]; - const sorted = sortedRows(rows, sorting); + const sorted = sortedRows(rows, sorting, getCellData); expect(sorted).toEqual([ { a: 1, b: 1 }, { a: 1, b: 2 }, @@ -59,7 +61,7 @@ describe('SortingState computeds', () => { it('can sort by several columns with different directions', () => { const sorting = [{ columnName: 'a', direction: 'asc' }, { columnName: 'b', direction: 'desc' }]; - const sorted = sortedRows(rows, sorting); + const sorted = sortedRows(rows, sorting, getCellData); expect(sorted).toEqual([ { a: 1, b: 2 }, { a: 1, b: 1 }, @@ -72,7 +74,7 @@ describe('SortingState computeds', () => { const immutableRows = Immutable(rows); const immutableSorting = Immutable([{ columnName: 'a', direction: 'desc' }]); - const sorted = sortedRows(immutableRows, immutableSorting); + const sorted = sortedRows(immutableRows, immutableSorting, getCellData); expect(sorted).toEqual([ { a: 2, b: 2 }, { a: 2, b: 1 }, diff --git a/packages/dx-react-demos/src/bootstrap3/data-accessors/custom-data-accessors-in-columns.jsx b/packages/dx-react-demos/src/bootstrap3/data-accessors/custom-data-accessors-in-columns.jsx new file mode 100644 index 0000000000..67f6c11ac7 --- /dev/null +++ b/packages/dx-react-demos/src/bootstrap3/data-accessors/custom-data-accessors-in-columns.jsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { + EditingState, +} from '@devexpress/dx-react-grid'; +import { + Grid, + TableView, + TableHeaderRow, + TableEditRow, + TableEditColumn, +} from '@devexpress/dx-react-grid-bootstrap3'; + +import { + generateRows, + defaultNestedColumnValues, +} from '../../demo-data/generator'; + +export default class Demo extends React.PureComponent { + constructor(props) { + super(props); + + this.state = { + columns: [ + { + name: 'firstName', + title: 'First Name', + getCellData: row => (row.user ? row.user.firstName : undefined), + createRowChange: (row, value) => ({ + user: { + ...row.user, + firstName: value, + }, + }), + }, + { + name: 'lastName', + title: 'Last Name', + getCellData: row => (row.user ? row.user.lastName : undefined), + createRowChange: (row, value) => ({ + user: { + ...row.user, + lastName: value, + }, + }), + }, + { + name: 'car', + title: 'Car', + getCellData: row => (row.car ? row.car.model : undefined), + createRowChange: (row, value) => ({ + car: { + model: value, + }, + }), + }, + { name: 'position', title: 'Position' }, + { name: 'city', title: 'City' }, + ], + rows: generateRows({ + columnValues: { id: ({ index }) => index, ...defaultNestedColumnValues }, + length: 14, + }), + }; + + this.commitChanges = ({ added, changed, deleted }) => { + let rows = this.state.rows; + if (added) { + const startingAddedId = (rows.length - 1) > 0 ? rows[rows.length - 1].id + 1 : 0; + rows = [ + ...rows, + ...added.map((row, index) => ({ + id: startingAddedId + index, + ...row, + })), + ]; + } + if (changed) { + rows = rows.map(row => (changed[row.id] ? { ...row, ...changed[row.id] } : row)); + } + if (deleted) { + const deletedSet = new Set(deleted); + rows = rows.filter(row => !deletedSet.has(row.id)); + } + this.setState({ rows }); + }; + } + render() { + const { rows, columns } = this.state; + + return ( + row.id} + > + + + + + + + ); + } +} diff --git a/packages/dx-react-demos/src/bootstrap3/data-accessors/custom-data-accessors-in-columns.test.jsx b/packages/dx-react-demos/src/bootstrap3/data-accessors/custom-data-accessors-in-columns.test.jsx new file mode 100644 index 0000000000..efb04b9c4c --- /dev/null +++ b/packages/dx-react-demos/src/bootstrap3/data-accessors/custom-data-accessors-in-columns.test.jsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import CustomDataAccessorsInColumns from './custom-data-accessors-in-columns'; + +describe('BS3: custom data accessors in columns demo', () => { + it('should work', () => { + mount( + , + ); + }); +}); diff --git a/packages/dx-react-demos/src/bootstrap3/data-accessors/custom-data-accessors.jsx b/packages/dx-react-demos/src/bootstrap3/data-accessors/custom-data-accessors.jsx new file mode 100644 index 0000000000..2ed7cb7d75 --- /dev/null +++ b/packages/dx-react-demos/src/bootstrap3/data-accessors/custom-data-accessors.jsx @@ -0,0 +1,105 @@ +import React from 'react'; +import { + EditingState, +} from '@devexpress/dx-react-grid'; +import { + Grid, + TableView, + TableHeaderRow, + TableEditRow, + TableEditColumn, +} from '@devexpress/dx-react-grid-bootstrap3'; + +import { + generateRows, + defaultNestedColumnValues, +} from '../../demo-data/generator'; + +export default class Demo extends React.PureComponent { + constructor(props) { + super(props); + + this.state = { + columns: [ + { name: 'user.firstName', title: 'First Name' }, + { name: 'user.lastName', title: 'Last Name' }, + { name: 'car.model', title: 'Car' }, + { name: 'position', title: 'Position' }, + { name: 'city', title: 'City' }, + ], + rows: generateRows({ + columnValues: { id: ({ index }) => index, ...defaultNestedColumnValues }, + length: 14, + }), + }; + + this.commitChanges = ({ added, changed, deleted }) => { + let rows = this.state.rows; + if (added) { + const startingAddedId = (rows.length - 1) > 0 ? rows[rows.length - 1].id + 1 : 0; + rows = [ + ...rows, + ...added.map((row, index) => ({ + id: startingAddedId + index, + ...row, + })), + ]; + } + if (changed) { + rows = rows.map(row => (changed[row.id] ? { ...row, ...changed[row.id] } : row)); + } + if (deleted) { + const deletedSet = new Set(deleted); + rows = rows.filter(row => !deletedSet.has(row.id)); + } + this.setState({ rows }); + }; + this.splitColumnName = (columnName) => { + const parts = columnName.split('.'); + return { rootField: parts[0], nestedField: parts[1] }; + }; + } + render() { + const { rows, columns } = this.state; + + return ( + row.id} + getCellData={(row, columnName) => { + if (columnName.indexOf('.') > -1) { + const { rootField, nestedField } = this.splitColumnName(columnName); + return row[rootField] ? row[rootField][nestedField] : undefined; + } + return row[columnName]; + }} + > + { + if (columnName.indexOf('.') > -1) { + const { rootField, nestedField } = this.splitColumnName(columnName); + + return { + [rootField]: { + ...row[rootField], + [nestedField]: value, + }, + }; + } + return { [columnName]: value }; + }} + /> + + + + + + ); + } +} diff --git a/packages/dx-react-demos/src/bootstrap3/data-accessors/custom-data-accessors.test.jsx b/packages/dx-react-demos/src/bootstrap3/data-accessors/custom-data-accessors.test.jsx new file mode 100644 index 0000000000..a5c86301ea --- /dev/null +++ b/packages/dx-react-demos/src/bootstrap3/data-accessors/custom-data-accessors.test.jsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import CustomDataAccessors from './custom-data-accessors'; + +describe('BS3: custom data accessors demo', () => { + it('should work', () => { + mount( + , + ); + }); +}); diff --git a/packages/dx-react-demos/src/bootstrap3/editing/edit-row-controlled.test.jsx b/packages/dx-react-demos/src/bootstrap3/editing/edit-row-controlled.test.jsx index ebb0356c63..a0d7667f1c 100644 --- a/packages/dx-react-demos/src/bootstrap3/editing/edit-row-controlled.test.jsx +++ b/packages/dx-react-demos/src/bootstrap3/editing/edit-row-controlled.test.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { mount } from 'enzyme'; import EditRowControlledDemo from './edit-row-controlled'; -describe('MUI: edit row controlled demo', () => { +describe('BS3: edit row controlled demo', () => { it('should work', () => { mount( , diff --git a/packages/dx-react-demos/src/bootstrap3/editing/edit-row.test.jsx b/packages/dx-react-demos/src/bootstrap3/editing/edit-row.test.jsx index f8df243158..67616fc88e 100644 --- a/packages/dx-react-demos/src/bootstrap3/editing/edit-row.test.jsx +++ b/packages/dx-react-demos/src/bootstrap3/editing/edit-row.test.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { mount } from 'enzyme'; import EditRowDemo from './edit-row'; -describe('MUI: edit row controlled demo', () => { +describe('BS3: edit row controlled demo', () => { it('should work', () => { mount( , diff --git a/packages/dx-react-demos/src/demo-data/generator.js b/packages/dx-react-demos/src/demo-data/generator.js index aa710dfbfb..3e92c848d4 100644 --- a/packages/dx-react-demos/src/demo-data/generator.js +++ b/packages/dx-react-demos/src/demo-data/generator.js @@ -2,6 +2,10 @@ import randomSeed from './random'; const femaleFirstNames = ['Mary', 'Linda', 'Barbara', 'Maria', 'Lisa', 'Nancy', 'Betty', 'Sandra', 'Sharon']; const maleFirstNames = ['James', 'John', 'Robert', 'William', 'David', 'Richard', 'Thomas', 'Paul', 'Mark']; +const lastNames = [ + 'Smith', 'Johnson', 'Williams', 'Jones', 'Brown', 'Davis', 'Johnson', 'Miller', 'Wilson', 'Moore', 'Taylor', 'Anderson', + 'Thomas', 'Jackson', 'Williams', 'White', 'Harris', 'Davis', 'Martin', 'Thompson', 'Garcia', 'Martinez', 'Robinson', 'Clark', +]; const usStates = [ { name: 'Alabama', abbr: 'AL' }, { name: 'Alaska', abbr: 'AK' }, @@ -63,6 +67,9 @@ const usStates = [ { name: 'Wisconsin', abbr: 'WI' }, { name: 'Wyoming', abbr: 'WY' }, ]; +const cities = ['New York', 'Los Angeles', 'Chicago', 'Las Vegas', 'Austin', 'Tokyo', 'Rio de Janeiro', 'London', 'Paris']; +const cars = ['Honda Civic', 'Toyota Corolla', 'Chevrolet Cruze', 'Honda Accord', 'Nissan Altima', 'Kia Optima', 'Audi A4', 'BMW 750']; +const positions = ['CEO', 'IT Manager', 'Ombudsman', 'CMO', 'Controller', 'HR Manager', 'Shipping Manager', 'Sales Assistant', 'HR Assistant']; const generateDate = ({ random, @@ -84,8 +91,20 @@ export const defaultColumnValues = { Female: femaleFirstNames, }, ], - city: ['New York', 'Los Angeles', 'Chicago', 'Las Vegas', 'Austin', 'Tokyo', 'Rio de Janeiro', 'London', 'Paris'], - car: ['Honda Civic', 'Toyota Corolla', 'Chevrolet Cruze', 'Honda Accord', 'Nissan Altima', 'Kia Optima', 'Audi A4', 'BMW 750'], + city: cities, + car: cars, +}; + +export const defaultNestedColumnValues = { + user: [ + ...[...maleFirstNames, ...femaleFirstNames].map((name, i) => ({ + firstName: name, + lastName: lastNames[i], + })), + ], + position: positions, + city: cities, + car: cars.map(car => ({ model: car })), }; export const globalSalesValues = { @@ -123,11 +142,8 @@ export const employeeValues = { Female: femaleFirstNames, }, ], - lastName: [ - 'Smith', 'Johnson', 'Williams', 'Jones', 'Brown', 'Davis', 'Johnson', 'Miller', 'Wilson', 'Moore', 'Taylor', 'Anderson', - 'Thomas', 'Jackson', 'Williams', 'White', 'Harris', 'Davis', 'Martin', 'Thompson', 'Garcia', 'Martinez', 'Robinson', 'Clark', - ], - position: ['CEO', 'IT Manager', 'Ombudsman', 'CMO', 'Controller', 'HR Manager', 'Shipping Manager', 'Sales Assistant', 'HR Assistant'], + lastName: lastNames, + position: positions, state: usStates.map(state => state.name), birthDate: ({ random }) => generateDate({ random, @@ -205,7 +221,12 @@ export function generateRows({ values = values[1][record[values[0]]]; } - record[column] = values[Math.floor(random() * values.length)]; + const value = values[Math.floor(random() * values.length)]; + if (typeof value === 'object') { + record[column] = Object.assign({}, value); + } else { + record[column] = value; + } }); data.push(record); diff --git a/packages/dx-react-demos/src/demo-registry.js b/packages/dx-react-demos/src/demo-registry.js index d41223db48..46a0d91bdb 100644 --- a/packages/dx-react-demos/src/demo-registry.js +++ b/packages/dx-react-demos/src/demo-registry.js @@ -17,6 +17,16 @@ export const demos = { 'material-ui': require('./material-ui/basic/basic').default, }, }, + 'data-accessors': { + 'custom-data-accessors': { + bootstrap3: require('./bootstrap3/data-accessors/custom-data-accessors').default, + 'material-ui': require('./material-ui/data-accessors/custom-data-accessors').default, + }, + 'custom-data-accessors-in-columns': { + bootstrap3: require('./bootstrap3/data-accessors/custom-data-accessors-in-columns').default, + 'material-ui': require('./material-ui/data-accessors/custom-data-accessors-in-columns').default, + }, + }, 'column-reordering': { uncontrolled: { bootstrap3: require('./bootstrap3/column-reordering/uncontrolled').default, diff --git a/packages/dx-react-demos/src/material-ui/data-accessors/custom-data-accessors-in-columns.jsx b/packages/dx-react-demos/src/material-ui/data-accessors/custom-data-accessors-in-columns.jsx new file mode 100644 index 0000000000..5c67db66ec --- /dev/null +++ b/packages/dx-react-demos/src/material-ui/data-accessors/custom-data-accessors-in-columns.jsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { + EditingState, +} from '@devexpress/dx-react-grid'; +import { + Grid, + TableView, + TableHeaderRow, + TableEditRow, + TableEditColumn, +} from '@devexpress/dx-react-grid-material-ui'; + +import { + generateRows, + defaultNestedColumnValues, +} from '../../demo-data/generator'; + +export default class Demo extends React.PureComponent { + constructor(props) { + super(props); + + this.state = { + columns: [ + { + name: 'firstName', + title: 'First Name', + getCellData: row => (row.user ? row.user.firstName : undefined), + createRowChange: (row, value) => ({ + user: { + ...row.user, + firstName: value, + }, + }), + }, + { + name: 'lastName', + title: 'Last Name', + getCellData: row => (row.user ? row.user.lastName : undefined), + createRowChange: (row, value) => ({ + user: { + ...row.user, + lastName: value, + }, + }), + }, + { + name: 'car', + title: 'Car', + getCellData: row => (row.car ? row.car.model : undefined), + createRowChange: (row, value) => ({ + car: { + model: value, + }, + }), + }, + { name: 'position', title: 'Position' }, + { name: 'city', title: 'City' }, + ], + rows: generateRows({ + columnValues: { id: ({ index }) => index, ...defaultNestedColumnValues }, + length: 14, + }), + }; + + this.commitChanges = ({ added, changed, deleted }) => { + let rows = this.state.rows; + if (added) { + const startingAddedId = (rows.length - 1) > 0 ? rows[rows.length - 1].id + 1 : 0; + rows = [ + ...rows, + ...added.map((row, index) => ({ + id: startingAddedId + index, + ...row, + })), + ]; + } + if (changed) { + rows = rows.map(row => (changed[row.id] ? { ...row, ...changed[row.id] } : row)); + } + if (deleted) { + const deletedSet = new Set(deleted); + rows = rows.filter(row => !deletedSet.has(row.id)); + } + this.setState({ rows }); + }; + } + render() { + const { rows, columns } = this.state; + + return ( + row.id} + > + + + + + + + ); + } +} diff --git a/packages/dx-react-demos/src/material-ui/data-accessors/custom-data-accessors-in-columns.test.jsx b/packages/dx-react-demos/src/material-ui/data-accessors/custom-data-accessors-in-columns.test.jsx new file mode 100644 index 0000000000..fa8ad8fd1c --- /dev/null +++ b/packages/dx-react-demos/src/material-ui/data-accessors/custom-data-accessors-in-columns.test.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { MuiThemeProvider, createMuiTheme } from 'material-ui/styles'; +import CustomDataAccessorsInColumns from './custom-data-accessors-in-columns'; + +describe('MUI: custom data accessors in columns demo', () => { + it('should work', () => { + mount( + + + , + ); + }); +}); diff --git a/packages/dx-react-demos/src/material-ui/data-accessors/custom-data-accessors.jsx b/packages/dx-react-demos/src/material-ui/data-accessors/custom-data-accessors.jsx new file mode 100644 index 0000000000..6575a400cf --- /dev/null +++ b/packages/dx-react-demos/src/material-ui/data-accessors/custom-data-accessors.jsx @@ -0,0 +1,105 @@ +import React from 'react'; +import { + EditingState, +} from '@devexpress/dx-react-grid'; +import { + Grid, + TableView, + TableHeaderRow, + TableEditRow, + TableEditColumn, +} from '@devexpress/dx-react-grid-material-ui'; + +import { + generateRows, + defaultNestedColumnValues, +} from '../../demo-data/generator'; + +export default class Demo extends React.PureComponent { + constructor(props) { + super(props); + + this.state = { + columns: [ + { name: 'user.firstName', title: 'First Name' }, + { name: 'user.lastName', title: 'Last Name' }, + { name: 'car.model', title: 'Car' }, + { name: 'position', title: 'Position' }, + { name: 'city', title: 'City' }, + ], + rows: generateRows({ + columnValues: { id: ({ index }) => index, ...defaultNestedColumnValues }, + length: 14, + }), + }; + + this.commitChanges = ({ added, changed, deleted }) => { + let rows = this.state.rows; + if (added) { + const startingAddedId = (rows.length - 1) > 0 ? rows[rows.length - 1].id + 1 : 0; + rows = [ + ...rows, + ...added.map((row, index) => ({ + id: startingAddedId + index, + ...row, + })), + ]; + } + if (changed) { + rows = rows.map(row => (changed[row.id] ? { ...row, ...changed[row.id] } : row)); + } + if (deleted) { + const deletedSet = new Set(deleted); + rows = rows.filter(row => !deletedSet.has(row.id)); + } + this.setState({ rows }); + }; + this.splitColumnName = (columnName) => { + const parts = columnName.split('.'); + return { rootField: parts[0], nestedField: parts[1] }; + }; + } + render() { + const { rows, columns } = this.state; + + return ( + row.id} + getCellData={(row, columnName) => { + if (columnName.indexOf('.') > -1) { + const { rootField, nestedField } = this.splitColumnName(columnName); + return row[rootField] ? row[rootField][nestedField] : undefined; + } + return row[columnName]; + }} + > + { + if (columnName.indexOf('.') > -1) { + const { rootField, nestedField } = this.splitColumnName(columnName); + + return { + [rootField]: { + ...row[rootField], + [nestedField]: value, + }, + }; + } + return { [columnName]: value }; + }} + /> + + + + + + ); + } +} diff --git a/packages/dx-react-demos/src/material-ui/data-accessors/custom-data-accessors.test.jsx b/packages/dx-react-demos/src/material-ui/data-accessors/custom-data-accessors.test.jsx new file mode 100644 index 0000000000..bf5c57fbad --- /dev/null +++ b/packages/dx-react-demos/src/material-ui/data-accessors/custom-data-accessors.test.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { MuiThemeProvider, createMuiTheme } from 'material-ui/styles'; +import CustomDataAccessors from './custom-data-accessors'; + +describe('MUI: custom data accessors demo', () => { + it('should work', () => { + mount( + + + , + ); + }); +}); diff --git a/packages/dx-react-grid-bootstrap3/src/templates/table-cell.jsx b/packages/dx-react-grid-bootstrap3/src/templates/table-cell.jsx index c0240d8785..65478dcde2 100644 --- a/packages/dx-react-grid-bootstrap3/src/templates/table-cell.jsx +++ b/packages/dx-react-grid-bootstrap3/src/templates/table-cell.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -export const TableCell = ({ style, row, column }) => ( +export const TableCell = ({ style, column, value }) => ( ( ...style, }} > - {row[column.name]} + {value} ); TableCell.propTypes = { style: PropTypes.shape(), - row: PropTypes.shape(), + value: PropTypes.any, column: PropTypes.shape(), }; TableCell.defaultProps = { style: null, - row: {}, + value: undefined, column: {}, }; diff --git a/packages/dx-react-grid-bootstrap3/src/templates/table-cell.test.jsx b/packages/dx-react-grid-bootstrap3/src/templates/table-cell.test.jsx index 8fd042ae30..11bbd34768 100644 --- a/packages/dx-react-grid-bootstrap3/src/templates/table-cell.test.jsx +++ b/packages/dx-react-grid-bootstrap3/src/templates/table-cell.test.jsx @@ -4,6 +4,15 @@ import { setupConsole } from '@devexpress/dx-testing'; import { TableCell } from './table-cell'; describe('TableCell', () => { + const mountTableCell = column => ( + mount( + , + ) + ); + let resetConsole; beforeAll(() => { resetConsole = setupConsole({ ignore: ['validateDOMNesting'] }); @@ -14,25 +23,18 @@ describe('TableCell', () => { }); it('should have correct text alignment', () => { - let tree = mount( - , - ); + let tree = mountTableCell({}); expect(tree.find('td').prop('style').textAlign).toBe('left'); - tree = mount( - , - ); + tree = mountTableCell({ align: 'left' }); expect(tree.find('td').prop('style').textAlign).toBe('left'); - tree = mount( - , - ); + tree = mountTableCell({ align: 'right' }); expect(tree.find('td').prop('style').textAlign).toBe('right'); }); + + it('should have correct text', () => { + const tree = mountTableCell({}); + expect(tree.find('td').text()).toBe('text'); + }); }); diff --git a/packages/dx-react-grid-material-ui/src/templates/table-cell.jsx b/packages/dx-react-grid-material-ui/src/templates/table-cell.jsx index f709332a78..941399e472 100644 --- a/packages/dx-react-grid-material-ui/src/templates/table-cell.jsx +++ b/packages/dx-react-grid-material-ui/src/templates/table-cell.jsx @@ -20,7 +20,7 @@ export const styleSheet = createStyleSheet('TableCell', theme => ({ }, })); -const TableCellBase = ({ style, row, column, classes }) => ( +const TableCellBase = ({ style, column, value, classes }) => ( ( [classes.cellRightAlign]: column.align === 'right', })} > - {row[column.name]} + {value} ); TableCellBase.propTypes = { style: PropTypes.shape(), - row: PropTypes.shape(), + value: PropTypes.any, column: PropTypes.shape(), classes: PropTypes.object.isRequired, }; TableCellBase.defaultProps = { style: null, - row: {}, + value: undefined, column: {}, }; diff --git a/packages/dx-react-grid-material-ui/src/templates/table-cell.test.jsx b/packages/dx-react-grid-material-ui/src/templates/table-cell.test.jsx index 46e7795300..359961d3bf 100644 --- a/packages/dx-react-grid-material-ui/src/templates/table-cell.test.jsx +++ b/packages/dx-react-grid-material-ui/src/templates/table-cell.test.jsx @@ -8,6 +8,14 @@ describe('TableCell', () => { let resetConsole; let mount; let classes; + const mountTableCell = column => ( + mount( + , + ) + ); beforeAll(() => { resetConsole = setupConsole({ ignore: ['validateDOMNesting'] }); mount = createMount(); @@ -19,25 +27,18 @@ describe('TableCell', () => { }); it('should have correct text alignment', () => { - let tree = mount( - , - ); + let tree = mountTableCell({}); expect(tree.find(TableCellMUI).hasClass(classes.cellRightAlign)).toBeFalsy(); - tree = mount( - , - ); + tree = mountTableCell({ align: 'left' }); expect(tree.find(TableCellMUI).hasClass(classes.cellRightAlign)).toBeFalsy(); - tree = mount( - , - ); + tree = mountTableCell({ align: 'right' }); expect(tree.find(TableCellMUI).hasClass(classes.cellRightAlign)).toBeTruthy(); }); + + it('should have correct text', () => { + const tree = mountTableCell({}); + expect(tree.find(TableCellMUI).text()).toBe('text'); + }); }); diff --git a/packages/dx-react-grid/docs/guides/data-accessors.md b/packages/dx-react-grid/docs/guides/data-accessors.md new file mode 100644 index 0000000000..5272d3ef7d --- /dev/null +++ b/packages/dx-react-grid/docs/guides/data-accessors.md @@ -0,0 +1,114 @@ +# Data Accessors + +## Cell Data Access + +In a common scenario with a simple data structure, you can associate a column with a row field using the column's `name` field as shown in the following example: + +```js +const rows = [ + { firstName: 'John', lastName: 'Smith' }, + ... +]; +const columns = [ + { name: 'firstName', title: 'First Name' }, + { name: 'lastName', title: 'Last Name' }, + ... +]; + +``` + +In the case of nested data structure, use the `getCellData` function to calculate a column value as demonstrated below: + +```js +const rows = [ + { user: { firstName: 'John', lastName: 'Smith' } }, + ... +]; + (row.user ? row.user.firstName : undefined), + }, + { + name: 'lastName', + title: 'Last Name', + getCellData: row => (row.user ? row.user.lastName : undefined), + }, + ... + ], +/> +``` + +.embedded-demo(data-accessors/custom-data-accessors-in-columns) + +If you use a common data calculation algorithm for all columns, specify the `getCellData` function on the Grid's level. + +For example, you can implement dot notation support for columns like `{ name: 'user.firstName' }`. In this case, the function code looks as follows: + +```js + { + if (columnName.indexOf('.') > -1) { + const { rootField, nestedField } = this.splitColumnName(columnName); + return row[rootField] ? row[rootField][nestedField] : undefined; + } + return row[columnName]; + }} +> +``` + +The following demo shows this approach in action: + +.embedded-demo(data-accessors/custom-data-accessors) + +Note that the Grid's `getCellData` property has a higher priority than the column's property. + +The `getCellData` implementation presented in this demo is not optimized for frequent invocation. Avoid using it in production apps operating with large amounts of data. + +## Cell Data Editing + +If editing features are enabled, you can use the column's `createRowChange` function to create a row changes object: + +```js +const rows = [ + { user: { firstName: 'John', lastName: 'Smith' } }, + ... +]; + +const columns: [ + { + ... + createRowChange: (row, value) => ({ + user: { + ...row.user, + firstName: value, + }, + }), + }, + ... +] +``` + +Specify the `EditingState` plugin's `createRowChange` property if you use a common algorithm for all columns. + +```js + + { + // ... + }} + /> +/> +``` + +Note that the `EditingState` plugin's `createRowChange` property has a higher priority than the column's property. diff --git a/packages/dx-react-grid/docs/reference/editing-state.md b/packages/dx-react-grid/docs/reference/editing-state.md index dbfa4c0fba..e8d2302ecb 100644 --- a/packages/dx-react-grid/docs/reference/editing-state.md +++ b/packages/dx-react-grid/docs/reference/editing-state.md @@ -1,6 +1,6 @@ # EditingState Plugin Reference -A plugin that manages the editing state of grid rows. It arranges grid rows by different lists depending on a row's state. +A plugin that manages grid rows' editing state. It arranges grid rows by different lists depending on a row's state. ## User Reference @@ -25,6 +25,7 @@ deletedRows | Array<number | string> | | Specifies IDs of the rows pr defaultDeletedRows | Array<number | string> | | Specifies rows initially added to the `deletedRows` array in the uncontrolled mode onDeletedRowsChange | (deletedRows: Array<number | string>) => void | | Handles adding or removing a row from the `deletedRows` array onCommitChanges | (Array<[ChangeSet](#change-set)>) => void | | Handles row changes committing +createRowChange | (row: [Row](grid.md#row), columnName: string, value: string | number) => object | | Specifies the function used to create a row change ## Interfaces @@ -44,7 +45,9 @@ deleted? | Array<number | string> | An array of IDs representing rows ### Imports -none +Name | Plugin | Type | Description +-----|--------|------|------------ +columns | Getter | Array<[Column](grid.md#column)> | The grid columns ### Exports diff --git a/packages/dx-react-grid/docs/reference/grid.md b/packages/dx-react-grid/docs/reference/grid.md index 57555ca5d9..21d918ae8c 100644 --- a/packages/dx-react-grid/docs/reference/grid.md +++ b/packages/dx-react-grid/docs/reference/grid.md @@ -13,6 +13,7 @@ Name | Type | Default | Description rows | Array<[Row](#row)> | | Specifies rows with data to be rendered columns | Array<[Column](#column)> | | Specifies row fields to be rendered as columns getRowId | (row: [Row](#row)) => number | string | null | Specifies the function used to get a unique row identifier +getCellData | (row: [Row](#row), columnName: string) => any | null | Specifies the function used to get a cell data rootTemplate | (args: [RootArgs](#root-args)) => ReactElement | | Renders a root layout using the specified parameters headerPlaceholderTemplate | (args: [HeaderPlaceholderArgs](#header-placeholder-args)) => ReactElement | null | Renders a heading placeholder using the specified parameters footerPlaceholderTemplate | (args: [FooterPlaceholderArgs](#footer-placeholder-args)) => ReactElement | null | Renders a footer placeholder using the specified parameters @@ -34,6 +35,8 @@ A value with the following shape: Field | Type | Description ------|------|------------ name | string | Specifies the field name in the data row to obtain a column value. A unique key can also be used to identify a column +getCellData | (row: [Row](#row), columnName: string) => any | Specifies the function used to get a cell data +createRowChange | (row: [Row](#row), value: string | number, columnName: string) => object | Specifies the function used to create a row change ### RootArgs diff --git a/packages/dx-react-grid/docs/reference/local-filtering.md b/packages/dx-react-grid/docs/reference/local-filtering.md index 47cbfdb9c7..c6d02ef5a1 100644 --- a/packages/dx-react-grid/docs/reference/local-filtering.md +++ b/packages/dx-react-grid/docs/reference/local-filtering.md @@ -22,6 +22,7 @@ Name | Plugin | Type | Description -----|--------|------|------------ rows | Getter | Array<[Row](grid.md#row)> | Rows to be filtered filters | Getter | Array<[Filter](filtering-state.md#filter)> | Column filters to be applied +getCellData | Getter | (row: [Row](grid.md#row), columnName: string) => any | The function used to get cell data ### Exports diff --git a/packages/dx-react-grid/docs/reference/local-grouping.md b/packages/dx-react-grid/docs/reference/local-grouping.md index ce93217923..c24d2d78c7 100644 --- a/packages/dx-react-grid/docs/reference/local-grouping.md +++ b/packages/dx-react-grid/docs/reference/local-grouping.md @@ -21,6 +21,7 @@ Name | Plugin | Type | Description rows | Getter | Array<[Row](grid.md#row)> | Rows to be grouped grouping | Getter | Array<[Grouping](grouping-state.md#grouping)> | The current grouping state expandedGroups | Getter | Set<[GroupKey](grouping-state.md#group-key)> | Groups to be expanded +getCellData | Getter | (row: [Row](grid.md#row), columnName: string) => any | The function used to get cell data ### Exports diff --git a/packages/dx-react-grid/docs/reference/local-sorting.md b/packages/dx-react-grid/docs/reference/local-sorting.md index fbdb90f62e..15acaf3360 100644 --- a/packages/dx-react-grid/docs/reference/local-sorting.md +++ b/packages/dx-react-grid/docs/reference/local-sorting.md @@ -20,6 +20,7 @@ Name | Plugin | Type | Description -----|--------|------|------------ rows | Getter | Array<[Row](grid.md#row)> | Rows to be sorted sorting | Getter | Array<[Sorting](sorting-state.md#sorting)> | Column sorting to be applied +getCellData | Getter | (row: [Row](grid.md#row), columnName: string) => any | The function used to get a cell data ### Exports diff --git a/packages/dx-react-grid/docs/reference/table-edit-row.md b/packages/dx-react-grid/docs/reference/table-edit-row.md index 9c1b4fbfb1..e4f07d129f 100644 --- a/packages/dx-react-grid/docs/reference/table-edit-row.md +++ b/packages/dx-react-grid/docs/reference/table-edit-row.md @@ -41,6 +41,8 @@ tableBodyRows | Getter | Array<[TableRow](table-view.md#table-row)> | Rows editingRows | Getter | Array<number | string> | IDs of the rows being edited addedRows | Getter | Array<Object> | The created rows changedRows | Getter | { [key: string]: Object } | Uncommitted changed rows +getCellData | Getter | (row: [Row](grid.md#row), columnName: string) => any | The function used to get a cell data +createRowChange | Getter | (row: [Row](grid.md#row), columnName: string, value: string | string) => Object | The function used to set a cell data changeRow | Action | ({ rowId: number | string, change: Object }) => void | Applies a change to an existing row changeAddedRow | Action | ({ rowId: number, change: Object }) => void | Applies a change to a new row. Note: `rowId` is a row index within the `addedRows` array tableViewCell | Template | [TableCellArgs](table-view.md#table-cell-args) | A template that renders a table cell diff --git a/packages/dx-react-grid/docs/reference/table-view.md b/packages/dx-react-grid/docs/reference/table-view.md index 6b2c5ee045..604ef6d963 100644 --- a/packages/dx-react-grid/docs/reference/table-view.md +++ b/packages/dx-react-grid/docs/reference/table-view.md @@ -51,7 +51,7 @@ Field | Type | Description ------|------|------------ key | string | A unique identifier of the table row. type | string | Specifies the table row type. The specified value affects which cell template is used to render the row. -rowId? | number | string | Specifies the ID of the associated user data row. +rowId? | number | string | Specifies the associated user data row's ID. row? | [Row](grid.md#row) | Specifies the associated user data row. height? | number | Specifies the table row height. @@ -78,6 +78,7 @@ Field | Type | Description ------|------|------------ tableRow | [TableRow](#table-row) | Specifies a table row tableColumn | [TableColumn](#table-column) | Specifies a table column +value | any | The value to be rendered within a cell style? | Object | Styles that should be applied to the root cell element colSpan? | number | Specifies the number of columns the cell spans @@ -101,6 +102,7 @@ Name | Plugin | Type | Description rows | Getter | Array<[Row](grid.md#row)> | Rows to be rendered by the table view columns | Getter | Array<[Column](#column)> | Columns to be rendered by the table view getRowId | Getter | (row: [Row](grid.md#row)) => number | string | The function used to get a unique row identifier +getCellData | Getter | (row: [Row](grid.md#row), columnName: string) => any | The function used to get a cell data ### Exports diff --git a/packages/dx-react-grid/src/grid.jsx b/packages/dx-react-grid/src/grid.jsx index 47a4569d55..fce98c9d8b 100644 --- a/packages/dx-react-grid/src/grid.jsx +++ b/packages/dx-react-grid/src/grid.jsx @@ -12,22 +12,40 @@ const rowIdGetter = (getRowId, rows) => { }; }; +const getCellDataGetter = (columns) => { + let useFastAccessor = true; + + const map = columns.reduce((acc, column) => { + if (column.getCellData) { + useFastAccessor = false; + acc[column.name] = column.getCellData; + } + return acc; + }, {}); + + return useFastAccessor ? + (row, columnName) => row[columnName] : + (row, columnName) => (map[columnName] ? map[columnName](row, columnName) : row[columnName]); +}; + export class Grid extends React.PureComponent { constructor(props) { super(props); this.memoizedRowIdGetter = memoize(rowIdGetter); + this.memoizedGetCellDataGetter = memoize(getCellDataGetter); } render() { const { rows, getRowId, columns, rootTemplate, headerPlaceholderTemplate, footerPlaceholderTemplate, - children, + children, getCellData, } = this.props; return ( +