Monthly Update - July 2022

By Infinite Table Admin·

In July, we’ve been hard at work preparing for our Autumn release.

We have implemented a few new functionalities:

And we have updated some of the existing features:

  • lazy grouping
    • expands lazy loaded rows correctly and
    • also the server response can contain multiple levels of children, which basically allows the backend to send more data for groups you don’t want to load lazily
  • column groups are now improved with support for proportional column resizing
  • pivot columns are now easier to style and customize

Coming soon

At the end of July we started working on row and cell selection and we’ve made good progress on it.

Row selection is already implemented for non-lazy group data and we’re working on integrating it with lazy group data (e.g groups lazily loaded from the server). Of course, it will have integration with checkbox selection.

Multiple row selection will have 2 ways to select data:

  • via mouse/keyboard interaction - we’ve emulated the behavior you’re used to from your Finder in MacOS.
  • via checkbox - this is especially useful when the table is configured with grouping.

New Features

Column Resizing

By default columns are now resizable. You can control this at column level via column.resizable or at grid level via resizableColumns.

Find out more on column resizing

Read more about how you can configure column resizing to fit your needs.

Resizable columns example
For resizable columns, hover the mouse between column headers to grab & drag the resize handle.

Hold SHIFT when grabbing in order to share space on resize.

View Mode
Fork
import * as React from 'react';
import {
  InfiniteTable,
  DataSource,
  InfiniteTableColumn,
} from '@infinite-table/infinite-react';
import { useState } from 'react';

export const columns: Record<
  string,
  InfiniteTableColumn<Employee>
> = {
  firstName: {
    field: 'firstName',
    header: 'First Name',
  },
  country: {
    field: 'country',
    header: 'Country',
  },
  city: {
    field: 'city',
    header: 'City',
  },
  salary: {
    field: 'salary',
    type: 'number',
    header: 'Salary',
  },
};

export default function App() {
  const [resizableColumns, setResizableColumns] =
    useState(true);
  return (
    <>
      <div style={{ color: 'var(--infinite-cell-color)' }}>
        <button
          style={{
            padding: 10,
            border: '2px solid currentColor',
          }}
          onClick={() => setResizableColumns((r) => !r)}>
          Click to toggle
        </button>

        <p style={{ padding: 10 }}>
          Columns are currently{' '}
          {resizableColumns ? 'resizable' : 'NOT RESIZABLE'}
          .
        </p>
      </div>
      <DataSource<Employee>
        data={dataSource}
        primaryKey="id">
        <InfiniteTable<Employee>
          resizableColumns={resizableColumns}
          columns={columns}
          columnDefaultWidth={100}
          columnMinWidth={30}
        />
      </DataSource>
    </>
  );
}

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

export type Employee = {
  id: number;
  companyName: string;
  companySize: string;
  firstName: string;
  lastName: string;
  country: string;
  countryCode: string;
  city: string;
  streetName: string;
  streetNo: string;
  department: string;
  team: string;
  salary: number;
  age: number;
  email: string;
};

A nice feature is support for SHIFT resizing - which will share space on resize between adjacent columns - try it in the example above.

Column Reordering

Read more on column order

Column order is a core functionality of InfiniteTable - read how you can leverage it in your app.

The default column order is the order in which columns appear in the columns object, but you can specify a defaultColumnOrder or tightly control it via the controlled property columnOrder - use onColumnOrderChange to get notifications when columns are reordered by the user.

Column order
View Mode
Fork
import * as React from 'react';
import {
  InfiniteTable,
  DataSource,
  InfiniteTablePropColumns,
} from '@infinite-table/infinite-react';
import { useState } from 'react';

export type Employee = {
  id: number;
  companyName: string;
  companySize: string;
  firstName: string;
  lastName: string;
  country: string;
  countryCode: string;
  city: string;
  streetName: string;
  streetNo: string;
  department: string;
  team: string;
  salary: number;
  age: number;
  email: string;
};

