A modular table based on a CSS grid layout optimized for customization

react-grid-table

A modular table, based on a CSS grid layout, optimized for customization.

Supported features:

  • Sort by column
  • Column resize
  • Column reorder
  • Search with highlight
  • Pagination
  • Row selection
  • Inline row editing
  • Column pinning (pre-configured)
  • Column visibility management
  • Sticky header
  • Dynamic row height

Install

npm install --save @nadavshaar/react-grid-table

Usage

import React from "react";

import GridTable from '@nadavshaar/react-grid-table'
// applying styles - required
import '@nadavshaar/react-grid-table/dist/index.css'

import Username from "./components/Username"
import * as MOCK_DATA from "./MOCK_DATA.json";

let rows = MOCK_DATA.default;
// row data example: 
// { 
//     "id": 1, 
//     "username": "wotham0", 
//     "gender": "Male", 
//     "last_visited": "12/08/2019", 
//     "object_value_field": {"x": 1, "y": 2}, 
//     ... 
// } 

const MyAwesomeTable = () => {
    
    const columns = [
        {
            id: 1,
            field: 'checkbox',
            pinned: true,
        },
        {
            id: 2, 
            field: 'username', 
            label: 'Username',
            cellRenderer: Username,
        }, 
        {
            id: 3, 
            field: 'gender', 
            label: 'Gender',
        },
        {
            id: 4, 
            field: 'last_visited', 
            label: 'Last Visited',
            sort: ({a, b, isAscending}) => {
                let aa = a.split('/').reverse().join(),
                bb = b.split('/').reverse().join();
                return aa < bb ? isAscending ? -1 : 1 : (aa > bb ? isAscending ? 1 : -1 : 0);
            }
        },
        {
            id: 5, 
            field: 'object_value_field', 
            label: 'Object Value',
            getValue: ({value, column}) => value.x.toString(),
        }
    ];

    return (
        <GridTable 
            columns={columns}
            rows={rows} 
        />
    )
};

export default MyAwesomeTable;

Table of contents

Components structure

HEADER (optional | customizable): search & column visibility management
TABLE HEADER:  sort, resize & column reorder
TABLE BODY:  displaying data / loader / no-results, row editing & row selection
FOOTER (optional | customizable): items information & pagination

props

name type description default value
columns* array of objects columns configuration (details) [ ]
rows* array of objects rows data (details) [ ]
rowIdField string the name of the field in the row's data that should be used as the row identifier - must be unique 'id'
selectedRowsIds array of ids selected rows ids (details) [ ]
searchText string text for search ""
isRowSelectable function whether row selection for the current row is disabled or not row => true
isRowEditable function whether row editing for the current row is disabled or not row => true
editRowId string, number, null the id of the row to edit, (more details about row editing) null
cellProps object global props for all data cells { }
headerCellProps object global props for all header cells { }

Table configuration props

name type description default value
isPaginated boolean determine whether the pagination controls sholuld be shown in the footer and if the rows data should be splitted into pages true
pageSizes array of numbers page size options [20, 50, 100]
pageSize number the selected page size 20
sortBy string, number, null the id of the column that should be sorted null
sortAscending boolean determine the sort direction true
minColumnWidth number minimum width for all columns 70
highlightSearch boolean whether to highlight the search term true
showSearch boolean whether to show the search in the header true
searchMinChars number the minimum characters to apply search and highlighting 2
isLoading boolean whether to render a loader false
isHeaderSticky boolean whether the table header will be stick to the top when scrolling or not true
manageColumnVisibility boolean whether to display the columns visibility management button (located at the top right of the header) true
icons object with nodes custom icons config (current supprt for sort icons only) { sort: { ascending: ▲, descending: ▼ } }

Event props

name type description usage
onColumnsChange function triggers when the columns has been changed columns => { }
onSelectedRowsChange function triggers when rows selection has been changed selectedRowsIds => { }
onSearchChange function used for updating the search text when controlled from outside of the component searchText => { }
onSortChange function used for updating the sortBy and its direction when controlled from outside of the component (columnId, isAscending) => { }
onRowClick function triggers when a row has been clicked ({rowIndex, row, column, event}) => { }

Custom rendering props

A set of functions that are used for rendering custom components.

