React 中的表单

表单元素和所有其他元素的不同之处在于:表单元素是输入组件,它是有内部状态的,所有的其他元素都是输出元素。原生的 HTML 表单在 React 中本身也是能用的,但是一般情况下,我们可能会交给一个函数来处理提交表单这个事情,因为需要表单验证等等,这时候我们就可以用”受控组件”了。

什么是受控组件

受控组件就是数据不保存在 DOM 中,而是始终保存在 JS 中,随时通过 value/onChange 读取验证。非受控组件数据依然保存在 DOM 树中,只在 submit 时候读取。

function MyForm(props) {
  value, setValue = useState("");

  function handleChange(e) {
    setValue(e.target.value)
  }

  function handleSubmit(e) {
    console.log("A name was submitted: " + value);
    e.preventDefault();
  }

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

使用原生表单

完全可以使用原生组件,而不要直接使用受控组件。

function form2kv(form) {
  const fd = new FormData(form);
  const ret = {};
  for (let key of fd.keys()) {
    ret[key] = fd.get(key);
  }
  return ret;
}

function MyForm(props) {

  // 也可以直接通过读取 events.target 的值来获取
  function handleSubmit(event) {
    event.preventDefault();
    console.log(event.target.elements.username.value) // from elements property
    console.log(event.target.username.value)          // or directly
  }

  function handleSubmit(e) {
    e.preventDefault();
    const data = form2kv(e.target);
    axios.post("/api/myresource", data);
  }
  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="username">Enter username</label>
      <input type="text" id="username" name="username">
      <label htmlFor="password">Enter password</label>
      <input type="password" id="password" name="password">
      <label htmlFor="birthdate">Enter your birth date</label>
      <input type="date" id="birthdate" name="birthdate">
      <button type="submit">Send data!</button>
    </form>
  )
}

在上面的例子中,我们使用了浏览器自带的 Formdata 对象,通过传入 DOM 中的 Form 元素来读取其中的数据。另外需要注意的一点是,我们使用了 onSubmit 事件,而不是 Submit Button 的 onClick 时间,这样保留了浏览器的原生处理方式,也就是可以使用回车键提交。

处理数据

如果当我们需要变换数据格式时呢?

  • 用户输入的数据是 MM/DD/YYYY, 服务端要求是 YYYY-MM-DD
  • 用户名应该全部大写

这时候可以使用另一个小技巧,data-x 属性。

const inputParsers = {
  date(input) {
    const [month, day, year] = input.split('/');
    return `${year}-${month}-${day}`;
  },
  uppercase(input) {
    return input.toUpperCase();
  },
  number(input) {
    return parseFloat(input);
  },
};

function form2kv(form, parsers = {}) {
  const fd = new FormData(form);
  const ret = {};
  for (let name of data.keys()) {
    const input = form.elements[name];
    const parseName = input.dataset.parse;

    if (parseName) {
      const parser = inputParsers[parseName];
      const parsedValue = parser(data.get(name));
      ret[name] = parsedValue;
    } else {
      ret[name] = data.get(name);
    }
  }
  return ret;
}

function MyForm(props) {
  function handleSubmit(e) {
    e.preventDefault();
    const data = form2kv(e.target);
    fetch("api/myresource", method="POST", body=JSON.stringify(data));
  }
  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="username">Enter username</label>
      <input type="text" id="username" name="username" data-parse="uppercase">
      <label htmlFor="password">Enter password</label>
      <input type="password" id="password" name="password">
      <label htmlFor="birthdate">Enter your birth date</label>
      <input type="date" id="birthdate" name="birthdate" data-parse="date">
      <button type="submit">Send data!</button>
    </form>
  )
}

在上面的例子中,我们通过使用 data-x 属性指定了应该使用的处理函数。通过表单的 elements 属性,我们可以按照 input 的 name 访问元素。而节点的 data-x 属性可以在 dataset[x] 属性中访问。

