在上一部分中,我们学习了如何使用 React Tanstack 添加和删除表格的行。在本文中,我们将学习如何验证表格的行和字段,例如必填的输入、日期、选择和其他自定义验证。
下面展示了我们希望在本教程结束时表格的外观和行为:
输入验证
在本节中,我们将利用原生的 HTML5 输入框验证。我们将使用required
属性来验证必填字段。我们还将使用pattern
属性来验证更复杂的场景。
必填字段
添加required
属性是验证必填字段的简单方法。该属性是布尔值,这意味着它不需要值。它只需要出现在元素中即可验证该字段。
我们可以直接将其添加到TableCell
组件中的输入元素中。
<input value={value} onChange={e => setValue(e.target.value)} onBlur={onBlur} type={columnMeta?.type || "text"} required />
此外,当触发验证时,:invalid
伪类将被添加到输入元素。我们可以使用这个伪类来设置输入元素的样式。
input:invalid { border: 2px solid red; }
现在,如果我们尝试下面的演示,我们将看到空字段用红色边框突出显示。
我们上面使用的方法很简单。然而,它有一个局限性。所有字段都将被验证,即使是那些不需要的字段。为了解决这个问题,我们将在column.ts
文件中的列定义中添加一个required
属性。
... export const columns = [ ... columnHelper.accessor('name', { header: 'Full Name', cell: TableCell, meta: { type: 'text', required: true, }, }) ... ]
然后,在TableCell
组件中,我们将检查required
属性是否存在于columnMeta
对象中。这样,只有具有该required
属性的字段才会被验证。
... 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>; };
上面的演示显示了如何仅在“name”字段上触发required
属性。
选择验证
选择验证与之前的验证类似。我们将使用required
属性来验证select
字段,但我们必须向 select 元素添加一个空选项才能使其正常工作。此外,我们将使用:invalid
伪类来设置select
元素的样式。
... 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; }
让我们在下面的演示中测试一下。当将“major”字段更改为“Select”时,我们将看到该字段以红色边框突出显示。
模式验证
pattern
验证使用正则表达式来验证输入字段。“name”字段就是一个很好的例子。我们想要验证名称仅包含字母和空格。
让我们引入一个在column.ts
文件中的新属性pattern
,它有正则表达式值。
... export const columns = [ ... columnHelper.accessor('name', { header: 'Full Name', cell: TableCell, meta: { type: 'text', required: true, pattern: '^[a-zA-Z ]+$', }, }), ... ]
然后,在TableCell
组件中,我们将pattern
属性添加到输入元素。该值将来自columnMeta
对象。
... 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>; };
现在,在下面的演示中,我们将看到“name”字段使用正则表达式模式进行验证,并且仅接受字母和空格。
显示验证消息
前面,我们在触发验证时为输入元素添加了红色边框,但没有显示任何验证消息。
要显示验证消息,我们可以使用输入元素的title
属性来在用户将鼠标悬停在输入元素上时显示验证消息。
然后,我们需要向TableCell
组件添加一个新状态来存储验证消息和一个新函数来设置值。onBlur
和onChange
事件都会触发该函数。
以下是TableCell
组件的完整代码。
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>; };
现在,在下面的演示中,如果我们在“name”字段中输入无效值并将鼠标悬停在其上,我们将看到验证消息。
请注意,由于样式选项有限,使用title
属性可能不是显示验证消息的最佳方式。我们可以创建自定义工具提示或使用第三方库(例如react-tooltip),但这超出了本教程的范围。
自定义验证
当我们想要使用更复杂的逻辑来验证字段并且使用正则表达式可能会增加代码的复杂性时,自定义验证非常有用。我们以日期为例。我们想要验证该日期不是将来的日期。
为此,我们将添加一个在column.ts
文件中的新属性validate
,其中包含具有验证逻辑的回调函数。该函数将返回一个布尔值。此外,我们将添加一个名为validationMessage
的新属性来显示自定义验证消息。
... 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', }, }), ... ]
然后,在displayValidationMessage
函数中,我们将检查validate
属性是否存在于columnMeta
对象中。如果是,我们将调用该validate
函数并设置新的validationMessage
状态。
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>; };
请注意,我们使用了setCustomValidity
函数。由于我们使用的是原生 HTML5 验证,因此我们需要使用setCustomValidity
函数来触发样式的:invalid
伪类。
在下面的演示中,我们将看到如果日期是将来的日期,则使用自定义验证来验证“date”字段。
行验证
当我们想要验证整个行时,行验证非常有用。例如,如果行中存在无效字段,我们希望禁用保存按钮,以避免保存无效数据。
由于保存按钮位于不同的组件中,因此我们需要在Table
组件中添加一个新的validRows
状态来存储有效行并将其传递给表的元信息。此外,我们将更新updateData
函数中的validRows
状态,该函数将有一个新的isValid
参数。
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 }, })); }, ... }, }); ... };
快速说明下setValidRows
正在做什么,它通过向对象添加新的键值对来更新validRows
状态。键是行索引,值是以列 id 为键、验证状态为值的对象。
下面是validRows
状态的 JSON 表示。
{ "0": { "name": true, "major": true, "date": false }, "1": { "name": true, "major": false, "date": true }, ... }
现在updateData
有了新的isValid
参数,我们需要更新TableCell
组件以将isValid
参数传递给updateData
函数。
... 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); }; ... };
最后,如果当前行中有无效字段,我们将更新EditCell
组件以禁用保存按钮。我们还将设置保存按钮的样式以提示它已被禁用。
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; }
这是表格的最终演示,当我们尝试保存无效行时,我们将看到保存按钮被禁用。
完整代码
完整的代码可以在这个仓库中找到。如果您喜欢本教程,请为仓库加星,并随时提出新功能的需求!
总结
在这一部分中,我们学习了如何使用 React Tanstack 验证表格的行和字段。我们学习了如何验证必填字段、选择字段和自定义验证。我们还学习了如何显示验证消息并在行中存在无效字段时禁用保存按钮。