angular4更改表单中显示的值_我果然还是喜欢白一点的 —— React开发中的白盒表单vs黑盒表单...

2023-05-16

图片来源网络,侵删

是的,我冒着“涉嫌开车”以及“zz正确”的风险取了这么个标题 (狗头)


一会儿再瞎扯,马上进入主题!

在我的这篇文章(目前202赞,250收藏,没看过的快去看看)中

FreewheelLee:以React表单库Formik为例谈优秀的三方库应该是什么样的​zhuanlan.zhihu.com

我提到了受控组件和非受控组件,引起了一点争议。

简单解释一下:

受控组件

受控组件,我称之为白盒组件 —— 是React官方推崇的一种表单的实现方式。特点是表单的值和输入回调都受我们的代码直接控制

如:

import React, {useState} from 'react';

const ControlledForm = () => {
  const [name, setName] = useState('');
  const handleChange = (e) => {
    setName(e.target.value);
  }
  const handleSubmit = (e) => {
    e && e.preventDefault();
    const payload = {
      name: name
    };
    //发送http请求或其他操作
    alert(JSON.stringify(payload, null, 4))
  }

  return (
      <div>
        <form onSubmit={handleSubmit}>
          <label>
            Name: &nbsp;
            <input type="text" value={name} onChange={handleChange}/>
          </label> &nbsp;
          <input type="submit" value="Submit"/>
        </form>
      </div>
  );
};

export default ControlledForm;

可以看到 Input 元素的value 和 onChange 回调都是我们的代码,所以我们理论上可以控制其中的流程,于是我称之为白盒。

非受控组件

非受控组件呢?我称之为黑盒组件,特点是表单相关的值、回调都交给浏览器的原生代码处理,通过组件的引用获取当前表单状态。

如:

import React, {useRef} from 'react';

const UnControlledForm = () => {
  const ref = useRef();
  const handleSubmit = (e) => {
    e && e.preventDefault();
    const payload = {
      name: ref.current.value,
    }
    //发送http请求或其他操作
    alert(JSON.stringify(payload, null, 4))
  }

  return (
      <div>
        <form onSubmit={handleSubmit} >
          <label>
            Name: &nbsp;
            <input type="text" ref={ref}/>
          </label> &nbsp;
          <input type="submit" value="Submit"/>
        </form>
      </div>
  );
};

export default UnControlledForm;

可以看到 input 元素中加了个 ref 作为引用,但是 input 的值如何变化,当前值是什么都不受我们的代码直接控制。唯一推荐我们的代码做的 —— 就是通过 ref 引用间接获取当前 input 的值。

思考:为什么不推荐使用 ref 去更改当前 input 的值 ?

答:因为这导致了 input 的值 有多个来源,既可能是用户通过浏览器更改的,也可能是ref更改的 —— 违反了 single source of truth 原则,容易产生bug,或降低代码可读性 (后文会继续阐释这一条)

表单库

上面阐述的受控与非受控思想也深入到了表单库中,我个人推荐的 Formik 就是基于受控组件的思想,而 react-hook-form 则是基于非受控组件。

为什么我更青睐受控表单库

因为更灵活且更一致

我工作中的项目需要写很多表单,有的表单可能就像上面的例子一样简单,有的表单相当复杂 —— 这里的复杂不仅仅是表单项多那么简单。

在复杂表单中,我发现很难使用非受控表单实现,相反,使用受控表单就能游刃有余处理各种逻辑。

以下我举三类例子:

B项的值跟随 A项变化

在某个表单中,当用户改变 A 选项框的值,B 选项框的当前值也需要变化。

举个简单的例子:假如 A 项代表的是国家,B 代表的是城市

  1. 用户在 A 输入了中国,在 B 中输入了 北京
  2. 用户更改了 A, 输入了日本 —— B 此时肯定不能继续显示北京,产品经理要求默认值是 东京(即当前国家的首都)