export const columns: InfiniteTablePropColumns<Employee> = {
  firstName: {
    field: 'firstName',
    header: 'First Name',
  },
  country: {
    field: 'country',
    header: 'Country',
    columnGroup: 'location',
  },

  city: {
    field: 'city',
    header: 'City',
    columnGroup: 'address',
  },
  salary: {
    field: 'salary',
    type: 'number',
    header: 'Salary',
  },
  department: {
    field: 'department',
    header: 'Department',
  },
  team: {
    field: 'team',
    header: 'Team',
  },
  company: { field: 'companyName', header: 'Company' },

  companySize: {
    field: 'companySize',
    header: 'Company Size',
  },
};

export default function App() {
  const [columnOrder, setColumnOrder] = useState<
    string[] | true
  >([
    'firstName',
    'country',
    'team',
    'company',
    'firstName',
    'not existing column',
    'companySize',
  ]);

  return (
    <>
      <div style={{ color: 'var(--infinite-cell-color)' }}>
        <p>
          Current column order:{' '}
          <code>
            <pre>{JSON.stringify(columnOrder)}.</pre>
          </code>
        </p>
        <p>
          Note: if the column order contains columns that
          don't exist in the `columns` definition, they will
          be skipped.
        </p>

        <button
          style={{
            border: '2px solid currentColor',
            padding: 5,
          }}
          onClick={() => {
            setColumnOrder(['firstName', 'country']);
          }}>
          Click to only show "firstName" and "country".
        </button>

        <button
          style={{
            border: '2px solid currentColor',
            padding: 5,
            marginLeft: 5,
          }}
          onClick={() => {
            setColumnOrder(true);
          }}>
          Click to reset column order.
        </button>
      </div>

      <DataSource<Employee>
        data={dataSource}
        primaryKey="id">
        <InfiniteTable<Employee>
          columns={columns}
          columnOrder={columnOrder}
          onColumnOrderChange={setColumnOrder}
          columnDefaultWidth={200}
        />
      </DataSource>
    </>
  );
}

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

Keyboard Navigation

Both cell and row navigation is supported - use keyboardNavigation to configure it. By default, cell navigation is enabled.

Keyboard navigation

This example starts with cell [2,0] already active.

View Mode
Fork
import {
  InfiniteTable,
  DataSource,
  DataSourceData,
} from '@infinite-table/infinite-react';
import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react';
import * as React 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: 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> = {
  preferredLanguage: { field: 'preferredLanguage' },
  country: { field: 'country' },
  salary: {
    field: 'salary',
    type: 'number',
  },
  age: { field: 'age' },
  canDesign: { field: 'canDesign' },
  firstName: { field: 'firstName' },
  stack: { field: 'stack' },
  id: { field: 'id' },
  hobby: { field: 'hobby' },
  city: { field: 'city' },
  currency: { field: 'currency' },
};

const domProps = { style: { height: '90vh' } };

export default function KeyboardNavigationForCells() {
  return (
    <>
      <DataSource<Developer>
        primaryKey="id"
        data={dataSource}>
        <InfiniteTable<Developer>
          defaultActiveCellIndex={[2, 0]}
          columns={columns}
        />
      </DataSource>
    </>
  );
}

Updated Features

Lazy grouping

Server side grouping has support for lazy loading - InfiniteTable will automatically load lazy rows that are configured as expanded.

Lazy loaded rows are properly expanded

In this example, France is specified as expanded, so as soon as it is rendered, InfiniteTable will also request its children.

View Mode
Fork
import * as React from 'react';
import {
  InfiniteTable,
  DataSource,
  DataSourceData,
  InfiniteTablePropColumns,
  GroupRowsState,
  DataSourceGroupBy,
  DataSourcePropAggregationReducers,
} 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 aggregationReducers: DataSourcePropAggregationReducers<Developer> =
  {
    salary: {
      name: 'Salary (avg)',
      field: 'salary',
      reducer: 'avg',
    },
  };

