Skip to content

Pre-populate new rows with default values when users add rows to the grid.

Basic spare rows

To keep one empty row at the bottom of the grid, set minSpareRows to 1.

JavaScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
registerAllModules();
const data = [
['', 'Tesla', 'Nissan', 'Toyota', 'Honda'],
['2017', 10, 11, 12, 13],
['2018', 20, 11, 14, 13],
['2019', 30, 15, 12, 13],
];
const ExampleComponent = () => {
return (
<HotTable
data={data}
minSpareRows={1}
height="auto"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;
TypeScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
registerAllModules();
const data = [
['', 'Tesla', 'Nissan', 'Toyota', 'Honda'],
['2017', 10, 11, 12, 13],
['2018', 20, 11, 14, 13],
['2019', 30, 15, 12, 13],
];
const ExampleComponent = () => {
return (
<HotTable
data={data}
minSpareRows={1}
height="auto"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;

Spare rows with placeholder styling

To hint what to enter in the spare row, add a custom cell renderer that displays greyed-out placeholder text in empty cells. The renderer checks whether the whole row is empty, then shows a template value in a lighter color.

JavaScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
import { textRenderer } from 'handsontable/renderers/textRenderer';
registerAllModules();
const templateValues = ['one', 'two', 'three'];
const data = [
['', 'Tesla', 'Nissan', 'Toyota', 'Honda'],
['2017', 10, 11, 12, 13],
['2018', 20, 11, 14, 13],
['2019', 30, 15, 12, 13],
];
const ExampleComponent = () => {
function isEmptyRow(instance, row) {
const rowData = instance.getDataAtRow(row);
for (let i = 0, ilen = rowData.length; i < ilen; i++) {
if (rowData[i] !== null) {
return false;
}
}
return true;
}
function defaultValueRenderer(instance, td, row, col) {
const args = arguments;
if (args[5] === null && isEmptyRow(instance, row)) {
args[5] = templateValues[col];
td.style.color = '#999';
} else {
td.style.color = '';
}
textRenderer.apply(this, args);
}
return (
<HotTable
data={data}
minSpareRows={1}
height="auto"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
cells={() => ({ renderer: defaultValueRenderer })}
/>
);
};
export default ExampleComponent;
TypeScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
import { textRenderer } from 'handsontable/renderers/textRenderer';
import Handsontable from 'handsontable/base';
registerAllModules();
const templateValues = ['one', 'two', 'three'];
const data = [
['', 'Tesla', 'Nissan', 'Toyota', 'Honda'],
['2017', 10, 11, 12, 13],
['2018', 20, 11, 14, 13],
['2019', 30, 15, 12, 13],
];
const ExampleComponent = () => {
function isEmptyRow(instance: Handsontable, row: number) {
const rowData = instance.getDataAtRow(row);
for (let i = 0, ilen = rowData.length; i < ilen; i++) {
if (rowData[i] !== null) {
return false;
}
}
return true;
}
function defaultValueRenderer(
this: Handsontable,
instance: Handsontable,
td: HTMLTableCellElement,
row: number,
col: number
) {
const args = arguments;
if (args[5] === null && isEmptyRow(instance, row)) {
args[5] = templateValues[col];
td.style.color = '#999';
} else {
td.style.color = '';
}
textRenderer.apply(this, args as any);
}
return (
<HotTable
data={data}
minSpareRows={1}
height="auto"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
cells={() => ({ renderer: defaultValueRenderer })}
/>
);
};
export default ExampleComponent;

Auto-populating with template values

For full pre-population, use the beforeChange hook to fill all cells in a spare row with template values the moment the user starts editing. The isEmptyRow() helper detects whether the row is untouched, and the hook pushes changes for every column except the one the user is editing.

JavaScript
import { useRef } from 'react';
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
import { textRenderer } from 'handsontable/renderers/textRenderer';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
const hotRef = useRef(null);
const templateValues = ['one', 'two', 'three'];
const data = [
['', 'Tesla', 'Nissan', 'Toyota', 'Honda'],
['2017', 10, 11, 12, 13],
['2018', 20, 11, 14, 13],
['2019', 30, 15, 12, 13],
];
function isEmptyRow(instance, row) {
const rowData = instance.getDataAtRow(row);
for (let i = 0, ilen = rowData.length; i < ilen; i++) {
if (rowData[i] !== null) {
return false;
}
}
return true;
}
function defaultValueRenderer(instance, td, row, col) {
const args = arguments;
if (args[5] === null && isEmptyRow(instance, row)) {
args[5] = templateValues[col];
td.style.color = '#999';
} else {
td.style.color = '';
}
textRenderer.apply(this, args);
}
return (
<>
<HotTable
ref={hotRef}
data={data}
startRows={8}
startCols={5}
minSpareRows={1}
contextMenu={true}
height="auto"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
cells={function (row, col, prop) {
const cellProperties = {};
cellProperties.renderer = defaultValueRenderer;
return cellProperties;
}}
beforeChange={function (changes) {
const instance = hotRef.current?.hotInstance;
const columns = instance?.countCols() || 0;
const rowColumnSeen = {};
const rowsToFill = {};
for (let i = 0; i < changes.length; i++) {
const cellChanges = changes;
// if oldVal is empty
if (cellChanges[i][2] === null && cellChanges[i][3] !== null) {
if (isEmptyRow(instance, cellChanges[i][0])) {
// add this row/col combination to the cache so it will not be overwritten by the template
rowColumnSeen[`${cellChanges[i][0]}/${cellChanges[i][1]}`] = true;
rowsToFill[cellChanges[i][0]] = true;
}
}
}
for (const r in rowsToFill) {
if (rowsToFill.hasOwnProperty(r)) {
for (let c = 0; c < columns; c++) {
// if it is not provided by user in this change set, take the value from the template
if (!rowColumnSeen[`${r}/${c}`]) {
changes.push([r, c, null, templateValues[c]]);
}
}
}
}
}}
/>
</>
);
};
export default ExampleComponent;
TypeScript
import { useRef } from 'react';
import { HotTable, HotTableRef } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
import { textRenderer } from 'handsontable/renderers/textRenderer';
import Handsontable from 'handsontable/base';
import { CellChange } from 'handsontable/common';
// register Handsontable's modules
registerAllModules();
const ExampleComponent = () => {
const hotRef = useRef<HotTableRef>(null);
const templateValues = ['one', 'two', 'three'];
const data = [
['', 'Tesla', 'Nissan', 'Toyota', 'Honda'],
['2017', 10, 11, 12, 13],
['2018', 20, 11, 14, 13],
['2019', 30, 15, 12, 13],
];
function isEmptyRow(instance: Handsontable, row: number) {
const rowData = instance.getDataAtRow(row);
for (let i = 0, ilen = rowData.length; i < ilen; i++) {
if (rowData[i] !== null) {
return false;
}
}
return true;
}
function defaultValueRenderer(
this: Handsontable,
instance: Handsontable,
td: HTMLTableCellElement,
row: number,
col: number
) {
const args = arguments;
if (args[5] === null && isEmptyRow(instance, row)) {
args[5] = templateValues[col];
td.style.color = '#999';
} else {
td.style.color = '';
}
textRenderer.apply(this, args as any);
}
return (
<>
<HotTable
ref={hotRef}
data={data}
startRows={8}
startCols={5}
minSpareRows={1}
contextMenu={true}
height="auto"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
cells={function (row, col, prop) {
const cellProperties: Handsontable.CellMeta = {};
cellProperties.renderer = defaultValueRenderer;
return cellProperties;
}}
beforeChange={function (changes) {
const instance = hotRef.current?.hotInstance;
const columns = instance?.countCols() || 0;
const rowColumnSeen = {};
const rowsToFill = {};
for (let i = 0; i < changes.length; i++) {
const cellChanges = changes as CellChange;
// if oldVal is empty
if (cellChanges[i][2] === null && cellChanges[i][3] !== null) {
if (isEmptyRow(instance!, cellChanges[i][0])) {
// add this row/col combination to the cache so it will not be overwritten by the template
rowColumnSeen[`${cellChanges[i][0]}/${cellChanges[i][1]}`] = true;
rowsToFill[cellChanges[i][0]] = true;
}
}
}
for (const r in rowsToFill) {
if (rowsToFill.hasOwnProperty(r)) {
for (let c = 0; c < columns; c++) {
// if it is not provided by user in this change set, take the value from the template
if (!rowColumnSeen[`${r}/${c}`]) {
changes.push([r, c, null, templateValues[c]]);
}
}
}
}
}}
/>
</>
);
};
export default ExampleComponent;

Pre-populate from an adjacent row

You can copy values from the row above when the user inserts a new row. Use the afterCreateRow hook to read the source row’s data and write it to the newly created row.

const hot = new Handsontable(container, {
data: [
['Product A', 10, 'active'],
['Product B', 20, 'inactive'],
],
colHeaders: ['Name', 'Quantity', 'Status'],
afterCreateRow(index) {
if (index > 0) {
const sourceRow = hot.getSourceDataAtRow(index - 1);
sourceRow.forEach((value, col) => {
hot.setDataAtCell(index, col, value);
});
}
},
licenseKey: 'non-commercial-and-evaluation',
});

Pre-populate from a server-fetched default

You can fetch default row values from a server and apply them when the user adds a new row. Use the afterCreateRow hook to trigger the request and populate the row once the response arrives.

const hot = new Handsontable(container, {
data: [],
colHeaders: ['Name', 'Quantity', 'Status'],
afterCreateRow(index) {
fetch('/api/row-defaults')
.then((response) => response.json())
.then((defaults) => {
hot.setDataAtRow(index, [defaults.name, defaults.quantity, defaults.status]);
});
},
licenseKey: 'non-commercial-and-evaluation',
});

Result

Your grid keeps one or more empty rows at the bottom. Depending on the approach, spare rows show greyed-out placeholder text or auto-fill all cells with template values when the user starts editing.

Hooks