mantine-datatable icon indicating copy to clipboard operation
mantine-datatable copied to clipboard

Inline cell editing

Open mikkurogue opened this issue 1 year ago • 4 comments

In our implementation, we want to be able to add records to the array inline from the datatable, think of a structure of the table and at the bottom an "empty" row that works as a button that adds a new entry to the records. The way we do this now is that we use an unstyled button and use a transparent styling to indicate "interactivity".

However, what we can't yet do or figure out is inline cell value editing. I understand we can handle cell clicks but I'd rather make it that we can focus on the cell and have an input in them and then "save" the value to the records array once we unfocus the input

Some pseudo code to kind of illustrate what I mean


const records = [{name: 'row 1', some_val: 100}, {name: 'row2', some_val: 101}];

const create = () => {
    // push an "empty" record, or a default variant of a record
    records.push({name: '', some_val: 0})
}

// cellEdit are pseudo props - does not exist
<DataTable columns={table_columns} records={records} cellEditable />
<Button color="rgba(173, 173, 173, 1)" variant="transparent" onClick={create}>New record...</Button>

What would be recommended way of doing this?

I apologize before hand if there is already an issue or fix for this but after searching I can't seem to find a relevant issue or article in docs.

mikkurogue avatar Jun 26 '24 11:06 mikkurogue

I did find this: https://icflorescu.github.io/mantine-datatable/examples/expanding-rows/#using-collapse-function-in-row-expansion-content

However I think for our use-case this would be a bit too "verbose" as we want to edit directly in the cell.

mikkurogue avatar Jun 26 '24 12:06 mikkurogue

Current workaround is to define the render prop in the column to render an unstyled input. Not sure if I'm a big fan but for now this "works". I'll keep this open for now still for discussion in case we may want to look into supporting cell editing.

@icflorescu its your call :)

mikkurogue avatar Jun 26 '24 14:06 mikkurogue

Did you manage to do proper cell editing?

planecore avatar Dec 16 '24 14:12 planecore

Not necessarily, I havent yet had the time to fork and implement something myself. The way I eventually solved it was by using cell click handlers to "replace" the cell with an input and then when listen to the leave focus event to then save the value in the dataset and re-render the row

mikkurogue avatar Dec 16 '24 18:12 mikkurogue

Current workaround is to define the render prop in the column to render an unstyled input. Not sure if I'm a big fan but for now this "works". I'll keep this open for now still for discussion in case we may want to look into supporting cell editing.

@icflorescu its your call :)

Got some snippet for your implementation?

lukasbash avatar Sep 18 '25 09:09 lukasbash

Current workaround is to define the render prop in the column to render an unstyled input. Not sure if I'm a big fan but for now this "works". I'll keep this open for now still for discussion in case we may want to look into supporting cell editing. @icflorescu its your call :)

Got some snippet for your implementation?

Sure, I put this together fast for you but this is by no means the best, but it is a start.

type DataPoint = {
  id: string;
  name: string;
};

const initialData: DataPoint[] = [
  {
    id: "user_1",
    name: "mikkurogue",
  },
];
const columns: DataTableColumn<DataPoint>[] = [
  {
    accessor: "id",
  },
  {
    accessor: "name",
  },
];


---

export default function Home() {
  const [data, setData] = useState(initialData);
  const [editingCell, setEditingCell] = useState<string | null>(null);

  function handleCellEdit(id: string, field: string, value: any) {
    setData((prevData) =>
      prevData.map((row) => (row.id === id ? { ...row, [field]: value } : row)),
    );
    setEditingCell(null);
  }

  return (
    <Table
      columns={columns}
      data={data}
      editingCell={editingCell}
      onCellClick={(id, field) => setEditingCell(`${id}-${field}`)}
      onCellEdit={handleCellEdit}
    />
  );
}

---
type TableProps<T extends { id: string }> = {
  columns: DataTableColumn<T>[];
  data: T[];
  editingCell: string | null;
  onCellClick: (id: string, field: string) => void;
  onCellEdit: (id: string, field: string, value: any) => void;
};

function Table<T extends { id: string }>({
  columns,
  data,
  editingCell,
  onCellClick,
  onCellEdit,
}: TableProps<T>) {
  return (
    <DataTable
      withTableBorder
      withColumnBorders
      striped
      records={data}
      columns={columns.map((col) => ({
        ...col,
        render: (record: T) => {
          if (
            editingCell === `${record.id}-${col.accessor}` &&
            col.accessor !== "id"
          ) {
            return (
              <TextInput
                defaultValue={record[col.accessor as keyof T] as string}
                onBlur={(e) =>
                  onCellEdit(
                    record.id,
                    col.accessor as string,
                    e.currentTarget.value,
                  )
                }
                autoFocus
              />
            );
          }
          return (
            <div
              onClick={() => onCellClick(record.id, col.accessor as string)}
              style={{ width: "100%", height: "100%" }}
            >
              {record[col.accessor as keyof T]}
            </div>
          );
        },
      }))}
    />
  );
}

export default Table;

In this example, I basically abstract the table to a separate component but you can just do it all in the same place where the implementation of DataTable happens too. Its just important to make sure you have then set proper types for the data/records you are trying to play with

mikkurogue avatar Sep 18 '25 16:09 mikkurogue