const columns: InfiniteTablePropColumns<Developer> = {
  preferredLanguage: { field: 'preferredLanguage' },
  age: { field: 'age' },

  salary: {
    field: 'salary',
    type: 'number',
  },
  canDesign: { field: 'canDesign' },
  country: { field: 'country' },
  firstName: { field: 'firstName' },
  stack: { field: 'stack' },
  id: { field: 'id' },
  hobby: { field: 'hobby' },
  city: { field: 'city' },
  currency: { field: 'currency' },
};

const groupRowsState = new GroupRowsState({
  expandedRows: [['France']],
  collapsedRows: true,
});
export default function RemoteGroupByExample() {
  const groupBy: DataSourceGroupBy<Developer>[] =
    React.useMemo(() => [{ field: 'country' }], []);

  return (
    <DataSource<Developer>
      primaryKey="id"
      lazyLoad
      data={dataSource}
      groupBy={groupBy}
      defaultGroupRowsState={groupRowsState}
      aggregationReducers={aggregationReducers}>
      <InfiniteTable<Developer>
        columns={columns}
        columnDefaultWidth={220}
      />
    </DataSource>
  );
}

const dataSource: DataSourceData<Developer> = ({
  pivotBy,
  aggregationReducers,
  groupBy,

  lazyLoadStartIndex,
  lazyLoadBatchSize,

  groupKeys = [],
  sortInfo,
}) => {
  if (sortInfo && !Array.isArray(sortInfo)) {
    sortInfo = [sortInfo];
  }

  const startLimit: string[] = [];

  if (lazyLoadBatchSize && lazyLoadBatchSize > 0) {
    const start = lazyLoadStartIndex || 0;
    startLimit.push(`start=${start}`);
    startLimit.push(`limit=${lazyLoadBatchSize}`);
  }
  const args = [
    ...startLimit,
    pivotBy
      ? 'pivotBy=' +
        JSON.stringify(
          pivotBy.map((p) => ({ field: p.field }))
        )
      : null,
    `groupKeys=${JSON.stringify(groupKeys)}`,
    groupBy
      ? 'groupBy=' +
        JSON.stringify(
          groupBy.map((p) => ({ field: p.field }))
        )
      : null,
    sortInfo
      ? 'sortInfo=' +
        JSON.stringify(
          sortInfo.map((s) => ({
            field: s.field,
            dir: s.dir,
          }))
        )
      : null,

    aggregationReducers
      ? 'reducers=' +
        JSON.stringify(
          Object.keys(aggregationReducers).map((key) => ({
            field: aggregationReducers[key].field,
            id: key,
            name: aggregationReducers[key].reducer,
          }))
        )
      : null,
  ]
    .filter(Boolean)
    .join('&');
  return fetch(
    'https://infinite-table.com/.netlify/functions/json-server' +
      `/developers1k-sql?` +
      args
  ).then((r) => r.json());
};

Another nice feature is the ability for a group node to also contain its direct children in the server response, which basically allows the backend to eagerly load data for certain groups.

More on lazy grouping

Lazy grouping (with or without batching) is an advanced feature that allows you to integrate with huge datasets without loading them into the browser.

Column grouping

Column grouping was enhanced with support for pinned columns. Now you can use them in combination.

More on column groups

Column groups is a powerful way to arrange columns to fit your business requirements - read how easy it is to define them.

Column groups with pinning

Note the country column is pinned at the start of the table but is also part of a column group

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

import {
  InfiniteTable,
  DataSource,
  InfiniteTableColumnGroup,
  InfiniteTablePropColumnPinning,
} 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> = {
  currency: {
    field: 'currency',
    columnGroup: 'finance',
    maxWidth: 130,
  },
  salary: {
    field: 'salary',
    columnGroup: 'finance',
    maxWidth: 130,
  },
  country: {
    field: 'country',
    columnGroup: 'regionalInfo',
    maxWidth: 400,
  },
  preferredLanguage: {
    field: 'preferredLanguage',
    columnGroup: 'regionalInfo',
  },
  id: { field: 'id', defaultWidth: 80 },
  firstName: {
    field: 'firstName',
  },
  stack: {
    field: 'stack',
  },
};

