Column Rendering
Columns render the field
value of the data they are bound to. This is the default behavior, which can be customized in a number of ways that we’re exploring below.
Note
If you want to explicitly use the TypeScript type definition for columns, import the InfiniteTableColumn
type
import { InfiniteTableColumn } from '@infinite-table/infinite-react'
Note that it’s a generic type, so when you use it, you have to bind it to your DATA_TYPE
(the type of your data object).
Note
When using custom rendering or custom components for columns, make sure all your rendering logic is controlled and that it doesn’t have local/transient state.
This is important because InfiniteTable
uses virtualization heavily, in both column cells and column headers, so custom components can and will be unmounted and re-mounted multiple times, during the virtualization process (triggered by user scrolling, sorting, filtering and a few other interactions).
Change the value using valueGetter
The simplest way to change what’s being rendered in a column is to use the valueGetter
prop and return a new value for the column.
const nameColumn: InfiniteTableColumn<Employee> = {
header: 'Employee Name',
valueGetter: ({ data }) =>
`${data.firstName} ${data.lastName}`,
};
Note
The columns.valueGetter
prop is a function that takes a single argument - an object with data
and rowInfo
properties.
Note that the data
property is of type DATA_TYPE | Partial<DATA_TYPE> | null
and not simply DATA_TYPE
, because there are cases when you can have grouping (so for group rows with aggregations data
will be Partial<DATA_TYPE>
) or when there are lazily loaded rows or group rows with no aggregations - for which data
is still null
.
import * as React from 'react'; import { InfiniteTable, DataSource, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch( 'https://infinite-table.com/.netlify/functions/json-server' + '/developers1k' ) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns<Developer> = { id: { field: 'id', defaultWidth: 80 }, name: { header: 'Full Name', sortable: false, valueGetter: ({ data }) => { if (!data) { return null; } return `${data.firstName} ${data.lastName}`; }, }, preferredLanguage: { field: 'preferredLanguage' }, stack: { field: 'stack' }, }; export default function ColumnValueGetterExample() { return ( <> <DataSource<Developer> primaryKey="id" data={dataSource}> <InfiniteTable<Developer> columns={columns} columnDefaultWidth={200} /> </DataSource> </> ); }
Use renderValue
and render
to display custom content
The next step in customizing the column is to use the columns.renderValue
or the columns.render
props. In those functions, you have access to more information than in the columns.valueGetter
function. For example, you have access to the current value of groupBy
and pivotBy
props.
renderValue
and render
can return any value that React can render.The renderValue
and render
functions are called with an object that has the following properties:
- data - the data object (of type
DATA_TYPE | Partial<DATA_TYPE> | null
) for the row. - rowInfo - very useful information about the current row:
rowInfo.value
- the value that will be rendered by defaultrowInfo.collapsed
- if the row is collased or not.rowInfo.groupBy
- the current group by for the rowrowInfo.indexInAll
- the index of the row in the whole data setrowInfo.indexInGroup
- the index of the row in the current group- … there are other useful properties that we’ll document in the near future
Note
The difference between columns.renderValue
and columns.render
is only for special columns (for now, only group columns are special columns, but more will come) when InfiniteTable
renders additional content inside the column (eg: collapse/expand tool for group rows). The columns.render
function allows you to override the additional content. So if you specify this function, it’s up to you to render whatever content, including the collapse/expand tool.
Note that for customizing the collapse/expand tool, you can use specify renderGroupIcon
function on the group column.
import * as React from 'react'; import { InfiniteTable, DataSource, DataSourceGroupBy, InfiniteTablePropGroupColumn, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch( 'https://infinite-table.com/.netlify/functions/json-server' + '/developers1k' ) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns<Developer> = { id: { field: 'id', defaultWidth: 80 }, stack: { field: 'stack', renderValue: ({ data, rowInfo }) => { if (rowInfo.isGroupRow) { return <>{rowInfo.value} stuff</>; } return <b>🎇 {data?.stack}</b>; }, }, firstName: { field: 'firstName', }, preferredLanguage: { field: 'preferredLanguage' }, }; const defaultGroupBy: DataSourceGroupBy<Developer>[] = [ { field: 'stack' }, ]; const groupColumn: InfiniteTablePropGroupColumn<Developer> = { defaultWidth: 250, renderValue: ({ rowInfo }) => { if (rowInfo.isGroupRow) { return ( <> Grouped by <b>{rowInfo.value}</b> </> ); } }, }; export default function ColumnValueGetterExample() { return ( <> <DataSource<Developer> primaryKey="id" data={dataSource} defaultGroupBy={defaultGroupBy}> <InfiniteTable<Developer> groupColumn={groupColumn} columns={columns} columnDefaultWidth={200} /> </DataSource> </> ); }
Changing the group icon using render
. The icon can also be changed using renderGroupIcon
.
import * as React from 'react'; import { InfiniteTable, DataSource, DataSourceGroupBy, InfiniteTablePropGroupColumn, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch( 'https://infinite-table.com/.netlify/functions/json-server' + '/developers1k' ) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns<Developer> = { id: { field: 'id', defaultWidth: 80 }, stack: { field: 'stack', }, firstName: { field: 'firstName', }, preferredLanguage: { field: 'preferredLanguage' }, }; const defaultGroupBy: DataSourceGroupBy<Developer>[] = [ { field: 'stack' }, ]; const groupColumn: InfiniteTablePropGroupColumn<Developer> = { defaultWidth: 250, render: ({ rowInfo, toggleCurrentGroupRow }) => { if (rowInfo.isGroupRow) { const { collapsed } = rowInfo; const expandIcon = ( <svg style={{ display: 'inline-block', fill: collapsed ? '#b00000' : 'blue', }} width="20px" height="20px" viewBox="0 0 24 24" fill="#000000"> {collapsed ? ( <> <path d="M0 0h24v24H0V0z" fill="none" /> <path d="M12 5.83L15.17 9l1.41-1.41L12 3 7.41 7.59 8.83 9 12 5.83zm0 12.34L8.83 15l-1.41 1.41L12 21l4.59-4.59L15.17 15 12 18.17z" /> </> ) : ( <path d="M7.41 18.59L8.83 20 12 16.83 15.17 20l1.41-1.41L12 14l-4.59 4.59zm9.18-13.18L15.17 4 12 7.17 8.83 4 7.41 5.41 12 10l4.59-4.59z" /> )} </svg> ); return ( <div style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', color: collapsed ? '#b00000' : 'blue', }} onClick={() => toggleCurrentGroupRow()}> <i style={{ marginRight: 5 }}>Grouped by</i>{' '} <b>{rowInfo.value}</b> {expandIcon} </div> ); } }, }; export default function ColumnCustomRenderExample() { return ( <> <DataSource<Developer> primaryKey="id" data={dataSource} defaultGroupBy={defaultGroupBy}> <InfiniteTable<Developer> groupColumn={groupColumn} columns={columns} columnDefaultWidth={200} /> </DataSource> </> ); }
Using hooks for custom rendering
Inside the columns.render
and columns.renderValue
functions, you can use hooks - both provided by InfiniteTable
and any other React
hooks.
Hook: useInfiniteColumnCell
When you’re inside a rendering function for a column cell, you can use useInfiniteColumnCell hook
to get access to the current cell’s rendering information - the argument passed to the render
or renderValue
functions.
import {
useInfiniteColumnCell,
InfiniteTableColumn,
} from '@infinite-table/infintie-react';
function CustomName() {
const { data, rowInfo } =
useInfiniteColumnCell<Employee>();
return (
<>
<b>{data.firstName}</b>, {data.lastName}
</>
);
}
const nameColumn: InfiniteTableColumn<Employee> = {
header: 'Employee Name',
renderValue: () => <CustomName />,
};
import * as React from 'react'; import { InfiniteTable, DataSource, useInfiniteColumnCell, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; import { HTMLProps } from 'react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch( 'https://infinite-table.com/.netlify/functions/json-server' + '/developers1k' ) .then((r) => r.json()) .then((data: Developer[]) => data); }; function CustomCell(props: HTMLProps<HTMLElement>) { const { value, data } = useInfiniteColumnCell<Developer>(); let emoji = '🤷'; switch (value) { case 'photography': emoji = '📸'; break; case 'cooking': emoji = '👨🏻🍳'; break; case 'dancing': emoji = '💃'; break; case 'reading': emoji = '📚'; break; case 'sports': emoji = '⛹️'; break; } const label = data?.stack === 'frontend' ? '⚛️' : ''; return ( <b> {emoji} + {label} </b> ); } const columns: InfiniteTablePropColumns<Developer> = { id: { field: 'id', maxWidth: 80 }, firstName: { field: 'firstName' }, hobby: { field: 'hobby', // we're not using the arg of the render function directly // but CustomCell uses `useInfiniteColumnCell` to retrieve it instead render: () => <CustomCell />, }, }; export default function ColumnRenderWithHooksExample() { return ( <> <DataSource<Developer> primaryKey="id" data={dataSource}> <InfiniteTable<Developer> columns={columns} columnDefaultWidth={200} /> </DataSource> </> ); }
Hook: useInfiniteHeaderCell
For column headers, you can use useInfiniteHeaderCell
hook to get access to the current header’s rendering information - the argument passed to the columns.header
function.
import {
useInfiniteHeaderCell,
InfiniteTableColumn,
} from '@infinite-table/infintie-react';
function CustomHeader() {
const { column } = useInfiniteHeaderCell<Employee>();
return <b>{column.field}</b>;
}
const nameColumn: InfiniteTableColumn<Employee> = {
header: 'Employee Name',
field: 'firstName',
header: () => <CustomHeader />,
};
import * as React from 'react'; import { InfiniteTable, DataSource, useInfiniteHeaderCell, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch( 'https://infinite-table.com/.netlify/functions/json-server' + '/developers1k' ) .then((r) => r.json()) .then((data: Developer[]) => data); }; const columns: InfiniteTablePropColumns<Developer> = { id: { field: 'id', maxWidth: 80 }, stack: { field: 'stack', }, hobby: { field: 'hobby', header: () => { const { column } = useInfiniteHeaderCell<Developer>(); return ( <b style={{ color: '#0000c2' }}> {column?.field} 🤷📸👨🏻🍳💃📚⛹️ </b> ); }, }, }; export default function ColumnHeaderExampleWithHooks() { return ( <> <DataSource<Developer> primaryKey="id" data={dataSource}> <InfiniteTable<Developer> columns={columns} columnDefaultWidth={200} /> </DataSource> </> ); }
Use column.components
to customize the column
There are cases when custom rendering via the columns.render
and columns.renderValue
props is not enough and you want to fully control the column cell and render your own custom component for that.
For such scenarios, you can specify column.components.HeaderCell
and column.components.ColumnCell
, which will use those components to render the DOM nodes of the column header and column cells respectively.
import { InfiniteTableColumn } from '@infinite-table/infintie-react';
const ColumnCell = (
props: React.HTMLProps<HTMLDivElement>
) => {
const { domRef, rowInfo } =
useInfiniteColumnCell<Developer>();
return (
<div
ref={domRef}
{...props}
style={{ ...props.style, color: 'red' }}>
{props.children}
</div>
);
};
const HeaderCell = (
props: React.HTMLProps<HTMLDivElement>
) => {
const { domRef, sortTool } =
useInfiniteHeaderCell<Developer>();
return (
<div
ref={domRef}
{...props}
style={{ ...props.style, color: 'red' }}>
{sortTool}
First name
</div>
);
};
const nameColumn: InfiniteTableColumn<Developer> = {
header: 'Name',
field: 'firstName',
components: {
ColumnCell,
HeaderCell,
},
};
Note
When using custom components, make sure you get domRef
from the corresponding hook (useInfiniteColumnCell
for column cells and useInfiniteHeaderCell
for header cells) and pass it on to the final JSX.Element
that is the DOM root of the component.
// inside a component specified in column.components.ColumnCell
const { domRef } = useInfiniteColumnCell<DATA_TYPE>();
return <div ref={domRef}>
...
</div>
Also you have to make sure you spread all other props
you receive in the component, as they are HTMLProps
that need to end-up in the DOM (eg: className
for theming and default styles, etc).
Both components.ColumnCell
and components.HeaderCell
need to be declared with props
being of type HTMLProps<HTMLDivElement>
.
import * as React from 'react'; import { InfiniteTable, DataSource, DataSourceGroupBy, InfiniteTablePropGroupColumn, useInfiniteColumnCell, useInfiniteHeaderCell, InfiniteTablePropColumnTypes, } from '@infinite-table/infinite-react'; import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react'; type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; currency: string; preferredLanguage: string; stack: string; canDesign: 'yes' | 'no'; hobby: string; salary: number; age: number; }; const dataSource = () => { return fetch( 'https://infinite-table.com/.netlify/functions/json-server' + '/developers1k' ) .then((r) => r.json()) .then((data: Developer[]) => data); }; const DefaultHeaderComponent: React.FunctionComponent< React.HTMLProps<HTMLDivElement> > = (props) => { const { column, domRef, columnSortInfo } = useInfiniteHeaderCell<Developer>(); const style = { ...props.style, border: '1px solid #fefefe', }; let sortTool = ''; switch (columnSortInfo?.dir) { case undefined: sortTool = '👉'; break; case 1: sortTool = '👇'; break; case -1: sortTool = '☝🏽'; break; } return ( <div ref={domRef} {...props} style={style}> {/* here you would usually have: */} {/* {props.children} {sortTool} */} {/* but in this case we want to override the default sort tool as well (which is part of props.children) */} {column.field} {sortTool} </div> ); }; const StackComponent: React.FunctionComponent< React.HTMLProps<HTMLDivElement> > = (props) => { const { value, domRef } = useInfiniteColumnCell<Developer>(); const isFrontEnd = value === 'frontend'; const emoji = isFrontEnd ? '⚛️' : '💽'; const style = { padding: '5px 20px', border: `1px solid ${isFrontEnd ? 'red' : 'green'}`, ...props.style, }; return ( <div ref={domRef} {...props} style={style}> {props.children} <div style={{ flex: 1 }} /> {emoji} </div> ); }; const columnTypes: InfiniteTablePropColumnTypes<Developer> = { default: { // override all columns to use these components components: { HeaderCell: DefaultHeaderComponent, }, }, }; const columns: InfiniteTablePropColumns<Developer> = { id: { field: 'id', defaultWidth: 80 }, stack: { field: 'stack', renderValue: ({ data }) => 'Stack: ' + data?.stack, components: { HeaderCell: DefaultHeaderComponent, ColumnCell: StackComponent, }, }, firstName: { field: 'firstName', }, preferredLanguage: { field: 'preferredLanguage', }, }; export default function ColumnValueGetterExample() { return ( <> <DataSource<Developer> primaryKey="id" data={dataSource}> <InfiniteTable<Developer> columns={columns} columnTypes={columnTypes} /> </DataSource> </> ); }
Note
If you’re using the useInfiniteColumnCell
hook inside the columns.render
or columns.renderValue
functions (and not as part of a custom component in columns.components.ColumnCell
), you don’t need to pass on the domRef
to the root of the DOM you’re rendering (same is true if you’re using useInfiniteHeaderCell
inside the columns.header
function).