In the previous part, we learned how to add and remove table rows using React Tanstack. In this part, we will learn how to validate table rows and fields such as required input, date, select, and other custom validations.
Below is a short video of how we want the table to look and behave at the end of this tutorial:
As we continue building upon the same code from the previous part, it’s recommended that you go through previous parts to understand how the code structure works as we will not be explaining it here. You can navigate to any part using the table of contents.
Input Validation
In this section, we will utilize the native HTML5 input validation. We will use the required
attribute to validate the required fields. We will also use the pattern
attribute to validate more complex scenarios.
Required Fields
Adding required
attribute is a straightforward way to validate the required fields. The attribute is a boolean, meaning it does not require a value. It only needs to be present in the element to validate the field.
We can add it directly to the input element in the TableCell
component.
<input
value={value}
onChange={e => setValue(e.target.value)}
onBlur={onBlur}
type={columnMeta?.type || "text"}
required
/>
Also, when the validation is triggered, an :invalid
pseudo-class will be added to the input element. We can use this pseudo-class to style the input element.
input:invalid {
border: 2px solid red;
}
Now, if we try the demo below, we will see that an empty field will be highlighted with a red border.
The approach we used above is simple. However, it has a limitation. All fields will be validated, even the ones that are not required. To solve this problem, we will add a required
property to the column definition in the column.ts
file.
...
export const columns = [
...
columnHelper.accessor('name', {
header: 'Full Name',
cell: TableCell,
meta: {
type: 'text',
required: true,
},
})
...
]
Then, in the TableCell
component, we will check if the required
property is present in the columnMeta
object. This way, only the fields with the required
property will be validated.
...
export const TableCell = ({ getValue, row, column, table }) => {
...
const columnMeta = column.columnDef.meta;
...
if (tableMeta?.editedRows[row.id]) {
return columnMeta?.type === "select" ? (
<select
onChange={onSelectChange}
value={initialValue}
>
{columnMeta?.options?.map((option: Option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
) : (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
onBlur={onBlur}
type={columnMeta?.type || "text"}
required={columnMeta?.required}
/>
);
}
return <span>{value}</span>;
};
The above demo shows how the required
property is only triggered on the “name” field.
Select Validation
The select validation is similar to the previous validation. We will use the required
attribute to validate the select
field, but we must add an empty option to the select element to make it work. Also, we will use the :invalid
pseudo-class to style the select
element.
...
export const columns = [
...
columnHelper.accessor('major', {
header: 'Major',
cell: TableCell,
meta: {
type: 'select',
required: true,
options: [
{ value: '', label: 'Select' },
{ value: 'Computer Science', label: 'Computer Science' },
{ value: 'Communications', label: 'Communications' },
{ value: 'Business', label: 'Business' },
{ value: 'Psychology', label: 'Psychology' },
],
},
}),
columnHelper.display({
id: 'edit',
cell: EditCell,
}),
]
...
export const TableCell = ({ getValue, row, column, table }) => {
...
const columnMeta = column.columnDef.meta;
...
if (tableMeta?.editedRows[row.id]) {
return columnMeta?.type === "select" ? (
<select
onChange={onSelectChange}
value={initialValue}
required={columnMeta?.required}
>
{columnMeta?.options?.map((option: Option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
) : (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
onBlur={onBlur}
type={columnMeta?.type || "text"}
required={columnMeta?.required}
/>
);
}
return <span>{value}</span>;
};
select:invalid, input:invalid {
border: 2px solid red;
}
Let’s test it in the demo below. When changing the “major” field to “Select”, we will see that the field will be highlighted with a red border.
Pattern Validation
The pattern
validation uses regular expressions to validate the input field. A good example of this is the “name” field. We want to validate that the name only contains letters and spaces.
Let’s introduce a new property called pattern
in the column.ts
file with a regex value.
...
export const columns = [
...
columnHelper.accessor('name', {
header: 'Full Name',
cell: TableCell,
meta: {
type: 'text',
required: true,
pattern: '^[a-zA-Z ]+$',
},
}),
...
]
Then, in the TableCell
component, we will add the pattern
property to the input element. The value will come from the columnMeta
object.
...
export const TableCell = ({ getValue, row, column, table }) => {
...
const columnMeta = column.columnDef.meta;
...
if (tableMeta?.editedRows[row.id]) {
return columnMeta?.type === "select" ? (
<select
onChange={onSelectChange}
value={initialValue}
required={columnMeta?.required}
>
{columnMeta?.options?.map((option: Option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
) : (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
onBlur={onBlur}
type={columnMeta?.type || "text"}
required={columnMeta?.required}
pattern={columnMeta?.pattern}
/>
);
}
return <span>{value}</span>;
};
Now, in the demo below, we will see that the “name” field will be validated using the regex pattern and only accept letters and spaces.
Display Validation Message
Previously, we added a red border to the input element when the validation was triggered, but we did not display any validation message.
To display the validation message, we can use the title
attribute of the input element to show the validation message when the user hovers over the input element.
Then, we will need to add a new state to the TableCell
component to store the validation message and a new function to set the value. Both onBlur
and onChange
events will trigger the function.
Below is the full code of the TableCell
component.
...
export const TableCell = ({ getValue, row, column, table }) => {
import { useState, useEffect, ChangeEvent } from "react";
import "./table.css";
type Option = {
label: string;
value: string;
};
export const TableCell = ({ getValue, row, column, table }) => {
const initialValue = getValue();
const columnMeta = column.columnDef.meta;
const tableMeta = table.options.meta;
const [value, setValue] = useState(initialValue);
const [validationMessage, setValidationMessage] = useState("");
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
const onBlur = (e: ChangeEvent<HTMLInputElement>) => {
displayValidationMessage(e);
tableMeta?.updateData(row.index, column.id, value);
};
const onSelectChange = (e: ChangeEvent<HTMLSelectElement>) => {
displayValidationMessage(e);
setValue(e.target.value);
tableMeta?.updateData(row.index, column.id, e.target.value);
};
const displayValidationMessage = <
T extends HTMLInputElement | HTMLSelectElement
>(
e: ChangeEvent<T>
) => {
if (e.target.validity.valid) {
setValidationMessage("");
} else {
setValidationMessage(e.target.validationMessage);
}
};
if (tableMeta?.editedRows[row.id]) {
return columnMeta?.type === "select" ? (
<select
onChange={onSelectChange}
value={initialValue}
required={columnMeta?.required}
title={validationMessage}
>
{columnMeta?.options?.map((option: Option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
) : (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
onBlur={onBlur}
type={columnMeta?.type || "text"}
required={columnMeta?.required}
pattern={columnMeta?.pattern}
title={validationMessage}
/>
);
}
return <span>{value}</span>;
};
Now, in the demo below, if we enter an invalid value in the “name” field and hover over it, we will see the validation message.
Note that using title
attribute might not be the best way to display the validation message because of the limited styling options. We can always create a customomized tooltip or use a third-party library such as react-tooltip, but this is out of the scope of this tutorial.
Custom Validation
The custom validation is useful when we want to validate the field using a more complex logic and using regex might add more complexity to the code. Let’s take the date as an example. We want to validate that the date is not in the future.
To do this, we will add a new property called validate
in the column.ts
file with a callback function that has the validation logic. The function will return a boolean value. Also, we will add a new property called validationMessage
to display the custom validation message.
...
export const columns = [
...
columnHelper.accessor('date', {
header: 'Date',
cell: TableCell,
meta: {
type: 'date',
required: true,
validate: (value: string) => {
const date = new Date(value);
const today = new Date();
return date <= today;
},
validationMessage: 'Date cannot be in the future',
},
}),
...
]
Then, in the displayValidationMessage
function, we will check if the validate
property is present in the columnMeta
object. If it is, we will call the validate
function and set the new validationMessage
state.
import { useState, useEffect, ChangeEvent } from "react";
import "./table.css";
type Option = {
label: string;
value: string;
};
export const TableCell = ({ getValue, row, column, table }) => {
...
const displayValidationMessage = <
T extends HTMLInputElement | HTMLSelectElement
>(
e: ChangeEvent<T>
) => {
if (columnMeta?.validate) {
const isValid = columnMeta.validate(e.target.value);
if (isValid) {
e.target.setCustomValidity("");
setValidationMessage("");
} else {
e.target.setCustomValidity(columnMeta.validationMessage);
setValidationMessage(columnMeta.validationMessage);
}
} else if (e.target.validity.valid) {
setValidationMessage("");
} else {
setValidationMessage(e.target.validationMessage);
}
};
...
}
return <span>{value}</span>;
};
Notice that we used the setCustomValidity
function. Since we are using the native HTML5 validation, we need to use the setCustomValidity
function to trigger the :invalid
pseudo-class for the styling.
In the demo below, we will see that the “date” field will be validated using the custom validation if the date is in the future.
Row Validation
The row validation is useful when we want to validate the row as a whole. For example, we want to disable the save button if there is an invalid field in the row to avoid saving invalid data.
Since the save button is in a different component, we will need to add a new validRows
state in the Table
component to store the valid rows and pass it to the table meta. In addition, we will update the validRows
state in the updateData
function, which will have a new isValid
parameter.
import { useState } from "react";
import { Student, data as defaultData } from "./data";
import "./table.css";
import {
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import { columns } from "./columns";
import { FooterCell } from "./FooterCell";
export const Table = () => {
...
const [validRows, setValidRows] = useState({});
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
enableRowSelection: true,
meta: {
...
validRows,
setValidRows,
...
updateData: (rowIndex: number, columnId: string, value: string, isValid: boolean) => {
setData((old) =>
old.map((row, index) => {
if (index === rowIndex) {
return {
...old[rowIndex],
[columnId]: value,
};
}
return row;
})
);
setValidRows((old) => ({
...old,
[rowIndex]: { ...old[rowIndex], [columnId]: isValid },
}));
},
...
},
});
...
};
A quick clarification to what the setValidRows
is doing, it is updating the validRows
state by adding a new key-value pair to the object. The key is the row index, and the value is an object with the column id as the key and the validation status as the value.
Below is a JSON representation of the validRows
state.
{
"0": {
"name": true,
"major": true,
"date": false
},
"1": {
"name": true,
"major": false,
"date": true
},
...
}
Now that the updateData
has a new isValid
parameter, we will need to update the TableCell
component to pass the isValid
parameter to the updateData
function.
...
export const TableCell = ({ getValue, row, column, table }) => {
...
const onBlur = (e: ChangeEvent<HTMLInputElement>) => {
displayValidationMessage(e);
tableMeta?.updateData(row.index, column.id, value, e.target.validity.valid);
};
const onSelectChange = (e: ChangeEvent<HTMLSelectElement>) => {
displayValidationMessage(e);
setValue(e.target.value);
tableMeta?.updateData(row.index, column.id, e.target.value, e.target.validity.valid);
};
...
};
Finally, we will update the EditCell
component to disable the save button if there is an invalid field in the current row. We will also style the save button to indicate that it is disabled.
import { MouseEvent } from "react";
export const EditCell = ({ row, table }) => {
const meta = table.options.meta;
const validRow = meta?.validRows[row.id];
const disableSubmit = validRow ? Object.values(validRow)?.some(item => !item) : false;
...
return (
<div className="edit-cell-container">
{meta?.editedRows[row.id] ? (
<div className="edit-cell-action">
<button onClick={setEditedRows} name="cancel">
⚊
</button>{" "}
<button onClick={setEditedRows} name="done" disabled={disableSubmit}>
✔
</button>
</div>
) : (
<div className="edit-cell-action">
<button onClick={setEditedRows} name="edit">
✐
</button>
<button onClick={removeRow} name="remove">
X
</button>
</div>
)}
<input
type="checkbox"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
</div>
);
};
table .edit-cell-action button[name='done']:disabled {
background-color: #ccc;
cursor: not-allowed;
}
Here is the final demo of the table, when we try to save an invalid row, we will see that the save button is disabled.
Complete code and live demo
The complete code is available in this repository. If you liked the tutorial, please star the repository, and feel free to request new features!
Alternatively, you can try the code in the live demo below:
Summary
In this part, we learned how to validate table rows and fields using React Tanstack. We learned how to validate required fields, select fields, and custom validations. We also learned how to display validation messages and disable the save button if there is an invalid field in the row.
Bye for now 👋