const columnGrous: Record<
  string,
  InfiniteTableColumnGroup
> = {
  regionalInfo: {
    header: 'Regional Info',
  },
  finance: {
    header: 'Finance',
    columnGroup: 'regionalInfo',
  },
};

const defaultColumnPinning: InfiniteTablePropColumnPinning =
  {
    country: 'start',
  };
export default function ColumnGroupsWithPinningExample() {
  return (
    <>
      <DataSource<Developer>
        primaryKey="id"
        data={dataSource}>
        <InfiniteTable<Developer>
          columnGroups={columnGrous}
          columns={columns}
          columnDefaultWidth={100}
          defaultColumnPinning={defaultColumnPinning}
        />
      </DataSource>
    </>
  );
}

Pivoting

Pivot columns are now easier to style and benefit from piped rendering to allow maximum customization.

Pivoting docs

Pivoting is probably our most advanced use-case. We offer full support for server-side pivoting and aggregations.

Customized pivot columns

Pivot columns for the canDesign field are customized.

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

import {
  InfiniteTable,
  DataSource,
  DataSourceGroupBy,
  DataSourcePivotBy,
  InfiniteTableColumnAggregator,
  DataSourcePropAggregationReducers,
  InfiniteTableColumn,
} 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 },
  preferredLanguage: { field: 'preferredLanguage' },
  stack: { field: 'stack' },
};

const defaultGroupBy: DataSourceGroupBy<Developer>[] = [
  {
    field: 'country',
  },
  {
    field: 'city',
  },
];

const defaultPivotBy: DataSourcePivotBy<Developer>[] = [
  {
    field: 'stack',
  },
  {
    field: 'canDesign',
    column: {
      renderValue: ({ value }) => {
        return (
          <span style={{ color: 'tomato' }}>{value}</span>
        );
      },
      // use piped rendering - the renderBag object
      // contains the renderBag.value as returned by the `renderValue` fn
      // use `value` to decide if there was a real value or not to be rendered
      render: ({ value, renderBag }) => {
        return value == null ? '—' : renderBag.value;
      },
    },
    columnGroup: ({ columnGroup }) => {
      return {
        ...columnGroup,
        header:
          columnGroup.pivotGroupKey === 'yes'
            ? 'Designer'
            : 'Non-designer',
      };
    },
  },
];

const avgReducer: InfiniteTableColumnAggregator<
  Developer,
  any
> = {
  initialValue: 0,

  reducer: (acc, sum) => acc + sum,
  done: (sum, arr) =>
    Math.round(arr.length ? sum / arr.length : 0),
};

const aggregations: DataSourcePropAggregationReducers<Developer> =
  {
    salary: {
      ...avgReducer,
      name: 'Salary (avg)',
      field: 'salary',
    },
    age: {
      ...avgReducer,
      name: 'Age (avg)',
      field: 'age',
    },
  };

const groupColumn: InfiniteTableColumn<Developer> = {
  renderSelectionCheckBox: false,
};

export default function ColumnValueGetterExample() {
  return (
    <>
      <DataSource<Developer>
        primaryKey="id"
        defaultGroupBy={defaultGroupBy}
        defaultPivotBy={defaultPivotBy}
        aggregationReducers={aggregations}
        data={dataSource}>
        {({ pivotColumns, pivotColumnGroups }) => {
          return (
            <InfiniteTable<Developer>
              groupRenderStrategy="single-column"
              groupColumn={groupColumn}
              columns={columns}
              columnDefaultWidth={200}
              pivotColumns={pivotColumns}
              pivotColumnGroups={pivotColumnGroups}
              pivotTotalColumnPosition="start"
            />
          );
        }}
      </DataSource>
    </>
  );
}