Tamarac Reporting datagrid
javascript
yarn add @tamarac/datagrid
`
`javascript
import React, from 'react';
import React, { Component } from "react";
import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import { Provider } from 'react-redux';
import { DataGrid, DataGridReducers } from '@tamarac/datagrid';
const store = createStore(
DataGridReducers,
compose(
applyMiddleware(thunk),
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
)
);
class Accounts extends Component {
constructor(props) {
super(props);
this.columnDataFetch = this.columnDataFetch.bind(this);
this.externalFetch = this.externalFetch.bind(this);
}
...
render() {
return (
columnDataFetch={this.columnDataFetch}
columnIdProp={"id"}
externalFetch={this.externalFetch}
recordType={"accounts"}
rowCount={1}
rowIdProp={"id"}
/>
);
}
}
`
For an example of the DataGrid, see examples\src\index.js. To the run the example, you need to run the example API:
`
yarn api
`
...and the example app in another terminal:
`
yarn start
`
---
Props
`
/* Required /
`
- columnDataFetch - Function. Gets Column header data. Must return a promise.
- columnIdProp - String, required, column property to use as unique identifier
- externalFetch Function, required. DataGrid calls this function to perform external data operations (see below)
rowCount - Number, required
rowIdProp - String, required, row property to use as unique identifier
/* Optional /
`
- columnFilterFetch - Function. Gets column header filter data. Must return a promise.
- controlComponents - Array, accepts an array of components to be rendered in TableControls
- customColumnWidthsEnabled: Boolean, toggles column resizing; Specify which columns are resizable by setting isResizable prop on column object
- customColumnWidthsMax: Number, defaults to Number.MAX_SAFE_INTEGER. Max size of resizable column
- customColumnWidthsMin: Number, defaults to 75. Minimum size of resizable column
- dataGridContext - Object, provides component overrides, methods, editModeComponents, and validation schema (see below)
- dataGridId - String, key id for storing multiple dataDrid instances under the same redux store
- defaultSortColumn - String, used to prevent sort direction of NONE on the default sort column. Maps to columnFieldName. Required when default sort is DESC
- deleteRowFetch - Function.
- editingEnabled - Boolean, toggles whether Edit Controls render
- editModeCallback - Function. Called when cancelEditing action is dispatched
- editUpdateFetch - Function. Must return rows data.
- headerComponents - Array, accepts an array of components to be rendered in the DataGridHeader (the same div as the selection stats)
pageNumber - Number, defaults 1, first page to load
pagesPerSet - Number, default 5
paginationEnabled - Boolean, required, defaults false
recordType - String, e.g. "accounts" or "enterprises"
retainStore - Bool, prevents the store being cleared out on unmount during in place reload scenerios.
rowsPerPage - Number, defaults 10
rowsPerPageSelectorEnabled - Boolean, displays "Show N rows per page dropdown" control
rowsPerPageSelectorOptions - Array of Numbers, defaults [10, 25, 50]
subRowsFetch - Function. DataGrid calls this function to retrieve subrows for groups (similar to external fetch)
---
Datasource (rows, columns and filters)
Each of the Datasource items must be supplied via the corresponding fetch (rows: externalFetch, subRows: subRowsFetch, columns: columnDataFetch, filters: columnFilterFetch).
$3
Row data needing sorting, filtering, etc, needs to use the external fetch prop. This can point to an API or a local method.
`javascript
// row data structure
{
id: "number", // unique number for identifying row
"keyName": "property", // keyName maps to a column's columnFieldName. Property is what to display to user (eg. asOfDate: "01/03/2004")
isExpandable: "bool", // for use when showing an expandable row
isDeletable: "bool" // to indicate whether a row is deletable
}
// API response data structure required
{
data: {
[recordType]: [...]
},
meta: {
maxPages: "number", // maximum number of pages of rows
pageNumber: "number", // page number to display on render
pageSize: "number", // how many rows per page
totalRows: "number" // total rows avail to user (minus filter, search, etc.)
}
}
`
$3
Expandable TableRows render SubRows when expanded. Clicking the expand button in the parent row makes a call to the subRowsFetch function provided as a prop to the DataGrid instance. SubRows are rendered as siblings of the parent row; the visual hierarchy between the parent/child rows is accomplished via styling. Each TableRow instance caches its subRows and re-renders on subsequent expands.
Example subRowsFetch function:
`javascript
const subRowsFetch = ({ row, id }) => {
//fetch subRows for specified row id
return fetch("http://localhost:3004/subrows/{id}")
.then(response => response.json())
.then(subRows => ({ subRows }));
};
`
$3
If columns is loaded from an api, all row data must also load from an api using the externalFetch prop.
`javascript
// column data structure
{
id: "number", // optional
columnFieldName: "string", // maps to row data (eg. inceptionStateDate, accountNumber), required
columnName: "string", // Displayed to user, required
width: "string", // default: auto (eg. 20px *px only), optional
isEditable: "bool", // allows edit mode on column, optional
isFilterable: "bool", // allows filtering of column, optional
isResizable: "bool", //allows resizing of column when top-level customColumnWidthsEnabled === true, optional
isSortable: "bool" // allows sorting of column, optional
}
// API response data structure required
{
data: {
columns: [...]
},
meta: {
hasExpandableRows: "bool", // adds spacer cell for row toggle
selectionEnabled: "bool", // adds all selection checkboxes
selectionMenuEnabled: "bool", // adds selection dropdown
}
}
`
$3
Filter data is strictly build on the API and requires the column data also be from the API. This is because of the unique id used to tie the filter data and column data together to map both.
`javascript
// filter data structure
/// multiselect via checkboxes
{
id: "number",
filterName: "string", // identifier for passing back to API
columnId: "string|number", // should be same as column data for mapping
filterType: "multiselect", // type of filter to display
options: [
{
name: "string", // text to display to user
value: "number" // value of checkbox (eg. id)
}
],
selectedValues: [...], // default selected values, should match to values of the options
label: "string" // label to display
},
/// text input
{
id: "number",
filterName: "string", // identifier for passing back to API
columnId: "string|number",
defaultValue: "string", // empty if no filter applied
filterType: "text", // type of filter to display
placeholder: "string", // default placeholder to display
label: "string" // text to display
},
/// date range input
{
id: "number",
filterName: "string", // identifier for passing back to API
columnId: "string|number",
filterType: "daterange", // type of filter to display
label: "string", // text to display
startDate: "string", // YYYY-MM-DD
endDate: "string" // YYYY-MM-DD
}
];
`
---
externalFetch
externalFetch is a Promise function provided to the grid and used when either paginationEnabled or on sort props is set to true. This callback provides data to the grid from an external source via the rows key.
`javascript
const externalFetch = ({ data, appliedFilter }) => {
const {
rowsPerPage,
pageNumber,
paginationEnabled
} = data.pagination;
const {
direction,
sortColumn
} = data.sort;
const apiEndpoint = 'http://api/accounts';
return fetch(apiEndpoint, {
method: 'POST' ,
body: JSON.stringify({
pageNumber,
pageSize: rowsPerPage,
sortColumnName: sortColumn,
sortDirection: direction,
appliedFilter
})
})
.then(res => res.json())
.then(res => {
return {
rows: res.rows,
rowCount: res.meta.totalRows
}
})
.catch(err => {
console.error('error: ', err);
});
};
externalFetch={externalFetch}
...
/>
`
---
context
Sub-component overrides and additional methods can be configured via the context prop. This feature makes use of React Context to inject dependencies to sub-components as needed instead of passing dependencies as props down through the entire component tree. This also allows overriding of sub-components without altering the overridden components' ancestors.
#### dataGridContext.components
The datagrid exports the following components:
`javascript
export const components = {
DataGrid,
DataGridReducers,
Table,
TableBody,
TableHeader,
TableHeaderCell,
TableRow,
TableRowCell,
TableRowCellContents,
DataGridContextConsumer
};
`
To override TableRow with a CustomRow component, do the following:
`javascript
import { CustomRow } from './CustomRow';
...
//in render/return of a functional component
dataGridContext={{
components: {
TableRow: CustomRow
}
}}
...
/>
`
Overriding components should consume the DataGridContextConsumer and render child components from the context's component property:
`javascript
import { DataGridContextConsumer } from '@tamarac/datagrid';
...
//in render/return of a functional component
{({ components }) => {
const { TableRow } = components;
return (
...
);
}}
;
`
#### dataGridContext.methods
The primary use of dataGridContext.methods is to pass an externalCellContentsComposer function to the TableRowCell component. This is used to provide external, instance-specific logic for rendering table cell contents. externalCellContentsComposer makes use of higher-order components to compose behavior associated with certain data types. (@TODO: move HOCs to a separate package)
To add cell rendering logic to your instance, first define an HOC for the behavior:
`javascript
import React, { Fragment } from "react";
export const isImportant = () => WrappedComponent => {
const IsImportant = props => (
);
return IsImportant;
};
`
Then define contents composer function. The following example applies the isImportant HOC to any objective cell with the value of 'red':
`javascript
import { isImportant } from "./hoc/isImportant";
export const externalCellContentsComposer = props => {
const { row, code } = props;
const hocs = [];
if (code === "objective" && row[code] === "red") {
hocs.push(isImportant());
}
return hocs;
};
`
Now include the externalCellContentsComposer function in the dataGridContext.methods object:
`javascript
`
#### dataGridContext.formikProps
FormikProps are used to pass a Yup validationSchema to the DataGrid's Formik instance. Disabling and Enabling entire column inputs is also passed through this prop. (See Edit Mode below.)
#### dataGridContext.editModeComponents
The dataGridContext.editModeComponents property is used to override the default editMode component for the specified column(s). To override an editMode component, add the columnFieldName of the column for which you wish to customize to the editModeComponent.
`javascript
...
{
...
//columnFieldName: ComponentName
accountName: CustomAccountNameEditingComponent
}
`
The component is passed the following props:
`javascript
column={column}
columnFieldName={columnFieldName}
disabled={disabled}
dropdownOptions={dropdownOptions}
errors={errors}
handleBlur={handleBlur}
id={id}
isTouched={isTouched}
idValid={isValid}
rowIndex={rowIndex}
setFieldTouched={setFieldTouched}
setFieldValue={setFieldValue}
value={value}
/>
`
@TODO - Need more documentation on EditModeComponent props. For now, consult with @tamarac UI devs for help using these props.
---
Edit Mode
Edit mode is for tables that have editable data on them. When the prop editingEnabled is passed to the module's overall component as true, the editing controls will appear in a div above the table. If editingEnabled is false, nothing will render, even if there are editable columns in the dataSource.
For cells to change from regular to edit mode, their corresponding column object must have the the isEditable attribute set to true. Toggling edit mode on changes editable cells to @tamarac/reactui ValidatedInput components. The default is for these to be text inputs, but if there are dropdown options available, it will render as a dropdown with those options.
Below is an example of an array of column objects. The first is editing with a text input, the second is editable with a dropdown, the third is not editable.
`json
columns: [
{
columnId: 0,
columnName: 'Vegetable',
isEditable: true,
dropdownOptions: [],
},
{
columnId: 0,
columnName: 'Fruit',
isEditable: true,
dropdownOptions: ['Mango','Papaya','Guava'],
},
{
columnId: 0,
columnName: 'Bread',
isEditable: false,
dropdownOptions: [],
},
]
`
Edits done by the user are persisted in Formik until submitted by the SAVE button.
#### editUpdateFetch
For Edit Mode to be able to save edits, it relies on passing the edits in the store to an API, which should return the updated data. Here's an example of the prop editUpdateFetch which is passed to the component. It assumes there is a RESTful API.
The DataGrid passes the current store state, as well as Formik's values and touched objects which are of the following shape:
`javascript
const values = {
rows: [
{id: 1, accountName: 'Big Bucks; No Whammies'},
...
]
}
const touched = {
rows: [
{
accountName: true
}
]
}
`
The editUpateFetch function can parse changed rows/fields from values object as such:
`javascript
const editUpdateFetch = (state, values, touched) => {
const { data, editing } = state;
const { active } = editing;
const rowsData = data.rows.byId;
if (active.length === 1) {
rowsData[active[0]] = values.rows[0];
} else {
touched.rows.forEach((touchedRow, index) => {
if (typeof touchedRow !== "undefined") {
const rowId = data.rows.order[index];
Object.keys(touchedRow).forEach(key => {
const row = rowsData[rowId];
if (touchedRow[key]) {
row[key] = values.rows[index][key];
}
});
}
});
}
return new Promise((resolve, reject) => {
resolve(rowsData);
});
};
`
$3
The disabledFields prop can allow for dynamic input disabling in edit mode. The prop passed should match the column primary key. The entire editing column of inputs will be disabled.
`js
dataGridContext={
{
disabledFields: {
accountName: true === true,
validationSchema: Yup.object()
}
}
} .../>
`
$3
The validationSchema is defined by a Yup schema passed to the DataGrid in dataGridContext.formikProps.validationSchema. It's not necessary to enumerate all editable fields in the schema - only the ones that should be validated. Below is an example schema for an editable DataGrid with a required performanceInceptionDate field:
`javascript
import * as Yup from 'yup';
...
const validationSchema = Yup.object().shape({
rows: Yup.array().of(
Yup.object().shape({
performanceInceptionDate: Yup.string().required("I am required")
})
)
});
...
`
---
Updating data
$3
If editing is enabled, a deleteRowFetch function is required to make the api call to remove rows from the dataset.
`javascript
const deleteRowFetch = (state, rowIds) => {
const confirmed = confirm("Delete?");
return new Promise((resolve, reject) => {
if (confirmed) {
//IRL - api call to delete the row
resolve();
} else {
reject();
}
});
};
`
$3
To update field values for a specific row (such as in the case of deferred columns), import the setAsyncData action into your app and dispatch the action with an array of change objects as such:
`javascript
import {setAsyncData} from '@tamarac/datagrid';
...
const data = [
{ row: 614, field: 'currentValue', value: 1000 },
{ row: 615, field: 'currentValue', value: 1000 },
{ row: 117, field: 'currentValue', value: 1000 },
{ row: 39, field: 'currentValue', value: 1000 },
{ row: 161, field: 'currentValue', value: 1000 },
{ row: 155, field: 'currentValue', value: 1000 },
{ row: 122, field: 'currentValue', value: 1000 }
];
setTimeout(() => store.dispatch(setAsyncData(data)), 5000);
`
$3
To update the entire data set--for example, when filtering or searching data externally--call the fetchTableData action. This action requires the store's dispatch and getState actions. getState is function that can return a mutated state with the added filter/search/etc. options. This action calls the provided externalFetch function to get its data.
`javascript
class ConnectedComponent extends Component {
state = {
filters: [...]
}
stateWithFilters() {
const filters = this.state.filters.reduce((acc, filter) => {
return {
...acc,
[filter.name]: filter.id
};
}, {});
// entire state from mapStateToProps
const storeState = this.props.state;
return {
...storeState,
filters
};
}
onFilter() {
const callback = fetchTableData();
callback(this.props.dispatch, this.stateWithFilters);
}
render() {
return (
)
}
``