@tamarac/datagrid
v2.1.5
Published
Tamarac Reporting datagrid
Downloads
88
Keywords
Readme
Tamarac Datagrid
Datagrid Component for Tamarac Trading & Reporting
tl;dr
Install the component and add an instance to your app.
yarn add @tamarac/datagrid
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 (
<Provider store={store}>
<DataGrid
columnDataFetch={this.columnDataFetch}
columnIdProp={"id"}
externalFetch={this.externalFetch}
recordType={"accounts"}
rowCount={1}
rowIdProp={"id"}
/>
</Provider>
);
}
}
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 identifierexternalFetch
Function, required. DataGrid calls this function to perform external data operations (see below)`rowCount
- Number, requiredrowIdProp
- 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 TableControlscustomColumnWidthsEnabled
: Boolean, toggles column resizing; Specify which columns are resizable by settingisResizable
prop on column objectcustomColumnWidthsMax
: Number, defaults toNumber.MAX_SAFE_INTEGER
. Max size of resizable columncustomColumnWidthsMin
: Number, defaults to 75. Minimum size of resizable columndataGridContext
- Object, provides component overrides, methods, editModeComponents, and validation schema (see below)dataGridId
- String, key id for storing multiple dataDrid instances under the same redux storedefaultSortColumn
- String, used to prevent sort direction ofNONE
on the default sort column. Maps tocolumnFieldName
. Required when default sort isDESC
deleteRowFetch
- Function.editingEnabled
- Boolean, toggles whether Edit Controls rendereditModeCallback
- Function. Called whencancelEditing
action is dispatchededitUpdateFetch
- 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 loadpagesPerSet
- Number, default 5paginationEnabled
- Boolean, required, defaults falserecordType
- String, e.g. "accounts" or "enterprises"retainStore
- Bool, prevents the store being cleared out on unmount during in place reload scenerios.rowsPerPage
- Number, defaults 10rowsPerPageSelectorEnabled
- Boolean, displays "Show N rows per page dropdown" controlrowsPerPageSelectorOptions
- 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
).
Rows
Row data needing sorting, filtering, etc, needs to use the external fetch prop. This can point to an API or a local method.
// 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.)
}
}
SubRows
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:
const subRowsFetch = ({ row, id }) => {
//fetch subRows for specified row id
return fetch("http://localhost:3004/subrows/{id}")
.then(response => response.json())
.then(subRows => ({ subRows }));
};
Columns
If columns is loaded from an api, all row data must also load from an api using the externalFetch prop.
// 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
}
}
Filters
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.
// 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.
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);
});
};
<DataGrid
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:
export const components = {
DataGrid,
DataGridReducers,
Table,
TableBody,
TableHeader,
TableHeaderCell,
TableRow,
TableRowCell,
TableRowCellContents,
DataGridContextConsumer
};
To override TableRow
with a CustomRow
component, do the following:
import { CustomRow } from './CustomRow';
...
//in render/return of a functional component
<DataGrid
dataGridContext={{
components: {
TableRow: CustomRow
}
}}
...
/>
Overriding components should consume the DataGridContextConsumer
and render child components from the context's component
property:
import { DataGridContextConsumer } from '@tamarac/datagrid';
...
//in render/return of a functional component
<DataGridContextConsumer>
{({ components }) => {
const { TableRow } = components;
return (
<Fragment>
<TableRow />
...
</Fragment>
);
}}
</DataGridContextConsumer>;
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:
import React, { Fragment } from "react";
export const isImportant = () => WrappedComponent => {
const IsImportant = props => (
<Fragment>
<span style={{ color: "tomato" }}>
<WrappedComponent {...props} />
</span>
</Fragment>
);
return IsImportant;
};
Then define contents composer function. The following example applies the isImportant
HOC to any objective cell with the value of 'red':
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:
<DataGrid dataGridContext={{ methods: { externalCellContentsComposer } }} ... />
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
.
...
{
...
//columnFieldName: ComponentName
accountName: CustomAccountNameEditingComponent
}
The component is passed the following props:
<EditModeComponent
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.
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 <DataGrid/>
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:
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:
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);
});
};
Disabling Inputs
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.
<DataGrid
dataGridContext={
{
disabledFields: {
accountName: true === true,
validationSchema: Yup.object()
}
}
} .../>
Validation
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:
import * as Yup from 'yup';
...
const validationSchema = Yup.object().shape({
rows: Yup.array().of(
Yup.object().shape({
performanceInceptionDate: Yup.string().required("I am required")
})
)
});
...
<DataGrid dataGridContext={{formikProps: {validationSchema}}} .../>
Updating data
Deleting Rows
If editing is enabled, a deleteRowFetch function is required to make the api call to remove rows from the dataset.
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();
}
});
};
Specific Row Data
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:
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);
Whole Data Set
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.
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 (
<FilterComponent onFilter={this.onFilter} />
)
}