如果采用的是非受控表单库,实现起来就很麻烦:

  1. 非受控表单库(如 react-hook-form )的引用对象通常是整个 form 而不是 单一的输入组件,且基于非受控思想一般不直接参与对表单值的篡改 —— 可以参考 react-hook-form 的示例代码 Get Started
  2. 如果使用入侵性代码,强行更改表单项的值就会带来违反single source of truth 原则的问题
  3. 代码可读性会显著下降

如果采用的是受控表单库Formik,实现就简单很多:

import React from 'react';
import {useFormik} from 'formik';

const ControlledForm2 = () => {

  const {values, errors, handleSubmit, handleChange, setFieldValue} = useFormik({
    initialValues: {
      country: 'China',
      city: 'Beijing'
    },
    onSubmit: (values) => {
      alert(JSON.stringify(values, null, 4))
    }
  });

  const onSubmit = e=>{
    e.preventDefault();
    handleSubmit();
  }


  return (
      <div>
        <form onSubmit={onSubmit}>
          <label>
            Country: &nbsp;
            <input type="text" name="country" value={values.country} onChange={(e)=>{
              handleChange(e);
              const country = e.target.value;
              if (country === 'Japan') {
                // 方案一:
                setFieldValue('city', 'Tokyo');
                // 方案二:
                //const mockEvent = { // 模拟一个 change 事件,让 city 更改
                //  target: {
                //    name: 'city',
                //    value: 'Tokyo',
                //  }
                //}
                //handleChange(mockEvent);
              }
            }}/>
          </label>
          <br/>
          <label>
            City: &nbsp;
            <input type="text" name="city" value={values.city} onChange={handleChange}/>
          </label>
          <br/>
          <input type="submit" value="Submit"/>
        </form>
      </div>
  );
};

export default ControlledForm2;

唯一添加的代码就是 country 的 onChange 回调。

有两种方案,第一种是 使用 useFormik() 返回的 setFieldValue 方法;

第二种是 模拟一个 change 事件,调用 handleChange 让 city 更改。

方案一和二底层实现其实是一样的,handleChange 从 event 中提取出 name 和 value 然后调用 setFieldValue —— 最终都能修改 values 中 city 的值。

值得注意的是:我们没有打破 single source of truth,代码流程保持了一致性 —— 更改表单项的值永远是通过handleChange/setFieldValue 来做的;所有 input 显示的值都来自于 values

敏感词禁止

假设我们在做一个评论框,当用户输入敏感词(如粗话、涉嫌黄赌毒),我们要实时替换掉敏感词。

例如我们禁止输入 sex 这个单词

使用受控表单库 Formik 实现的一种方案的核心代码如下

  if (values.comment.includes("sex")) {
    values.comment = values.comment.replace("sex", "***"); // 替换掉敏感词
  }


  return (
      <div>
        <form onSubmit={onSubmit}>
          <label>
            City: &nbsp;
            <input type="text" name="city" value={values.city} onChange={handleChange}/>
          </label>
          <br/>
          <label>
            Comment:
            <input type="text" name="comment" value={values.comment} onChange={handleChange}/>
          </label>
          <input type="submit" value="Submit"/>
        </form>
      </div>
  );

这个需求其实也可以使用上面提到的 setFieldValue 或者 handleChange 模拟事件 来实现,有兴趣的读者可以自己试试看。

容我再强调一遍,上面的代码仍然没有打破 single source of truth,代码流程保持了一致性 —— 更改表单项的值永远是通过handleChange/setFieldValue来做的;所有 input 显示的值都来自于 values

特殊场景下自定义表单验证错误消息

表单库中验证功能肯定不能少,但是大多数表单库实现的是静态的验证规则。

如,用户名需要符合某个正则表达式,年龄不能低于18岁等等。

无论是受控还是非受控表单,静态的验证规则实现起来相对容易。而实现动态的验证规则和错误信息在 Formik 中也一样轻松(感谢评论区一位读者的提醒, errors错误信息的设计 react-hook-form也有类似之处)。

众所周知,我国结婚的法定年龄是男22岁女20岁,产品经理希望你能根据这条规则做年龄验证,且输出不同的错误信息提示

(提示文本纯属搞笑,男拳女拳别打我)

