Muhi Logo Text
AboutBlogWork With Me

[Part 3] Validate Table Rows and Fields Using React Tanstack

Learn how to validate table input fields, select fields, and custom validations using React Tanstack.

Last updated on October 23, 2023

react
tanstack
React Table validation

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 👋

If you enjoyed this post, I regularly share similar content on Twitter. Follow me @muhimasri to stay up to date, or feel free to message me on Twitter if you have any questions or comments. I'm always open to discussing the topics I write about!

Recommended Reading

Learn how to create encapsulated and reusable Fieldset component with Material UI (MUI) and React.

react
mui

Discussion

Upskill Your Frontend Development Techniques 🌟

Subscribe to stay up-to-date and receive quality front-end development tutorials straight to your inbox!

No spam, sales, or ads. Unsubscribe anytime you wish.

© 2024, Muhi Masri