验证表单

HTML 的原生 API 就支持了很多的验证属性,其中最最好用的就是 pattern 属性了,他可以指定一个正则来验证表单。而且我们也可以通过 type 属性来制定 email 等等特殊的 text 类型。HTML 支持的验证属性有:

  • required, 必填字段,不能为空
  • pattern, 通过一个正则来验证输入
  • minlength/maxlength, 输入文本的长度
  • min/max 数字的区间
  • type, email 等等类型

在 JS 中可以调用 form.checkValidity() 函数获得检查结果。还需要在 form 中添加 novalidate 属性,否则浏览器就会在校验失败的时候直接不触发 submit 事件了。

当表单验证不通过的时候,会获得一个 :invalid 的伪类。但是比较坑爹的是,当表单还没有填写的时候,验证就已经生效了,这样对用户来说非常不友好,不过也很好克服,我们只要设定一个类,比如说 display-errors, 只有当用户尝试提交表单之后再应用这个类就好啦~

displayErrors, setDisplayErrors = useState(false);

function handleSubmit(e) {
  if (!e.target.checkValidity()) {
    setDisplayError(true);
  }
}

return (
  <form
    onSubmit={handleSubmit}
    noValidate  // 即使校验失败也要触发
    className={displayErrors ? 'displayErrors' : ''}
  >
    {/* ... */}
  </form>
)
.displayErrors input:invalid {
  border-color: red;
}

select 组件

原生的 select 组件

<select>
  <option value="grapefruit">Grapefruit</option>
  <option value="lime">Lime</option>
  <option selected value="coconut">Coconut</option>
  <option value="mango">Mango</option>
</select>

在受控的组件中,可以在 select 组件中使用 value 属性指定选项。读取可以使用 e.target.value

<select value={state.value} onChange={handleChange}>
  <option value="grapefruit">Grapefruit</option>
  <option value="lime">Lime</option>
  <option value="coconut">Coconut</option>
  <option value="mango">Mango</option>
</select>

如果需要多选的话,使用 <select multiple={true} value={['B', 'C']}>

file 组件

file 组件是只读的,所以只能使用

textarea 组件

react-hook-form

对于更复杂的逻辑,推荐使用 react-hook-form

react-hook-form 的 v7 中使用的是未受控的组件,这样性能更好。最近一直在使用 tailwind UI,对于原生组件来说写样式更方便一点。

当使用 RHF 的时候,可以配合使用 Yup 来做表单验证。

使用 antd 表单

表单的基本元素是 <Form/>, Form 中嵌套 Form.Item, 其中再嵌套 Input/TextArea 等组件。

表单的数据都在 form 元素中,使用 const form = Form.useForm() 来获取,并通过 form={form} 传递给 Form 控件。

设置了 name 属性的 Form.Item 会被 form 接管,也就是:

  1. 不需要通过 onChange 来更新属性值。当然依然可以通过 onChange 监听
  2. 不能使用 value 和 defaultValue,只能使用 form 的 initialValues/setFieldsValue 设置

总体来说,antd 的表单还是封装过度了,非常难以定制化。

参考

  1. https://reactjs.org/docs/uncontrolled-components.html
  2. Controlled and uncontrolled form inputs in React don’t have to be complicated
  3. How to handle forms with just React
  4. https://reactjs.org/docs/forms.html
  5. https://jsfiddle.net/everdimension/5ry2wdaa/
  6. https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/elements
  7. https://www.cnblogs.com/wonyun/p/6023363.html
  8. https://stackoverflow.com/questions/23427384/get-form-data-in-reactjs
  9. https://github.com/jquense/yup

及时获取更新,请关注公众号“爬虫技术学习”(spider-learn)

多年大厂求职&面试官经验,简历付费优化,¥ 500/次。

公众号“爬虫技术学习(spider-learn)”

About 逸飞

后端工程师

发表评论

邮箱地址不会被公开。 必填项已用*标注