Inline Editing

By default, inline editing is not enabled.

To enable inline editing globally, you can use the columnDefaultEditable boolean prop on the InfiniteTable component. This will enable the editing on all columns.

Or you can be more specific and choose to make individual columns editable via the column.defaultEditable prop. This overrides the global columnDefaultEditable.

Inline Editing in action

All columns (except id) are editable.

View Mode
Fork
import {
  InfiniteTable,
  DataSource,
  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 = () => {
  return fetch('https://infinite-table.com/.netlify/functions/json-server' + '/developers100')
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const columns: InfiniteTablePropColumns<Developer> = {
  id: { field: 'id', defaultWidth: 80, defaultEditable: false },
  stack: {
    field: 'stack',
    contentFocusable: true,
    header: 'Stack',
  },
  firstName: {
    field: 'firstName',
    header: 'Name',
  },
  age: {
    field: 'age',
    header: 'Age',
  },
  hobby: {
    field: 'hobby',
    header: 'Hobby',
  },
  preferredLanguage: {
    header: 'Language',
    field: 'preferredLanguage',
  },
};

export default function ColumnValueGetterExample() {
  return (
    <>
      <DataSource<Developer> primaryKey="id" data={dataSource}>
        <InfiniteTable<Developer> columns={columns} columnDefaultEditable />
      </DataSource>
    </>

Note

The column.defaultEditable property can be either a boolean or a function.

If it is a function, it will be called when an edit is triggered on the column. The function will be called with a single object that contains the following properties:

  • value - the current value of the cell (the value currently displayed, so after columns.valueFormatter and columns.renderValue have been applied)
  • rawValue - the current value of the cell, but before any formatting and custom rendering has been applied. This is either the field value from the current data object, or the result of the column valueGetter function.
  • data - the data object (of type DATA_TYPE) for the current row
  • rowInfo - the row info object that underlies the row
  • column - the current column on which editing is invoked
  • api - a reference to the InfiniteTable API
  • dataSourceApi - - a reference to the DataSource API

The function can return a boolean value or a Promise that resolves to a boolean value - this means you can asynchronously decide whether the cell is editable or not.

Making column.defaultEditable a function gives you the ability to granularly control which cells are editable or not (even within the same column, based on the cell value or other values you have access to).

In addition to the flags mentioned above, you can use the editable prop on the InfiniteTable component. This overrides all other properties and when it is defined, is the only source of truth for whether something is editable or not.

Note

The editable prop allows you to centralize editing logic in one place.

It has the same signature as the column.defaultEditable function.

Editing Flow Chart

A picture is worth a thousand words - see a chart for the editing flow.

Start Editing

Editing can be started either by user interaction or programmatically via the API.

The user can start editing by double-clicking on a cell or by pressing the Enter key while the cell is active (see Keyboard Navigation for Cells).

To start editing programmatically, use the startEdit({ columnId, rowIndex }) method.

Starting an Edit via the API
View Mode
Fork
import {
  InfiniteTable,
  DataSource,
  InfiniteTablePropColumns,
  InfiniteTableApi,
} from '@infinite-table/infinite-react';
import { useCallback, useRef, useState } from 'react';

type Developer = {
  id: number;
  firstName: string;
  currency: string;
  stack: string;
  hobby: string;
  salary: string;
};
const dataSource: Developer[] = [
  {
    id: 1,
    firstName: 'John',
    currency: 'USD',
    stack: 'frontend',
    hobby: 'gaming',
    salary: 'USD 1000',
  },
  {
    id: 2,
    firstName: 'Jane',
    currency: 'EUR',
    stack: 'backend',
    hobby: 'reading',
    salary: 'EUR 2000',
  },
  {
    id: 3,
    firstName: 'Jack',
    currency: 'GBP',
    stack: 'frontend',
    hobby: 'gaming',
    salary: 'GBP 3000',
  },
  {
    id: 4,
    firstName: 'Jill',
    currency: 'USD',
    stack: 'backend',
    hobby: 'reading',
    salary: 'USD 4000',
  },
];

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

  salary: {
    defaultWidth: 320,
    field: 'salary',
    header: 'Salary - edit accepts numbers only',
    style: { color: 'tomato' },
    getValueToEdit: ({ value }) => {
      return parseInt(value.substr(4), 10);
    },
    getValueToPersist: ({ value, data }) => {
      return `${data!.currency} ${parseInt(value, 10)}`;
    },
    shouldAcceptEdit: ({ value }) => {
      return parseInt(value, 10) == value;
    },
  },

  firstName: {
    field: 'firstName',
    header: 'Name',
  },
  currency: {
    field: 'currency',
    header: 'Currency',
  },
};

export default function InlineEditingExample() {
  const [activeRowIndex, setActiveRowIndex] = useState<number>(2);

  const apiRef = useRef<InfiniteTableApi<Developer> | null>(null);
  const onReady = useCallback(
    ({ api }: { api: InfiniteTableApi<Developer> }) => {
      apiRef.current = api;
    },
    [],
  );
  return (
    <>
      <button
        style={{
          border: '1px solid magenta',
          margin: 2,
          padding: 10,
          background: 'var(--infinite-background)',
          color: 'var(--infinite-cell-color)',
        }}
        onClick={() => {
          apiRef.current!.startEdit({
            rowIndex: activeRowIndex,
            columnId: 'salary',
          });
        }}
      >
        Edit the salary column for active row
      </button>

      <DataSource<Developer> primaryKey="id" data={dataSource}>
        <InfiniteTable<Developer>
          onReady={onReady}
          columns={columns}
          columnDefaultEditable
          activeRowIndex={activeRowIndex}
          onActiveRowIndexChange={setActiveRowIndex}
        />
      </DataSource>
    </>

Either way, be it user interaction or API call, those actions will trigger checks to see if the cell is editable - taking into account the columnDefaultEditable, column.defaultEditable or editable props, as described in the paragraphs above. Only if the result is true will the cell editor be displayed.

Customize Edit Value When Editing Starts

When editing starts, the column editor is displayed with the value that was in the cell. This (initial) edit value can be customized via the column.getValueToEdit prop. This allows you to start editing with a different value than the one that is displayed in the cell - and even with a value fetched asynchronously.


const columns = {
  salary: {
    field: 'salary',
    // this can return a Promise
    getValueToEdit: ({ value, data, rowInfo, column}) => {
      // suppose the value is a string like '$1000'
      // but we want to start editing with the number 1000
      return value.replace('$', '');
    }
  }
}
Inline Editing with custom getter for edit value

Try editing the salary column - it has a custom getter for the edit value, which removes the curency string.

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

type Developer = {
  id: number;
  firstName: string;
  currency: string;
  stack: string;
  hobby: string;
  salary: string;
};
const dataSource: Developer[] = [
  {
    id: 1,
    firstName: 'John',
    currency: 'USD',
    stack: 'frontend',
    hobby: 'gaming',
    salary: 'USD 1000',
  },
  {
    id: 2,
    firstName: 'Jane',
    currency: 'EUR',
    stack: 'backend',
    hobby: 'reading',
    salary: 'EUR 2000',
  },
  {
    id: 3,
    firstName: 'Jack',
    currency: 'GBP',
    stack: 'frontend',
    hobby: 'gaming',
    salary: 'GBP 3000',
  },
  {
    id: 4,
    firstName: 'Jill',
    currency: 'USD',
    stack: 'backend',
    hobby: 'reading',
    salary: 'USD 4000',
  },
];

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

  salary: {
    defaultWidth: 320,
    field: 'salary',
    header: 'Salary - edit accepts numbers only',
    style: { color: 'tomato' },
    getValueToEdit: ({ value }) => {
      return parseInt(value.substr(4), 10);
    },
    getValueToPersist: ({ value, data }) => {
      return `${data!.currency} ${parseInt(value, 10)}`;
    },
    shouldAcceptEdit: ({ value }) => {
      return parseInt(value, 10) == value;
    },
  },

  firstName: {
    field: 'firstName',
    header: 'Name',
  },
  currency: {
    field: 'currency',
    header: 'Currency',
  },
};

export default function InlineEditingExample() {
  return (
    <>
      <DataSource<Developer> primaryKey="id" data={dataSource}>
        <InfiniteTable<Developer> columns={columns} columnDefaultEditable />
      </DataSource>
    </>

Finishing an Edit

An edit is generally finished by user interaction - either the user confirms the edit by pressing the Enter key or cancels it by pressing the Escape key.

As soon as the edit is confirmed by the user, the InfiniteTable needs to decide whether the edit should be accepted or not.

In order to decide (either synchronously or asynchronously) whether an edit should be accepted or not, you can use the global shouldAcceptEdit prop or the column-level column.shouldAcceptEdit alternative.

Note

When neither the global shouldAcceptEdit nor the column-level column.shouldAcceptEdit are defined, all edits are accepted by default.

Note

Once an edit is accepted, the onEditAccepted callback prop is called, if defined.

When an edit is rejected, the onEditRejected callback prop is called instead.

The accept/reject status of an edit is decided by using the shouldAcceptEdit props described above. However an edit can also be cancelled by the user pressing the Escape key in the cell editor - to be notified of this, use the onEditCancelled callback prop.

Using shouldAcceptEdit to decide whether a value is acceptable or not

In this example, the salary column is configured with a shouldAcceptEdit function property that rejects non-numeric values.

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

type Developer = {
  id: number;
  firstName: string;
  currency: string;
  stack: string;
  hobby: string;
  salary: string;
};
const dataSource: Developer[] = [
  {
    id: 1,
    firstName: 'John',
    currency: 'USD',
    stack: 'frontend',
    hobby: 'gaming',
    salary: 'USD 1000',
  },
  {
    id: 2,
    firstName: 'Jane',
    currency: 'EUR',
    stack: 'backend',
    hobby: 'reading',
    salary: 'EUR 2000',
  },
  {
    id: 3,
    firstName: 'Jack',
    currency: 'GBP',
    stack: 'frontend',
    hobby: 'gaming',
    salary: 'GBP 3000',
  },
  {
    id: 4,
    firstName: 'Jill',
    currency: 'USD',
    stack: 'backend',
    hobby: 'reading',
    salary: 'USD 4000',
  },
];

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

  salary: {
    defaultWidth: 320,
    field: 'salary',
    header: 'Salary - edit accepts numbers only',
    style: { color: 'tomato' },
    getValueToEdit: ({ value }) => {
      return parseInt(value.substr(4), 10);
    },
    getValueToPersist: ({ value, data }) => {
      return `${data!.currency} ${parseInt(value, 10)}`;
    },
    shouldAcceptEdit: ({ value }) => {
      return parseInt(value, 10) == value;
    },
  },

  firstName: {
    field: 'firstName',
    header: 'Name',
  },
  currency: {
    field: 'currency',
    header: 'Currency',
  },
};

export default function InlineEditingExample() {
  return (
    <>
      <DataSource<Developer> primaryKey="id" data={dataSource}>
        <InfiniteTable<Developer> columns={columns} columnDefaultEditable />
      </DataSource>
    </>

Persisting an Edit

By default, accepted edits are persisted to the DataSource via the DataSourceAPI.updateData method.

To change how you persist values (which might include persisting to remote locations), use the persistEdit function prop on the InfiniteTable component.

Note

The persistEdit function prop can return a Promise for async persistence. To signal that the persisting failed, reject the promise or resolve it with an Error object.

After persisting the edit, if all went well, the onEditPersistSuccess callback prop is called. If the persisting failed (was rejected), the onEditPersistError callback prop is called instead.