使用受控表单库 Formik 实现的一种方案的核心代码如下

  const {values, errors, handleSubmit, handleChange} = useFormik({
    initialValues: {
      age: 30,
      sex: 'male'
    },
    onSubmit: (values) => {
      alert(JSON.stringify(values, null, 4))
    }
  });

  if (values.sex === 'male' && values.age < 22) {
    errors.age = "毛长齐了吗?存款多少?代码能一次通过编译不出错了吗?就想结婚!?"
  } else if (values.sex === 'female' && values.age < 20) {
    errors.age = "小姑娘的花样年华可别一时大意被猪拱了,过几年成熟点再考虑吧!"
  } else {
    errors.age = "";
  }

  return (
      <div>
        <form onSubmit={onSubmit}>
          <label>
            性别: &nbsp;
            <select name="sex" value={values.sex} onChange={handleChange}>
              <option value="male">男</option>
              <option value="female">女</option>
            </select>
          </label>
          <br/>
          <label>
            年龄: &nbsp;
            <input type="number" name="age" value={values.age} onChange={handleChange}/>
          </label>
          <br/>
          <br/>
          {errors.age && <span style={{color: 'red'}}>{errors.age}</span>}
          <br/>
          <br/>

          <input type="submit" value="Submit"/>
        </form>
      </div>
  );

在 Formik 中,errors 是用于保存表单验证错误信息的(Formik 有内置的表单验证功能,本文没展示)。

同样是因为基于受控组件的思想,和 values 一样 errors也暴露在我们的代码中,我们可以根据自己的需求篡改、自定义错误信息,因此上面这个需求对于Formik 而言十分轻松。

总结

上面三个例子,分别展示了受控表单库 Formik 允许我们从三个方面控制我们的表单逻辑

  1. handleChange/setFieldValue —— 通过setFieldValue 或 handleChange模拟事件,可以同时修改多个input的值
  2. values —— 通过拦截、篡改 values ,直接影响表单显示的值
  3. errors —— 通过拦截、篡改 errors ,方便自定义表单验证消息

而且,这个表单库在运作中始终保持 single source of truth —— view 的数据始终来自 values 和 errors,表单值的变化始终由 handleChange 发起

正是因为这种灵活性和一致性让我更喜欢白盒的受控表单 —— 简言之,我更喜欢白一点的(狗头)

踩过的坑

其实 useFormik() 还返回了 setFieldError 函数,可以用来设置 errors。 这个设计类似于 setFieldValue。

但是setFieldError 和 setFieldValue 都会触发重新渲染,所以使用不当会造成无限循环渲染。

拓展与思考

  1. single source of truth 的思想其实在 React 以相关生态的设计与开发中是很常见的,比如 React 的单数据流向、Redux 的 action-dispatch-reducer 的流程设计等。
    相反的是,假如你发现组件中某个状态既可能因为 props 的改变而变化,也会因为 state 的改变而变化,那么这个组件通常就是不合理的。你应该再抽象出一层组件,让内层的组件的 props 稳定下来 或者 让内部组件的状态只受内部 state 的影响 或 只受 props 的影响。
  2. 封装的程度
    个人认为在复杂表单的场景下,表单库的封装不应该太过分,适当地暴露出更多实现细节可以让业务实现更轻松。
    类似的,当自己写一个工具库时,封装内部实现时不要一味追逐黑盒,应多思考使用场景给调用者提供足够的灵活性 —— 设计从白盒开始往黑盒慢慢前进可能是个不错的选择。

相关文章

FreewheelLee:以React表单库Formik为例谈优秀的三方库应该是什么样的​zhuanlan.zhihu.com

参考链接:

受控组件: https://zh-hans.reactjs.org/docs/forms.html#controlled-components

非受控组件:https://zh-hans.reactjs.org/docs/uncontrolled-components.html

Formik: https://formik.org/docs/overview

react-hook-form: react-hook-form.com

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

angular4更改表单中显示的值_我果然还是喜欢白一点的 —— React开发中的白盒表单vs黑盒表单... 的相关文章

随机推荐