创建您自己的 React 验证库:功能(第 2 部分)

已发表: 2022-03-10
快速总结 ↬在 Kristofer 的上一篇文章中,他解释了如何实现验证库的基本部分。 下一部分将侧重于改善开发人员体验,而今天的文章将侧重于为第 1 部分中创建的内容添加更多功能。

实现一个验证库并不是那么难。 也不是添加所有那些使您的验证库比其他功能更好的额外功能。

本文将继续实现我们在本系列上一部分开始实现的验证库。 这些功能将把我们从一个简单的概念证明变成一个实际可用的库!

  • 第 1 部分:基础知识
  • 第 2 部分:功能
  • 第 3 部分:体验

仅在提交时显示验证

由于我们正在验证所有更改事件,因此我们太早地显示用户错误消息而无法获得良好的用户体验。 有几种方法可以缓解这种情况。

第一个解决方案只是提供submitted的标志作为useValidation挂钩的返回属性。 这样,我们可以在显示错误消息之前检查表单是否已提交。 这里的缺点是我们的“显示错误代码”变得有点长:

 <label> Username <br /> <input {...getFieldProps('username')} /> {submitted && errors.username && ( <div className="error">{errors.username}</div> )} </label>

另一种方法是提供第二组错误(我们称它们为submittedErrors ),如果submitted为假,则为空对象,如果为真,则为errors对象。 我们可以这样实现:

 const useValidation = config => { // as before return { errors: state.errors, submittedErrors: state.submitted ? state.errors : {}, }; }

这样,我们可以简单地解构出我们想要显示的错误类型。 当然,我们也可以在调用站点执行此操作——但通过在此处提供它,我们只是实现了一次,而不是在所有消费者内部实现。

  • 请参阅 CodeSandbox 演示,展示如何使用submittedErrors
跳跃后更多! 继续往下看↓

模糊显示错误消息

很多人希望在离开某个领域后看到错误。 我们可以添加对此的支持,通过跟踪哪些字段被“模糊”(导航离开),并返回一个对象blurredErrors ,类似于上面的submittedErrors错误。

该实现需要我们处理一个新的动作类型—— blur ,它将更新一个名为blurred的新状态对象:

 const initialState = { values: {}, errors: {}, blurred: {}, submitted: false, }; function validationReducer(state, action) { switch (action.type) { // as before case 'blur': const blurred = { ...state.blurred, [action.payload]: true }; return { ...state, blurred }; default: throw new Error('Unknown action type'); } }

当我们派发blur动作时,我们在blurred状态对象中创建一个新属性,其中字段名称作为键,指示字段已被模糊。

下一步是在我们的getFieldProps函数中添加一个onBlur属性,它会在适用时调度这个动作:

 getFieldProps: fieldName => ({ // as before onBlur: () => { dispatch({ type: 'blur', payload: fieldName }); }, }),

最后,我们需要从我们的useValidation钩子中提供blurredErrors以便我们可以仅在需要时显示错误。

 const blurredErrors = useMemo(() => { const returnValue = {}; for (let fieldName in state.errors) { returnValue[fieldName] = state.blurred[fieldName] ? state.errors[fieldName] : null; } return returnValue; }, [state.errors, state.blurred]); return { // as before blurredErrors, };

在这里,我们创建了一个记忆函数,该函数根据字段是否模糊来确定要显示哪些错误。 每当错误或模糊对象发生变化时,我们都会重新计算这组错误。 您可以在文档中阅读有关useMemo挂钩的更多信息。

  • 请参阅 CodeSandbox 演示

是时候进行微小的重构了

我们的useValidation组件现在返回三组错误——其中大部分在某个时间点看起来都是一样的。 我们不会沿着这条路线走,而是让用户在配置中指定他们希望在他们的表单中显示错误的时间。

我们的新选项showErrors将接受“提交”(默认)、“始终”或“模糊”。 如果需要,我们可以稍后添加更多选项。

 function getErrors(state, config) { if (config.showErrors === 'always') { return state.errors; } if (config.showErrors === 'blur') { return Object.entries(state.blurred) .filter(([, blurred]) => blurred) .reduce((acc, [name]) => ({ ...acc, [name]: state.errors[name] }), {}); } return state.submitted ? state.errors : {}; } const useValidation = config => { // as before const errors = useMemo( () => getErrors(state, config), [state, config] ); return { errors, // as before }; };

由于错误处理代码开始占用我们的大部分空间,我们将其重构为自己的函数。 如果您不遵循Object.entries.reduce的内容——那很好——这是对上一节中for...in代码的重写。

如果我们需要 onBlur 或即时验证,我们可以在我们的useValidation配置对象中指定showError属性。

 const config = { // as before showErrors: 'blur', }; const { getFormProps, getFieldProps, errors } = useValidation(config); // errors would now only include the ones that have been blurred
  • 请参阅 CodeSandbox 演示

关于假设的说明

“请注意,我现在假设每个表单都希望以相同的方式显示错误(始终处于提交状态,始终处于模糊状态等)。 对于大多数应用程序来说,这可能是正确的,但可能并非对所有应用程序都是如此。 了解您的假设是创建 API 的重要部分。”

允许交叉验证

验证库的一个非常强大的功能是允许交叉验证——也就是说,将一个字段的验证基于另一个字段的值。

为此,我们需要让我们的自定义钩子接受一个函数而不是一个对象。 将使用当前字段值调用此函数。 实现它其实只需要三行代码!

 function useValidation(config) { const [state, dispatch] = useReducer(...); if (typeof config === 'function') { config = config(state.values); } }

要使用此功能,我们可以简单地将一个返回配置对象的函数传递给useValidation

 const { getFieldProps } = useValidation(fields => ({ password: { isRequired: { message: 'Please fill out the password' }, }, repeatPassword: { isRequired: { message: 'Please fill out the password one more time' }, isEqual: { value: fields.password, message: 'Your passwords don\'t match' } } }));

在这里,我们使用fields.password的值来确保两个密码字段包含相同的输入(这是糟糕的用户体验,但这是另一篇博客文章的内容)。

  • 请参阅不允许用户名和密码为相同值的 CodeSandbox 演示。

添加一些辅助功能胜利

当您负责字段的 props 时,要做的一件巧妙的事情是默认添加正确的 aria-tags。 这将帮助屏幕阅读器解释您的表单。

一个非常简单的改进是如果字段有错误,则添加aria-invalid="true" 。 让我们实现它:

 const useValidation = config => { // as before return { // as before getFieldProps: fieldName => ({ // as before 'aria-invalid': String(!!errors[fieldName]), }), } };

这增加了一行代码,为屏幕阅读器用户提供更好的用户体验。

您可能想知道我们为什么要写String(!!state.errors[fieldName])state.errors[fieldName]是一个字符串,双重否定运算符为我们提供了一个布尔值(而不仅仅是一个真值或假值)。 但是, aria-invalid属性应该是一个字符串(除了“true”或“false”,它还可以读作“grammar”或“spelling”),因此我们需要将该布尔值强制转换为等效的字符串。

我们还可以做一些调整来提高可访问性,但这似乎是一个公平的开始。

速记验证消息语法

calidators包中的大多数验证器(以及我认为的大多数其他验证器)只需要一条错误消息。 如果我们可以只传递该字符串而不是具有包含该字符串的message属性的对象,那不是很好吗?

让我们在validateField函数中实现它:

 function validateField(fieldValue = '', fieldConfig, allFieldValues) { for (let validatorName in fieldConfig) { let validatorConfig = fieldConfig[validatorName]; if (typeof validatorConfig === 'string') { validatorConfig = { message: validatorConfig }; } const configuredValidator = validators[validatorName](validatorConfig); const errorMessage = configuredValidator(fieldValue); if (errorMessage) { return errorMessage; } } return null; }

这样,我们可以像这样重写我们的验证配置:

 const config = { username: { isRequired: 'The username is required', isEmail: 'The username should be a valid email address', }, };

干净多了!

初始字段值

有时,我们需要验证已经填写的表格。 我们的自定义钩子还不支持它——所以让我们开始吧!

初始字段值将在每个字段的配置中指定,在属性initialValue中。 如果未指定,则默认为空字符串。

我们将创建一个函数getInitialState ,它将为我们创建 reducer 的初始状态。

 function getInitialState(config) { if (typeof config === 'function') { config = config({}); } const initialValues = {}; const initialBlurred = {}; for (let fieldName in config.fields) { initialValues[fieldName] = config.fields[fieldName].initialValue || ''; initialBlurred[fieldName] = false; } const initialErrors = validateFields(initialValues, config.fields); return { values: initialValues, errors: initialErrors, blurred: initialBlurred, submitted: false, }; }

我们遍历所有字段,检查它们是否具有initialValue属性,并相应地设置初始值。 然后我们通过验证器运行这些初始值并计算初始误差。 我们返回初始状态对象,然后可以将其传递给我们的useReducer钩子。

由于我们在字段配置中引入了非验证器道具,因此在验证字段时需要跳过它。 为此,我们更改了validateField函数:

 function validateField(fieldValue = '', fieldConfig) { const specialProps = ['initialValue']; for (let validatorName in fieldConfig) { if (specialProps.includes(validatorName)) { continue; } // as before } }

随着我们不断添加更多这样的功能,我们可以将它们添加到我们的specialProps数组中。

  • 请参阅 CodeSandbox 演示

加起来

我们正在努力创建一个令人惊叹的验证库。 我们添加了大量功能,现在我们几乎是思想领袖。

在本系列的下一部分中,我们将添加所有使我们的验证库在 LinkedIn 上流行的附加功能。