Custom Sorting, Grouping, Filtering and More in DevExtreme Grid for React

Oliver's Blog
25 October 2017

We have received a lot of valuable feedback from users of our React Grid alpha versions. Many thanks to everybody! Some of the feedback has already been used as a basis for enhancements to the component. I should mention that we tend not to extend the Grid as a “black box” with lots of built-in features. Usually, we just provide customization capabilities to implement new features on top of our React Grid. Mostly we make API improvements that allow a developer to customize grid behavior and appearance by replacing some of its core algorithms or UI templates. Here is an overview of some recent changes.

Custom Sorting, Grouping, Filtering and More

Custom data accessors

Document-based NoSQL databases are getting more and more popular, which means the data from the server is not flat and tabular, but has complex structures involving nested objects and arrays. It is inconvenient to flatten the data every time it’s obtained from the server, before passing it to the Grid, and a flattening step makes it more complicated to send modified data back to the server. To display nested or computed data, you can now specify a getCellValue function in a column configuration object:

const rows = [
  { user: { firstName: 'John', lastName: 'Smith' } }
  /* ... */
];
const columns = [
  {
    name: 'firstName',
    title: 'First Name',
    getCellValue: row => (row.user ? row.user.firstName : undefined)
  },
  {
    name: 'lastName',
    title: 'Last Name',
    getCellValue: row => (row.user ? row.user.lastName : undefined)
  },
  /* ... */
];

/* ... */

<Grid
  rows={rows}
  columns={columns}
/>

If your data grid is editable, you also need a way to pass cell changes back to the row object. We introduced a createRowChange function, which accepts the whole row and the new cell value as parameters, and should return a “row change” object that reflects changes made to the row. Note that the original row should be regarded immutable, so we create and return a new object with the modified fields. This change is merged into the changedRows state via the EditingState plugin.

const rows = [
  { user: { firstName: 'John', lastName: 'Smith' } }
  /* ... */
];
const columns = [
  {
    name: 'firstName',
    title: 'First Name',
    getCellValue: row => (row.user ? row.user.firstName : undefined),
    createRowChange: (row, value) => ({
      user: {
        ...row.user,
        firstName: value,
      },
    }),
  },  
  /* ... */
];

Please refer to the data accessors guide for demos and more details.

Formatters and editors for custom data types

Now you can define any custom data type and specify how a value of this type should be displayed and edited. This capability is provided by the DataTypeProvider plugin. All you need to do is to add a dataType to the column configuration object and set up formatterTemplate and editorTemplate properties for the DataTypeProvider, which will be used to show or edit the column value:

const rows = [
  { product: 'SolarOne', price: '3039' }
];
const columns = [
  { name: 'product', title: 'Product' },
  { name: 'amount', title: 'Sale Amount', dataType: 'currency' }
];

<Grid
  rows={rows}
  columns={columns}
>
  <DataTypeProvider
    type="currency"
    formatterTemplate={({ value }) => <span>${value}</span>}
    editorTemplate={({ value, onValueChange }) => (
      <span>
        $<input
          type="number"
          value={value}
          onChange={e => onValueChange(Number(e.target.value))}
        />
      </span>
    )}   
  />
  <TableView/>
</Grid>

Please refer to the data types guide for demos and more details.

Custom filtering predicates

The default React Grid filtering predicate uses a case-insensitive “contains” algorithm. Now you can specify custom filtering predicates using the getColumnPredicate property on the LocalFiltering plugin. If the getColumnPredicate function returns undefined for a column, the default filtering predicate is used.

const startsWithPredicate = (value, filter) => 
  String(value).startsWith(String(filter.value));
const getColumnPredicate = columnName => {
  if (columnName === 'city') {
    return startsWithPredicate;
  }
};

<Grid
  rows={rows}
  columns={columns}
>
  <FilteringState />
  <LocalFiltering getColumnPredicate={getColumnPredicate} />
  <TableView />
  <TableHeaderRow />
  <TableFilterRow />
</Grid>

Please refer to the filtering guide for demos and more details.

Custom sorting compare functions

In some scenarios, custom sorting algorithms are required. For instance, you might have an enumeration value represented by its ID in your database. Imagine a ‘priority’ column with valid values “Low”, “Normal” and “High”. You would want this column to be sorted by the underlying numeric values, not alphabetically. The getColumnCompare property of the LocalSorting plugin allows you to implement this scenario.