name type description usage
headerRenderer function used for rendering a custom header (details) ({searchText, setSearchText, setColumnVisibility, columns}) => ( children )
footerRenderer function used for rendering a custom footer (details) ({page, totalPages, handlePagination, pageSize, pageSizes, setPageSize, setPage, totalRows, selectedRowsLength, numberOfRows }) => ( children )
loaderRenderer function used for rendering a custom loader () => ( children )
noResultsRenderer function used for rendering a custom component when there is no data to display () => ( children )
searchRenderer function used for rendering a custom search component (details) ({searchText, setSearchText}) => ( children )
columnVisibilityRenderer function used for rendering a custom columns visibility management component (details) ({columns, setColumnVisibility}) => ( children )
dragHandleRenderer function used for rendering a drag handle for the column reorder () => ( children )

props - detailed

columns

Type: array of objects.

This prop defines the columns configuration.

Each column supports the following properties:

name type description default value
id* string, number a unique id for the column ---
field* string the name of the field as in the row data / 'checkbox' (more details about checkbox column) ---
label string the label to display in the header cell the field property
pinned boolean whether the column will be pinned to the side, supported only in the first and last columns false
visible boolean whether to show the column (pinned columns are always visible) true
className string a custom class selector for all column cells ""
width string the initial width of the column in grid values (full list of values) "max-content"
minWidth number, null the minimum width of the column when resizing null
maxWidth number, null the maximum width of the column when resizing null
getValue function used for getting the cell value (usefull when the cell value is not a string - details) (({value, column}) => value
setValue function used for updating the cell value (usefull when the cell value is not a string) - details ({value, row, setRow, column}) => { setRow({...row, [column.field]: value}) }
searchable boolean whether to apply search filtering on the column true
editable boolean whether to allow editing for the column true
sortable boolean whether to allow sort for the column true
resizable boolean whether to allow resizing for the column true
sortableColumn boolean whether to allow column reorder true
search function the search function for this column ({value, searchText}) => value.toLowerCase().includes(searchText.toLowerCase())
sort function the sort function for this column ({a, b, isAscending}) => { if(a.toLowerCase() > b.toLowerCase()) return isAscending ? 1 : -1; else if(a.toLowerCase() < b.toLowerCase()) return isAscending ? -1 : 1; return 0; }
cellRenderer function used for custom rendering the cell ({value, row, column, rowIndex, searchText}) => ( children ) ---
headerCellRenderer function used for custom rendering the header cell ({label, column}) => ( children ) ---
editorCellRenderer function used for custom rendering the cell in edit mode ({value, field, onChange, row, rows, column, rowIndex}) => ( children ) ---

Example:

// column config

{
  id: 'some-unique-id',
  field: 'first_name',
  label: 'First Name',
  className: '',
  pinned: false,
  width: 'max-content',
  getValue: ({value, column}) => value, 
  setValue: ({value, row, setRow, column}) => { setRow({...row, [column.field]: value}) },
  minWidth: null,
  maxWidth: null,
  sortable: true,
  editable: true,
  searchable: true,
  visible: true,
  resizable: true,
  sortableColumn: true,
  // search: ({value, searchText}) => { },
  // sort: ({a, b, isAscending}) => { },
  // cellRenderer: ({value, row, column, rowIndex, searchText}) => { },
  // headerCellRenderer: ({label, column}) => ( ),
  // editorCellRenderer: ({value, field, onChange, row, rows, column, rowIndex}) => { }
}

checkbox-column

Rows selection is done by a predefined column, simply add a column with a field name of 'checkbox'.

Checkbox column has supports the following properties:

name type description default value
id* string, number a unique id for the column ---
field* string defines the column as a 'checkbox' column 'checkbox'
pinned boolean whether the column will be pinned to the side, supported only in the first and last columns false
visible boolean whether to show the column (pinned columns are always visible) true
className string a custom class for all column cells ""
width string the initial width of the column in grid values (full list of values) "max-content"
minWidth number, null the minimum width of the column when resizing null
maxWidth number, null the maximum width of the column when resizing null
resizable boolean whether to allow resizing for the column true
cellRenderer function used for custom rendering the checkbox cell ({isChecked, callback, disabled, rowIndex}) => ( <input type="checkbox" onChange={ callback } checked={ isChecked } disabled={ disabled } /> ) ---
headerCellRenderer function used for custom rendering the checkbox header cell ({isChecked, callback, disabled}) => ( <input type="checkbox" onChange={ callback } checked={ isChecked } disabled={ disabled } /> ) ---

Example:

// checkbox column config

{
  id: 'some-unique-id',
  field: 'checkbox',
  pinned: true,
  className: '',
  width: '53px',
  minWidth: null,
  maxWidth: null,
  resizable: false,
  visible: true,
  // cellRenderer: ({isChecked, callback, disabled, rowIndex}) => ( children )
  // headerCellRenderer: ({isChecked, callback, disabled}) => ( children )
}

rows

Type: array of objects.

This prop containes the data for the rows.

Each row should have a unique identifier field, which by default is id, but it can be changed to a different field using the rowIdField prop.

// row data

{
  "id": "some-unique-id", 
  "objectValueField": {"x": 1, "y": 2}, 
  "username":"wotham0",
  "first_name":"Waldemar",
  "last_name":"Otham",
  "avatar":"https://robohash.org/atquenihillaboriosam.bmp?size=32x32&set=set1",
  "email":"wotham0@skyrock.com",
  "gender":"Male",
  "ip_address":"113.75.186.33",
  "last_visited":"12/08/2019"
}

Note: If a property value is not of type string, you'll have to use the getValue function on the column in order to format the value.

Example:

Let's say the field's value for a cell is an object:

{ ... , fullName: {firstName: 'some-first-name', lastName: 'some-last-name'} },

Its getValue function for displaying the first and last name as a full name, would be:

getValue: (({value, column}) => value.firstName + ' ' + value.lastName

The value that returns from the getValue function will be used for searching, sorting etc...

headerRenderer

Type: function

This function is used for rendering a custom header.

By default the header renders a search and column visibility components, you can render your own custom components and still use the table's callbacks.

If you just want to replace the search or the column visibility management components, you can use the searchRenderer or the columnVisibilityRenderer props.

Arguments:

name type description default value
searchText string text for search ""
setSearchText function a callback function to update search text setSearchText(searchText)
setColumnVisibility function a callback function to update columns visibility that accepts the id of the column that should be toggled setColumnVisibility(columnId)
columns function the columns configuration [ ]

Example:

headerRenderer={({searchText, setSearchText, setColumnVisibility, columns}) => (
    <div style={{display: 'flex', flexDirection: 'column', padding: '10px 20px', background: '#fff', width: '100%'}}>
        <div>
            <label htmlFor="my-search" style={{fontWeight: 500, marginRight: 10}}>
                Search for:
            </label>
            <input 
                name="my-search"
                type="search" 
                value={searchText} 
                onChange={e => setSearchText(e.target.value)} 
                style={{width: 300}}
            />
        </div>
        <div style={{display: 'flex', marginTop: 10}}>
            <span style={{ marginRight: 10, fontWeight: 500 }}>Columns:</span>
            {
                columns.map((cd, idx) => (
                    <div key={idx} style={{flex: 1}}>
                        <input 
                            id={`checkbox-${idx}`}
                            type="checkbox" 
                            onChange={ e => setColumnVisibility(cd.id) } 
                            checked={ cd.visible !== false } 
                        />
                        <label htmlFor={`checkbox-${idx}`} style={{flex: 1, cursor: 'pointer'}}>
                          {cd.label || cd.field}
                        </label>
                    </div>
                ))
            }
        </div>
    </div>
)}

footerRenderer

Type: function

This function is used for rendering a custom footer.

By default the footer renders items information and pagination controls, you can render your own custom components and still use the table's callbacks.

Arguments:

name type description default value
page number the current page 1
totalPages number the total number of pages 0
handlePagination function sets the page to display handlePagination(pageNumber)
pageSizes array of numbers page size options [20, 50, 100]
pageSize number the selected page size 20
setPageSize function updates the page size setPageSize(pageSizeOption)
totalRows number total number of rows in the table 0
selectedRowsLength number total number of selected rows 0
numberOfRows number total number of rows in the page 0

Example:

footerRenderer={({
    page, 
    totalPages, 
    handlePagination, 
    pageSize, 
    pageSizes, 
    setPageSize, 
    totalRows,
    selectedRowsLength,
    numberOfRows
}) => (
    <div style={{display: 'flex', justifyContent: 'space-between', flex: 1, padding: '12px 20px'}}>
        <div style={{display: 'flex'}}>
          {`Total Rows: ${totalRows} 
          | Rows: ${numberOfRows * page - numberOfRows} - ${numberOfRows * page} 
          | ${selectedRowsLength} Selected`}
        </div>
        <div style={{display: 'flex'}}>
            <div style={{width: 200, marginRight: 50}}>
                <span>Items per page: </span>
                <select 
                    value={pageSize} 
                    onChange={e => {setPageSize(e.target.value); handlePagination(1)}}
                >
                    { pageSizes.map((op, idx) => <option key={idx} value={op}>{op}</option>) }
                </select>
            </div>
            <div style={{display: 'flex', justifyContent: 'space-between', width: 280}}>
                <button 
                    disabled={page-1 < 1} 
                    onClick={e => handlePagination(page-1)}
                >Back</button>

                <div>
                    <span>Page: </span>
                    <input 
                        style={{width: 50, marginRight: 5}}
                        onClick={e => e.target.select()}
                        type='number' 
                        value={totalPages ? page : 0} 
                        onChange={e => handlePagination(e.target.value*1)}
                    />
                    <span>of {totalPages}</span>
                </div>

                <button 
                    disabled={page+1 > totalPages} 
                    onClick={e => handlePagination(page+1)}
                >Next</button>
            </div>
        </div>
    </div>
)}

How to...

Row-Editing

Row editing can be done by rendering your row edit button using the cellRenderer property in the column configuration, then when clicked, it will set a state proprty with the clicked row id, and that row id would be used in the editRowId prop, then the table will render the editing components for columns that are defined as editable (true by default), and as was defined in the editorCellRenderer which by default will render a text input.

// state
const [rows, setRows] = useState(MOCK_DATA);
const [editRowId, setEditRowId] = useState(null)

// columns
let columns = [
  ...,
  {
    id: 'my-buttons-column'
    field: 'buttons', 
    label: '',
    pinned: true,
    sortable: false,
    resizable: false,
    cellRenderer: ({value, row, column, rowIndex, searchText}) => (
      <button onClick={e => setEditRowId(row.id)}>Edit</button>
    )
    editorCellRenderer: ({value, field, onChange, row, rows, column, rowIndex}) => (
      <div style={{display: 'inline-flex'}}>
        <button onClick={e => setEditRowId(null)}>Cancel</button>
        <button onClick={e => {
          let rowsClone = [...rows];
          let updatedRowIndex = rowsClone.findIndex(r => r.id === row.id);
          rowsClone[updatedRowIndex] = row;

          setRows(rowsClone);
          setEditRowId(null);
        }}>Save</button>
      </div>
    )
  }
];

// update handler
const updateRowData = (row) => {
  let rowsClone = [...rows];
  let rowIndex = rowsClone.findIndex(it => it.id === item.id);
  rowsClone[rowIndex] = row;
  setRows(rowsClone);
}

// render
<GridTable 
    columns={columns}
    rows={rows} 
    editRowId={editRowId}
    ...
/>

For columns which holds values other than string, you'll have to also define the setValue function on the column so the updated value won't override the original value.

Example:

  setValue: ({value, row, setRow, column}) => {
    // value: '35', 
    // row: { ..., { fieldToUpdate: '27' }} 
    let rowClone = { ...row };
    rowClone[column.field].fieldToUpdate = value;
    setRow(rowClone);
  }

Styling

Styling is done by css class selectors. the table's components are mapped with pre-defined classes, and you can add your own custom class per column in the columns configuration.

Component All available class selectors
Wrapper rgt-wrapper
Header rgt-header-container
Search rgt-search-container rgt-search-label rgt-search-icon rgt-search-input rgt-search-highlight
Columns Visibility Manager rgt-columns-manager-wrapper rgt-columns-manager-button rgt-columns-manager-popover rgt-columns-manager-popover-open rgt-columns-manager-popover-row
Table rgt-container
Header Cell rgt-cell-header rgt-cell-header-checkbox rgt-cell-header-[column.field] rgt-cell-header-sortable / rgt-cell-header-not-sortable rgt-cell-header-sticky rgt-cell-header-resizable / rgt-cell-header-not-resizable rgt-cell-header-searchable / rgt-cell-header-not-searchable rgt-cell-header-sortable-column / rgt-cell-header-not-sortable-column rgt-cell-header-pinned rgt-cell-header-pinned-left / rgt-cell-header-pinned-right [column.className] rgt-cell-header-inner rgt-cell-header-inner-checkbox-column rgt-header-checkbox-cell rgt-resize-handle rgt-sort-icon rgt-sort-icon-ascending / rgt-sort-icon-descending rgt-column-sort-ghost
Cell rgt-cell rgt-cell-[column.field] rgt-row-[rowNumber] rgt-row-odd / rgt-row-even rgt-row-hover rgt-row-selectable / rgt-row-not-selectable rgt-cell-inner rgt-cell-checkbox rgt-cell-pinned rgt-cell-pinned-left / rgt-cell-pinned-right rgt-cell-editor rgt-cell-editor-inner rgt-cell-editor-input
Pagination rgt-footer-items-per-page rgt-footer-pagination-button rgt-footer-pagination-container rgt-footer-page-input
Footer rgt-footer rgt-footer-items-information rgt-footer-right-container
Utils rgt-text-truncate rgt-clickable rgt-disabled rgt-disabled-button rgt-flex-child

GitHub