npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@tamarac/datagrid

v2.1.5

Published

Tamarac Reporting datagrid

Downloads

121

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 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).

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} />
    )
  }