Column Headers

Column headers have the same level of customization as column cells - you can fully control what is being rendered and when. Here's a summary of the things you can do in the column header:

  • customize the header label of a column
  • specify custom sort icon
  • configure and customize the menu icon
  • configure the column selection chechbox (for columns configured to display a selection checkbox)
  • customize the order of all of the above, and select which ones should be included

Column Header Label

By default, the label displayed for the column header is the field the column is bound to. If you want to customize this, use the header property.

type Developer = {
  id: string;
  firstName: string;
  lastName: string;
  age: number;
};
const columns: InfiniteTablePropColumns<Developer> = {
  id: {
    field: 'id', // will be used as default label in column header
    defaultWidth: 100,
  },
  name: {
    header: 'First and Last Name', // custom column header label
    valueGetter: ({ data }) => `${data.firstName} ${data.lastName}`,
  },
};
Simple table with both default and custom column headers
View Mode
Fork
import {
  InfiniteTable,
  DataSource,
  InfiniteTablePropColumns,
} from '@infinite-table/infinite-react';

import * as React from 'react';

const columns: InfiniteTablePropColumns<Developer> = {
  id: {
    field: 'id', // will be used as default label in column header
    defaultWidth: 100,
  },
  name: {
    header: 'First and Last Name', // custom column header label
    valueGetter: ({ data }) => `${data.firstName} ${data.lastName}`,
  },
};

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  age: number;
};