Note that the comparison function is expected to implement a three-way comparison. For brevity, the following example uses a simple method of calculating a result for numeric values.

const priorityWeights = {
  Low: 0,
  Normal: 1,
  High: 2
};

const comparePriority = (valA, valB) => 
  priorityWeights[valB] - priorityWeights[valA];

const getColumnCompare = columnName => {
  if (columnName === 'priority') {
    return comparePriority;
  }
}

<Grid
  rows={rows}
  columns={columns}
>
  <SortingState />
  <LocalSorting
    getColumnCompare={getColumnCompare}
  />
  <TableView />
  <TableHeaderRow allowSorting />
</Grid>

If the getColumnCompare function returns undefined, it applies the default sorting algorithm.

Please refer to the sorting guide for demos and more details.

Custom grouping values

Data grouping is a powerful feature that helps visualize and analyze large numbers of rows. Usually, a particular row is classified to belong to a specific group by its exact column value. For instance, two rows can belong to one group if they have equal values for the ‘city’ column. Sometimes, your application requires data grouping to use more complex algorithms for row classification.

One common scenario is to group items by only the first character of a string property. For instance, you might need to group people by the first characters of their last names. Another use case is to group orders by year, month, day, or other custom intervals. Our Grid provides the capability to perform these kinds of custom groupings by implementing the getColumnIdentity property of the LocalGrouping plugin:

const byFirstLetter = value => ({
  key: value.substr(0, 1)
});
const getColumnIdentity = (columnName) => {
  if (columnName === 'city') {
    return byFirstLetter;
  }
};

<Grid
  rows={rows}
  columns={columns}
>
  <GroupingState />
  <LocalGrouping getColumnIdentity={getColumnIdentity} />
  <TableView />
  <TableHeaderRow />
  <TableGroupRow />
</Grid>

Please refer to the grouping guide for demos and more details.

Remote and custom local grouping

In some cases, your data might be grouped already. For instance, the data may have been obtained from a server that supports grouping itself. You would like to pass this data to the Grid “as is”, while retaining the standard grouping UI with its interactive group expanding/collapsing features. The new CustomGrouping plugin has been introduced for this purpose. You need to configure it to describe your data structure and then pass the grouped data to the grid.

const columns = columns: [
  { name: 'name', title: 'Name' },
  { name: 'sex', title: 'Sex' },
];

const groupedData = [{
  key: 'Male',
  items: [
    { id: 1, name: 'Paul', sex: 'Male' },
    { id: 2, name: 'John', sex: 'Male' },
  ],
}, {
  key: 'Female',
  items: [
    { id: 3, name: 'Jane', sex: 'Female' },
    { id: 4, name: 'Kate', sex: 'Female' },
  ],
}];

const getChildGroups = groups => groups
  .map(group => ({ key: group.key, childRows: group.items }));

const grouping = [{ columnName: 'sex' }];

<Grid rows={groupedData} columns={columns}>
  <GroupingState grouping={grouping} />
  <CustomGrouping getChildGroups={getChildGroups} />
  <TableView />
  <TableHeaderRow />
  <TableGroupRow />
</Grid>

Please refer to the custom grouping and remote grouping guides for demos and more details.

UI customization via templates

To render individual UI elements, our React Grid uses templates that are passed via props to the Grid’s UI plugins. This happens automatically and internally, so you don’t normally see these templates specified anywhere. A template is a function that returns a React Element depending on the arguments it receives. If you need to modify the default appearance or behavior you can replace default templates with custom ones using template properties on the various plugins.

const customRowTemplate = ({ children, row }) => (
  <tr onClick={() => alert(row.id)}>
    {children}
  </tr>
);

<Grid
  rows={rows}
  columns={columns}
>
  <TableView tableRowTemplate={customRowTemplate} />
  <TableHeaderRow />
</Grid>

Please refer to the appearance customization guide for demos and more details. This approach is applicable to both the Bootstrap React Grid and the React Grid for Material Design.

Feel free to download your copy from npm and let us know what you think.

Free DevExpress Products - Get Your Copy Today

The following free DevExpress product offers remain available. Should you have any questions about the free offers below, please submit a ticket via the DevExpress Support Center at your convenience. We'll be happy to follow-up.
No Comments

Please login or register to post comments.