const dataSource = () => {
  return fetch('https://infinite-table.com/.netlify/functions/json-server' + '/developers1k')
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const domProps = {
  style: {
    minHeight: 300,
  },
};
export default function App() {
  return (
    <DataSource<Developer> primaryKey="id" data={dataSource}>
      <InfiniteTable<Developer> domProps={domProps} columns={columns} />
    </DataSource>

Having the header property be a strin value is useful but when you want more flexibility, you can use a function instead.

When the column header is a function, it is called with an object that contains the following properties:

  • column - the current column object. NOTE: it's not the same as the column object you passed to the columns prop - but rather an enhanced version of that, which contains additional properties and computed values. It is called a "computed" column - typed as InfiniteTableComputedColumn<DATA_TYPE>.
  • columnsMap - a map of all computed columns available in the table, keyed by the column id. This is useful if at runtime you need access to other columns in the table. NOTE: this map does not contain only the visible columns, but rather ALL the columns.
  • columnSortInfo - the sorting information for the current column, or null if the column is not sorted.
  • api - a reference to the table API object.
  • columnApi - a reference to the table Column API object for bound to the current column.
  • allRowsSelected: boolean
  • someRowsSelected: boolean
  • renderBag - more on that below - used to reference changes between the different render functions of the column header (those functions are the column header rendering pipeline described in the next section).

Note

All the render props exposed for the rendering pipeline of the column header are called with the same object as the first argument.

Having the column header as a function and having access to the state of the column and of the table allows you to create very dynamic column headers that accurately reflect column state.

Column Header Rendering Pipeline

The rendering pipeline of the column header is similar to the one of the column cells.

It's a series of functions defined on the column that are called while rendering elements found in the column header (the header label, the sort and menu icons, the filtering icon, the selection checkbox).

All of the functions that are part of the column header rendering pipeline are called with the same object as the first argument - the shape of this object is described in the previous section.

If you want to customize any of the above, use the corresponding function.

For even more control, the last function in the pipeline that gets called is the column.renderHeader function.

This function is called with the same object as the first argument, but it also has a renderBag property that contains the result of all the previous functions in the pipeline (eg: renderBag.sortIcon - the result of the renderSortIcon call, renderBag.filterIcon - the result of the renderFilterIcon call, etc).

So if you specify a custom renderHeader function, it's up to you to use the results of the previous functions in the pipeline, in order to fully take control of the column header.

Available properties on the renderBag

The renderBag object contains the following properties available to the render functions of the column header:

  • header - the label of the column header.
  • sortIcon - the default sort icon
  • filterIcon - the filter icon - displayed when the current column is used in filtering
  • filterEditor - the current filter editor
  • menuIcon - the menu icon that can be clicked to open the column menu
  • selectionCheckBox - the selection check box - displays the current selection status and controls the selection for all rows.
  • all - all of the above combined together in a React.Fragment.

Customizing the Sort Icon

For customizing the sort icon, use the column.renderSortIcon function.

Inside that function you can either use the object passed as a parameter to get information about the sort state of the column

Customizing_the_column_sort_icon
renderSortIcon({ columnSortInfo }) {
  if (!columnSortInfo) {
    return ' 🤷‍♂️';
  }
  return columnSortInfo.dir === 1 ? '▲' : '▼';
}

or you can use the useInfiniteHeaderCell hook to get the same information.

Customizing_the_column_sort_icon
import {
  useInfiniteHeaderCell,
} from '@infinite-table/infinite-react';

/// ...

renderSortIcon(){
  const { columnSortInfo } = useInfiniteHeaderCell();
  if (!columnSortInfo) {
    return ' 🤷‍♂️';
  }
  return columnSortInfo.dir === 1 ? '▲' : '▼';
},
Custom sort icon for the name column
View Mode
Fork
import {
  InfiniteTable,
  DataSource,
  InfiniteTablePropColumns,
  useInfiniteHeaderCell,
} from '@infinite-table/infinite-react';

import * as React from 'react';

const columns: InfiniteTablePropColumns<Developer> = {
  id: {
    field: 'id', // will be used as default label in column header
    defaultWidth: 100,
  },
  name: {
    header: 'Name', // custom column header label
    valueGetter: ({ data }) => `${data.firstName} ${data.lastName}`,
    renderSortIcon: () => {
      const { columnSortInfo } = useInfiniteHeaderCell(); // eslint-disable-line
      if (!columnSortInfo) {
        return ' 🤷‍♂️';
      }
      return columnSortInfo.dir === 1 ? '▲' : '▼';
    },
  },
};

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  age: number;
};

const dataSource = () => {
  return fetch('https://infinite-table.com/.netlify/functions/json-server' + '/developers1k')
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const domProps = {
  style: {
    minHeight: 300,
  },
};
export default function App() {
  return (
    <DataSource<Developer> primaryKey="id" data={dataSource}>
      <InfiniteTable<Developer> domProps={domProps} columns={columns} />
    </DataSource>

Customizing the Menu Icon

For customizing the menu icon, use the column.renderMenuIcon function.

Inside that function you can either use the object passed as a parameter to get information about the column

Customizing_the_menu_icon
renderMenuIcon({ column }) {
  return `🔧 ${column.id}`;
}

or you can use the useInfiniteHeaderCell hook to get the same information.

Customizing_the_menu_icon
import {
  useInfiniteHeaderCell,
} from '@infinite-table/infinite-react';

/// ...

renderMenuIcon(){
  const { column } = useInfiniteHeaderCell();
  return `🔧 ${column.id}`;
},
Custom menu icon for the name and age columns

Hover over the header for the Name and Age columns to see the custom menu icon.

Also, the id column has renderMenuIcon: false set, so it doesn't show a column menu at all.

View Mode
Fork
import {
  InfiniteTable,
  DataSource,
  InfiniteTablePropColumns,
} from '@infinite-table/infinite-react';

import * as React from 'react';

const columns: InfiniteTablePropColumns<Developer> = {
  id: {
    field: 'id',
    defaultWidth: 80,
    renderMenuIcon: false,
  },

  name: {
    header: 'Name', // custom column header label
    valueGetter: ({ data }) => `${data.firstName} ${data.lastName}`,

    // custom menu icon
    renderMenuIcon: () => <div>🌎</div>,
  },
  age: {
    field: 'age',
    header: 'Age',
    renderMenuIcon: ({ column }) => {
      return `🔧 ${column.id}`;
    },
  },
};

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  age: number;
};

const dataSource = () => {
  return fetch('https://infinite-table.com/.netlify/functions/json-server' + '/developers1k')
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const domProps = {
  style: {
    minHeight: 300,
  },
};
export default function App() {
  return (
    <DataSource<Developer> primaryKey="id" data={dataSource}>
      <InfiniteTable<Developer> domProps={domProps} columns={columns} />
    </DataSource>

Note

If you don't want to show a column menu (icon) at all, you can set the column.renderMenuIcon prop to false.

Also, see the column.renderMenuIcon docs for an example on how to use the api to open the column menu.

Customizing the Filter Icon

For customizing the filter icon, use the column.renderFilterIcon function.

Inside that function you can either use the object passed as a parameter to get information about the filtered state of the column

Customizing_the_filter_icon
renderFilterIcon({ filtered }) {
  return filtered ? '🔍' : '';
}

or you can use the useInfiniteHeaderCell hook to get the same information.

Customizing_the_menu_icon
import {
  useInfiniteHeaderCell,
} from '@infinite-table/infinite-react';

/// ...

renderMenuIcon(){
  const { filtered } = useInfiniteHeaderCell();
  return filtered ? '🔥' : '';
},

In addition, you can use the filtered property in the column.header function to determine if the column is filtered or not and render a different header label.

Note

If specified, the column.renderFilterIcon function prop is called even if the column is not currently filtered.

Custom filter icons for salary and name columns

The salary column will show a bolded label when filtered.

The firstName column will show a custom filter icon when filtered.

View Mode
Fork
import * as React from 'react';

import {
  DataSourceData,
  InfiniteTable,
  InfiniteTablePropColumns,
  DataSource,
} from '@infinite-table/infinite-react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;

  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';

  salary: number;
};

const data: DataSourceData<Developer> = () => {
  return fetch('https://infinite-table.com/.netlify/functions/json-server' + `/developers1k-sql?`)
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const columns: InfiniteTablePropColumns<Developer> = {
  id: {
    field: 'id',
    type: 'number',
    defaultWidth: 100,
  },
  salary: {
    field: 'salary',
    type: 'number',
    header: ({ filtered }) => {
      return filtered ? <b>Salary</b> : 'Salary';
    },

    renderFilterIcon: () => {
      return null;
    },
  },

  firstName: {
    field: 'firstName',
    renderFilterIcon: ({ filtered }) => {
      return filtered ? '🔥' : '';
    },
  },
  stack: { field: 'stack' },
  currency: { field: 'currency' },
};

const domProps = {
  style: {
    height: '100%',
  },
};
export default () => {
  return (
    <>
      <React.StrictMode>
        <DataSource<Developer>
          data={data}
          primaryKey="id"
          defaultFilterValue={[]}
          filterDelay={0}
          filterMode="local"
        >
          <InfiniteTable<Developer>
            domProps={domProps}
            columnDefaultWidth={150}
            columnMinWidth={50}
            columns={columns}
          />
        </DataSource>
      </React.StrictMode>
    </>

Changing the display of filters

Infinite Table allows very deep cusstomization of the column header, including the filters.

For example, you might not want to display the column filters under the column header, but rather in a separate menu popover.

This section shows how to do that. You can use showColumnFilters=false to hide the filters from under the column header.

Next, you can use the column.renderHeader function to render a custom filter icon that opens a filter popover when clicked.

You don't need to re-implement the filter editor, you have acces to it via the renderBag.filterEditor property. The code below shows how to do this.

Custom display of column filters
View Mode
Fork
import * as React from 'react';

import {
  DataSourceData,
  InfiniteTable,
  InfiniteTablePropColumns,
  DataSource,
  InfiniteTableColumn,
  useInfiniteHeaderCell,
  alignNode,
  useInfinitePortalContainer,
} from '@infinite-table/infinite-react';
import { createPortal } from 'react-dom';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;

  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';

  salary: number;
};

const data: DataSourceData<Developer> = () => {
  return fetch('https://infinite-table.com/.netlify/functions/json-server' + `/developers1k-sql?`)
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const FilterIcon = () => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    width="16"
    height="16"
    viewBox="0 0 24 24"
    fill="none"
    stroke="currentColor"
    stroke-width="2"
    stroke-linecap="round"
    stroke-linejoin="round"
  >
    <polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
  </svg>
);

function ColumnFilterMenuIcon() {
  const {
    renderBag,
    htmlElementRef: alignToRef,
    column,
  } = useInfiniteHeaderCell();

  const portalContainer = useInfinitePortalContainer();

  const [visible, setVisible] = React.useState(false);

  React.useEffect(() => {
    if (!domRef.current || !alignToRef.current) {
      return;
    }

    alignNode(domRef.current, {
      alignTo: alignToRef.current,
      alignPosition: [['TopRight', 'BottomRight']],
    });
  });

  const domRef = React.useRef<HTMLDivElement>(null);

  return (
    <div
      onMouseDown={(e) => {
        if (e.nativeEvent) {
          // @ts-ignore
          e.nativeEvent.__insideMenu = column.id;
        }
      }}
      onPointerDown={(event) => {
        event.stopPropagation();

        if (visible) {
          setVisible(false);
          return;
        }

        setVisible(true);

        function handleMouseDown(event: MouseEvent) {
          // @ts-ignore
          if (event.__insideMenu !== column.id) {
            setVisible(false);
            document.documentElement.removeEventListener(
              'mousedown',
              handleMouseDown,
            );
          }
        }
        document.documentElement.addEventListener('mousedown', handleMouseDown);
      }}
    >
      <FilterIcon />
      {createPortal(
        <div
          ref={domRef}
          style={{
            position: 'absolute',
            color: 'white',
            top: 0,
            overflow: 'visible',
            background: 'var(--infinite-background)',
            border: `var(--infinite-cell-border)`,
            left: 0,
            width: column.computedWidth,
            padding: 10,
            display: visible ? 'block' : 'none',
          }}
        >
          {renderBag.filterEditor}
        </div>,
        portalContainer!,
      )}
    </div>
  );
}

const customHeaderWithFilterMenu: InfiniteTableColumn<any>['renderHeader'] = ({
  renderBag,
}) => {
  return (
    <>
      {renderBag.header}
      <div style={{ flex: 1 }}></div>

      {renderBag.filterIcon}

      {renderBag.menuIcon}
      <ColumnFilterMenuIcon />
    </>
  );
};

const columns: InfiniteTablePropColumns<Developer> = {
  id: {
    field: 'id',
    type: 'number',
    defaultWidth: 100,
    renderHeader: customHeaderWithFilterMenu,
  },
  salary: {
    field: 'salary',
    type: 'number',
    renderHeader: customHeaderWithFilterMenu,
  },

  firstName: {
    field: 'firstName',
    renderHeader: customHeaderWithFilterMenu,
  },
  stack: { field: 'stack', renderHeader: customHeaderWithFilterMenu },
  currency: { field: 'currency', renderHeader: customHeaderWithFilterMenu },
};

export default () => {
  return (
    <>
      <React.StrictMode>
        <DataSource<Developer>
          data={data}
          primaryKey="id"
          defaultFilterValue={[]}
          shouldReloadData={{
            filterValue: false,
            sortInfo: false,
            groupBy: false,
            pivotBy: false,
          }}
        >
          <InfiniteTable<Developer>
            showColumnFilters={false}
            columnDefaultWidth={150}
            columnMinWidth={50}
            columns={columns}
          />
        </DataSource>
      </React.StrictMode>
    </>

Customizing the Selection Checkbox

For customizing the selection checkbox in the column header, use the column.renderHeaderSelectionCheckBox function.

Note

If you want another column, other than the group column, to show a selection checkbox, you have to also set the column.renderSelectionCheckBox prop to true.

Custom header checkbox selection for columns

The group column, as well as the stack column display a custom selection checkbox in the column header.

View Mode
Fork
import { InfiniteTable, DataSource } from '@infinite-table/infinite-react';
import type {
  InfiniteTableProps,
  InfiniteTablePropColumns,
  DataSourceProps,
} from '@infinite-table/infinite-react';
import * as React from 'react';

const columns: InfiniteTablePropColumns<Developer> = {
  country: {
    field: 'country',
  },
  firstName: {
    field: 'firstName',
    defaultHiddenWhenGroupedBy: '*',
  },
  stack: {
    renderSelectionCheckBox: true,
    renderHeaderSelectionCheckBox: ({ renderBag }) => {
      // render the default value and decorate it
      return <b>[{renderBag.selectionCheckBox}]</b>;
    },
    field: 'stack',
  },
  age: { field: 'age' },
  id: { field: 'id' },
  preferredLanguage: {
    field: 'preferredLanguage',
  },
  canDesign: {
    field: 'canDesign',
  },
};

const defaultGroupBy: DataSourceProps<Developer>['groupBy'] = [
  {
    field: 'canDesign',
  },
  {
    field: 'stack',
  },
  {
    field: 'preferredLanguage',
  },
];

const groupColumn: InfiniteTableProps<Developer>['groupColumn'] = {
  field: 'firstName',
  renderHeaderSelectionCheckBox: ({ renderBag }) => {
    // render the default value and decorate it
    return <b>[{renderBag.selectionCheckBox}]</b>;
  },
  defaultWidth: 300,
};

const domProps = {
  style: {
    flex: 1,
    minHeight: 500,
  },
};

export default function App() {
  return (
    <DataSource<Developer>
      data={dataSource}
      groupBy={defaultGroupBy}
      selectionMode="multi-row"
      primaryKey="id"
    >
      <InfiniteTable<Developer>
        columns={columns}
        domProps={domProps}
        groupColumn={groupColumn}
        columnDefaultWidth={150}
      />
    </DataSource>
  );
}

const dataSource = () => {
  return fetch('https://infinite-table.com/.netlify/functions/json-server' + '/developers100')
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

type Developer = {
  id