摘要:受控输入框只会显示通过传入的数据。例如,数组中的元素将会渲染三个单选框或复选框。属性接收一个布尔值,用来表示组件是否应该被渲染成选中状态。
原文地址:React.js Forms: Controlled Components
原文作者:Loren Stewart
译者:小 B0Y
校对者:珂珂君
本文涵盖以下受控组件:
文本输入框
数字输入框
单选框
复选框
文本域
下拉选择框
同时也包含:
表单数据的清除和重置
表单数据的提交
表单校验
介绍点击这里直接查看示例代码。
查看示例。
请在运行示例时打开浏览器的控制台。
在学习 React.js 时我遇到了一个问题,那就是很难找到受控组件的真实示例。受控文本输入框的例子倒是很丰富,但复选框、单选框、下拉选择框的例子却不尽人意。
本文列举了真实的受控表单组件示例,要是我在学习 React 的时候早点发现这些示例就好了。除了日期和时间输入框需要另开篇幅详细讨论,文中列举了所有的表单元素。
有时候,为了减少开发时间,有时候人们很容易为了一些东西(譬如表单元素)引入一个库。而对于表单,我发现当需要添加自定义行为或表单校验时,使用库会让事情变得更复杂。不过一旦掌握合适的 React 模式,你会发现构建表单组件并非难事,并且有些东西完全可以自己动手,丰衣足食。请把本文的示例代码当作你创建表单组件的起点或灵感之源。
除了提供多带带的组件代码,我还将这些组件放进表单中,方便你理解子组件如何更新父组件 state ,以及接下来父组件如何通过 props(单向数据流)更新子组件。
注意:本表单示例由很赞的 create-react-app 构建配置生成,如果你还没有安装该构建配置,我强烈推荐你安装一下(npm install -g create-react-app)。目前这是搭建 React 应用最简单的方式。
什么是受控组件?受控组件有两个特点:
受控组件提供方法,让我们在每次 onChange 事件发生时控制它们的数据,而不是一次性地获取表单数据(例如用户点提交按钮时)。“被控制“ 的表单数据保存在 state 中(在本文示例中,是父组件或容器组件的 state)。
(译注:这里作者的意思是通过受控组件, 可以跟踪用户操作表单时的数据,从而更新容器组件的 state ,再单向渲染表单元素 UI。如果不使用受控组件,在用户实时操作表单时,比如在输入框输入文本时,不会同步到容器组件的 state,虽然能同步输入框本身的 value,但与容器组件的 state 无关,因此容器组件只能在某一时间,比如提表单时一次性地拿到(通过 refs 或者选择器)表单数据,而难以跟踪)
受控组件的展示数据是其父组件通过 props 传递下来的。
这个单向循环 —— (数据)从(1)子组件输入到(2)父组件的 state,接着(3)通过 props 回到子组件,就是 React.js 应用架构中单向数据流的含义。
表单结构我们的顶级组件叫做 App,这是它的代码:
import React, { Component } from "react"; import "../node_modules/spectre.css/dist/spectre.min.css"; import "./styles.css"; import FormContainer from "./containers/FormContainer"; class App extends Component { render() { return (); } } export default App;React.js Controlled Form Components
App 只负责渲染 index.html 页面。整个 App 组件最有趣的部分是 13 行,FormContainer 组件。
插曲: 容器(智能)组件 VS 普通(木偶)组件是时候提及一下容器(智能)组件和普通(木偶)组件了。容器组件包含业务逻辑,它会发起数据请求或进行其他业务操作。普通组件则从它的父(容器)组件接收数据。木偶组件有可能触发更新 state (译注:容器组件的 state)这类逻辑行为,但它仅通过从父(容器)组件传入的方法来达到该目的。
注意: 虽然在我们的表单应用里父组件就是容器组件,但我要强调,并非所有的父组件都是容器组件。木偶组件嵌套木偶组件也是可以的。
回到应用结构FormContainer 组件包含了表单元素组件,它在生命周期钩子方法 componentDidMount 里请求数据,此外还包含更新表单应用 state 的逻辑行为。在下面的预览代码里,我移除了表单元素的 props 和 change 事件处理方法,这样看起来更简洁清晰(拉到文章底部,可以看到完整代码)。
import React, {Component} from "react"; import CheckboxOrRadioGroup from "../components/CheckboxOrRadioGroup"; import SingleInput from "../components/SingleInput"; import TextArea from "../components/TextArea"; import Select from "../components/Select"; class FormContainer extends Component { constructor(props) { super(props); this.handleFormSubmit = this.handleFormSubmit.bind(this); this.handleClearForm = this.handleClearForm.bind(this); } componentDidMount() { fetch("./fake_db.json") .then(res => res.json()) .then(data => { this.setState({ ownerName: data.ownerName, petSelections: data.petSelections, selectedPets: data.selectedPets, ageOptions: data.ageOptions, ownerAgeRangeSelection: data.ownerAgeRangeSelection, siblingOptions: data.siblingOptions, siblingSelection: data.siblingSelection, currentPetCount: data.currentPetCount, description: data.description }); }); } handleFormSubmit() { // 提交逻辑写在这 } handleClearForm() { // 清除表单逻辑写在这 } render() { return (); }
我们勾勒出了应用基础结构,接下来我们一起浏览下每个子组件的细节。
该组件可以是 text 或 number 输入框,这取决于传入的 props。通过 React 的 PropTypes,我们可以非常好地记录组件拿到的 props。如果漏传 props 或传入错误的数据类型, 浏览器的控制台中会出现警告信息。
下面列举
SingleInput.propTypes = { inputType: React.PropTypes.oneOf(["text", "number"]).isRequired, title: React.PropTypes.string.isRequired, name: React.PropTypes.string.isRequired, controlFunc: React.PropTypes.func.isRequired, content: React.PropTypes.oneOfType([ React.PropTypes.string, React.PropTypes.number, ]).isRequired, placeholder: React.PropTypes.string, };
PropTypes 声明了 prop 的类型(string、 number、 array、 object 等等),其中包括了必需(isRequired)和非必需的 prop,当然它还有更多的用途(欲知更多细节,请查看 React 文档)。
下面我们逐个讨论这些 PropType:
inputType:接收两个字符串:"text" 或 "number"。该设置指定渲染 组件或 组件。
title:接收一个字符串,我们将它渲染到输入框的 label 元素中。
name:输入框的 name 属性。
controlFunc:它是从父组件或容器组件传下来的方法。因为该方法挂载在 React 的 onChange 处理方法上,所以每当输入框的输入值改变时,该方法都会被执行,从而更新父组件或容器组件的 state。
content:输入框内容。受控输入框只会显示通过 props 传入的数据。
placeholder:输入框的占位符文本,是一个字符串。
既然该组件不需要任何逻辑行为和内部 state,那我们可以将它写成纯函数组件(pure functional component)。我们将纯函数组件赋值给一个 const 常量上。下面是
import React from "react"; const SingleInput = (props) => (); SingleInput.propTypes = { inputType: React.PropTypes.oneOf(["text", "number"]).isRequired, title: React.PropTypes.string.isRequired, name: React.PropTypes.string.isRequired, controlFunc: React.PropTypes.func.isRequired, content: React.PropTypes.oneOfType([ React.PropTypes.string, React.PropTypes.number, ]).isRequired, placeholder: React.PropTypes.string, }; export default SingleInput;
接着,我们用 handleFullNameChange 方法(它被传入到 controlFunc prop 属性)来更新
// FormContainer.js handleFullNameChange(e) { this.setState({ ownerName: e.target.value }); } // constructor 方法里别漏掉了这行: // this.handleFullNameChange = this.handleFullNameChange.bind(this);
随后我们将容器组件更新后的 state (译注:这里指 state 上挂载的 ownerName 属性)通过 content prop 传回
选择组件(就是下拉选择组件),接收以下 props:
Select.propTypes = { name: React.PropTypes.string.isRequired, options: React.PropTypes.array.isRequired, selectedOption: React.PropTypes.string, controlFunc: React.PropTypes.func.isRequired, placeholder: React.PropTypes.string };
name:填充表单元素上 name 属性的字符串变量。
options:是一个数组(本例是字符串数组)。通过在组件的 render 方法中使用 props.options.map(), 该数组中的每一项都会被渲染成一个选择项。
selectedOption:用以显示表单填充的默认选项,或用户已选择的选项(例如当用户编辑之前已提交过的表单数据时,可以使用这个 prop)。
controlFunc:它是从父组件或容器组件传下来的方法。因为该方法挂载在 React 的 onChange 处理方法上,所以每当改变选择框组件的值时,该方法都会被执行,从而更新父组件或容器组件的 state。
placeholder:作为占位文本的字符串,用来填充第一个 标签。本组件中,我们将第一个选项的值设置成空字符串(参看下面代码的第 10 行)。
import React from "react"; const Select = (props) => (); Select.propTypes = { name: React.PropTypes.string.isRequired, options: React.PropTypes.array.isRequired, selectedOption: React.PropTypes.string, controlFunc: React.PropTypes.func.isRequired, placeholder: React.PropTypes.string }; export default Select;
请注意 option 标签中的 key 属性(第 14 行)。React 要求被重复操作渲染的每个元素必须拥有独一无二的 key 值,我们这里的 .map() 方法就是所谓的重复操作。既然选择项数组中的每个元素是独有的,我们就把它们当成 key prop。该 key 值协助 React 追踪 DOM 变化。虽然在循环操作或 mapping 时忘加 key 属性不会中断应用,但是浏览器的控制台里会出现警告,并且渲染性能将受到影响。
以下是控制选择框组件(记住,该组件存在于
// FormContainer.js handleAgeRangeSelect(e) { this.setState({ ownerAgeRangeSelection: e.target.value }); } // constructor 方法里别漏掉了这行: // this.handleAgeRangeSelect = this.handleAgeRangeSelect.bind(this);
让我们深入 PropTypes 来更好地理解
CheckboxGroup.propTypes = { title: React.PropTypes.string.isRequired, type: React.PropTypes.oneOf(["checkbox", "radio"]).isRequired, setName: React.PropTypes.string.isRequired, options: React.PropTypes.array.isRequired, selectedOptions: React.PropTypes.array, controlFunc: React.PropTypes.func.isRequired };
title:一个字符串,用以填充单选或复选框集合的 label 标签内容。
type:接收 "checkbox" 或 "radio" 两种配置的一种,并用指定的配置渲染输入框(译注:这里指复选输入框或单选输入框)。
setName:一个字符串,用以填充每个单选或复选框的 name 属性值。
options:一个由字符串元素组成的数组,数组元素用以渲染每个单选框或复选框的值和 label 的内容。例如,["dog", "cat", "pony"] 数组中的元素将会渲染三个单选框或复选框。
selectedOptions:一个由字符串元素组成的数组,用来表示预选项。在示例 4 中,如果 selectedOptions 数组包含 "dog" 和 "pony" 元素,那么相应的两个选项会被渲染成选中状态,而 "cat" 选项则被渲染成未选中状态。当用户提交表单时,该数组将会是用户的选择数据。
controlFunc:一个方法,用来处理从 selectedOptions 数组 prop 中添加或删除字符串的操作。
这是本表单应用中最有趣的组件,让我们来看一下:
import React from "react"; const CheckboxOrRadioGroup = (props) => (); CheckboxOrRadioGroup.propTypes = { title: React.PropTypes.string.isRequired, type: React.PropTypes.oneOf(["checkbox", "radio"]).isRequired, setName: React.PropTypes.string.isRequired, options: React.PropTypes.array.isRequired, selectedOptions: React.PropTypes.array, controlFunc: React.PropTypes.func.isRequired }; export default CheckboxOrRadioGroup;{props.options.map(opt => { return ( ); })}
checked={ props.selectedOptions.indexOf(option) > -1 } 这一行代码表示单选框或复选框是否被选中的逻辑。
属性 checked 接收一个布尔值,用来表示 input 组件是否应该被渲染成选中状态。我们在检查到 input 的值是否是 props.selectedOptions 数组的元素之一时生成该布尔值。
myArray.indexOf(item) 方法返回 item 在数组中的索引值。如果 item 不在数组中,返回 -1,因此,我们写了 > -1。
注意,0 是一个合法的索引值,所以我们需要 > -1 ,否则代码会有 bug。如果没有 > -1,selectedOptions 数组中的第一个 item —— 其索引为 0 —— 将永远不会被渲染成选中状态,因为 0 是一个类 false 的值(译注:在 checked 属性中,0 会被当成 false 处理)。
本组件的处理方法同样比其他的有趣。
handlePetSelection(e) { const newSelection = e.target.value; let newSelectionArray; if(this.state.selectedPets.indexOf(newSelection) > -1) { newSelectionArray = this.state.selectedPets.filter(s => s !== newSelection) } else { newSelectionArray = [...this.state.selectedPets, newSelection]; } this.setState({ selectedPets: newSelectionArray }); }
如同所有处理方法一样,事件对象被传入方法,这样一来我们就能拿到事件对象的值(译注:准确来说,应该是事件目标元素的值)。我们将该值赋给newSelection 常量。接着我们在函数顶部附近定义 newSelectionArray 变量。因为我们将在一个 if/else 代码块里对该变量进行赋值,所以用 let 而非 const 来定义它。我们在代码块外部进行定义,这样一来被定义变量的作用域就是函数内部的最外沿,并且函数内的代码块都能访问到外部定义的变量。
该方法需要处理两种可能的情况。
如果 input 组件的值不在 selectedOptions 数组中,我们要将值添加进该数组。
如果 input 组件的值在 selectedOptions 数组中,我们要从数组中删除该值。
添加(第 8 - 10 行): 为了将新值添加进选项数组,我们通过解构旧数组(数组前的三点...表示解构)创建一个新数组,并且将新值添加到数组的尾部 newSelectionArray = [...this.state.selectedPets, newSelection];。
注意,我们创建了一个新数组,而不是通过类似 .push() 的方法来改变原数组。不改变已存在的对象和数组,而是创建新的对象和数组,这在 React 中是又一个最佳实践。开发者这样做可以更容易地跟踪 state 的变化,而第三方 state 管理库,如 Redux 则可以做高性能的浅比较,而不是阻塞性能的深比较。
删除(第 6 - 8 行):if 代码块借助此前用到的 .indexOf() 小技巧,检查选项是否在数组中。如果选项已经在数组中,通过.filter()方法,该选项将被移除。 该方法返回一个包含所有满足 filter 条件的元素的新数组(记住要避免在 React 直接修改数组或对象!)。
newSelectionArray = this.state.selectedPets.filter(s => s !== newSelection)
在这种情况下,除了传入到方法中的选项之外,其他选项都会被返回。
组件和我们已提到的那些组件非常相似,除了 resize 和 rows,目前你应该对它的 props 很熟悉了。
TextArea.propTypes = { title: React.PropTypes.string.isRequired, rows: React.PropTypes.number.isRequired, name: React.PropTypes.string.isRequired, content: React.PropTypes.string.isRequired, resize: React.PropTypes.bool, placeholder: React.PropTypes.string, controlFunc: React.PropTypes.func.isRequired };
title:接收一个字符串,用以渲染文本域的 label 标签内容。
rows:接收一个整数,用来指定文本域的行数。
name:文本域的 name 属性。
content:文本域的内容。受控组件只会显示通过 props 传入的数据。
resize: 接受一个布尔值,用来指定文本域能否调整大小。
placeholder:充当文本域占位文本的字符串。
controlFunc: 它是从父组件或容器组件传下来的方法。因为该方法挂载在 React 的 onChange 处理方法上,所以每当改变选择框组件的值时,该方法都会被执行,从而更新父组件或容器组件的 state。
组件的完整代码:
import React from "react"; const TextArea = (props) => (); TextArea.propTypes = { title: React.PropTypes.string.isRequired, rows: React.PropTypes.number.isRequired, name: React.PropTypes.string.isRequired, content: React.PropTypes.string.isRequired, resize: React.PropTypes.bool, placeholder: React.PropTypes.string, controlFunc: React.PropTypes.func.isRequired }; export default TextArea;
handleClearForm 和 handleFormSubmit 方法操作整个表单。
1. handleClearForm既然我们在表单的各处都使用了单向数据流,那么清除表单数据对我们来说也是小菜一碟。
清除表单子组件中显示的数据很简单,只要把容器的 state (译注:这里是指 state 对象上挂载的各个变量)设置成空数组和空字符串就可以了(如果有数字输入框的话则是将值设置成 0)。
handleClearForm(e) { e.preventDefault(); this.setState({ ownerName: "", selectedPets: [], ownerAgeRangeSelection: "", siblingSelection: [], currentPetCount: 0, description: "" }); }
注意,e.preventDefault() 阻止了页面重新加载,接着 setState() 方法用来清除表单数据。
2. handleFormSubmit为了提交表单数据,我们从 state 中抽取需要提交的属性值,创建了一个对象。接着使用 AJAX 库或技术将这些数据发送给 API(本文不包含此类内容)。
handleFormSubmit(e) { e.preventDefault(); const formPayload = { ownerName: this.state.ownerName, selectedPets: this.state.selectedPets, ownerAgeRangeSelection: this.state.ownerAgeRangeSelection, siblingSelection: this.state.siblingSelection, currentPetCount: this.state.currentPetCount, description: this.state.description }; console.log("Send this in a POST request:", formPayload); this.handleClearForm(e); }
请注意我们在提交数据后执行 this.handleClearForm(e) 清除了表单。
表单校验受控表单组件非常适合自定义表单校验。假设要从 组件中排除字母 "e",可以这样做:
handleDescriptionChange(e) { const textArray = e.target.value.split("").filter(x => x !== "e"); console.log("string split into array of letters",textArray); const filteredText = textArray.join(""); this.setState({ description: filteredText }); }
把 e.target.value 字符串分割成字母数组,就生成了上述的 textArray。这样字母 “e” (或其他设法排除的字母)就被过滤掉了。再把剩余的字母组成的数组拼成字符串,最后用该新字符串去设置组件 state。还不错吧?
以上代码放在本文的仓库中,但我将它们注释掉了,你可以按自己的需求自由地调整。
下面是我承诺给你们的
import React, {Component} from "react"; import CheckboxOrRadioGroup from "../components/CheckboxOrRadioGroup"; import SingleInput from "../components/SingleInput"; import TextArea from "../components/TextArea"; import Select from "../components/Select"; class FormContainer extends Component { constructor(props) { super(props); this.state = { ownerName: "", petSelections: [], selectedPets: [], ageOptions: [], ownerAgeRangeSelection: "", siblingOptions: [], siblingSelection: [], currentPetCount: 0, description: "" }; this.handleFormSubmit = this.handleFormSubmit.bind(this); this.handleClearForm = this.handleClearForm.bind(this); this.handleFullNameChange = this.handleFullNameChange.bind(this); this.handleCurrentPetCountChange = this.handleCurrentPetCountChange.bind(this); this.handleAgeRangeSelect = this.handleAgeRangeSelect.bind(this); this.handlePetSelection = this.handlePetSelection.bind(this); this.handleSiblingsSelection = this.handleSiblingsSelection.bind(this); this.handleDescriptionChange = this.handleDescriptionChange.bind(this); } componentDidMount() { // 模拟请求用户数据 //(create-react-app 构建配置里包含了 fetch 的 polyfill) fetch("./fake_db.json") .then(res => res.json()) .then(data => { this.setState({ ownerName: data.ownerName, petSelections: data.petSelections, selectedPets: data.selectedPets, ageOptions: data.ageOptions, ownerAgeRangeSelection: data.ownerAgeRangeSelection, siblingOptions: data.siblingOptions, siblingSelection: data.siblingSelection, currentPetCount: data.currentPetCount, description: data.description }); }); } handleFullNameChange(e) { this.setState({ ownerName: e.target.value }); } handleCurrentPetCountChange(e) { this.setState({ currentPetCount: e.target.value }); } handleAgeRangeSelect(e) { this.setState({ ownerAgeRangeSelection: e.target.value }); } handlePetSelection(e) { const newSelection = e.target.value; let newSelectionArray; if(this.state.selectedPets.indexOf(newSelection) > -1) { newSelectionArray = this.state.selectedPets.filter(s => s !== newSelection) } else { newSelectionArray = [...this.state.selectedPets, newSelection]; } this.setState({ selectedPets: newSelectionArray }); } handleSiblingsSelection(e) { this.setState({ siblingSelection: [e.target.value] }); } handleDescriptionChange(e) { this.setState({ description: e.target.value }); } handleClearForm(e) { e.preventDefault(); this.setState({ ownerName: "", selectedPets: [], ownerAgeRangeSelection: "", siblingSelection: [], currentPetCount: 0, description: "" }); } handleFormSubmit(e) { e.preventDefault(); const formPayload = { ownerName: this.state.ownerName, selectedPets: this.state.selectedPets, ownerAgeRangeSelection: this.state.ownerAgeRangeSelection, siblingSelection: this.state.siblingSelection, currentPetCount: this.state.currentPetCount, description: this.state.description }; console.log("Send this in a POST request:", formPayload) this.handleClearForm(e); } render() { return (); } } export default FormContainer; 总结
我承认用 React 构建受控表单组件要做一些重复劳动(比如容器组件中的处理方法),但就你对应用的掌控度和 state 变更的透明度来说,预先投入精力是超值的。你的代码会变得可维护并且很高效。
如果想在我发布新文章时接到通知,你可以在博客的导航栏部分注册我的邮件发送清单。
iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/91743.html
摘要:函数更新属性,进而更新元素的值。由于箭头函数存在于父组件中,所以中的指向父组件。一旦表单被提交,的值就被设置为。遗憾的是,没有节点是包含了集合的。在这种情况下,这个节点列表包含三个节点和被选中的值。 原文地址:React Forms: Using Refs 原文作者:Loren Stewart 译者:萌萌 校对者:小 boy React 提供了两种从 元素中获取值的标准方法。第一...
摘要:前端日报精选中的垃圾收集,图文指南十个免费的前端开发工具专题之递归如何在链中共享变量基于的爬虫框架中文译十六进制颜色揭秘掘金掘金小书基本环境安装小书教程中间件对闭包的一个巧妙使用简书源码分析掘金组件开发练习焦点图切换前端学 2017-09-13 前端日报 精选 V8 中的垃圾收集(GC),图文指南十个免费的web前端开发工具JavaScript专题之递归 · Issue #49 · m...
2017-07-28 前端日报 精选 React的新引擎—React Fiber是什么?Chromeless 让 Chrome 自动化变得简单【译】JavaScript属性名称中的隐藏信息前端测试框架 JestES6中的JavaScript工厂函数Why Composition is Harder with ClassesGET READY: A NEW V8 IS COMING, NODE.JS...
摘要:首次发表在个人博客受控组件或都要绑定一个事件每当表单的状态发生变化都会被写入组件的中这种组件在中被称为受控组件在受控组件中组件渲染出的状态与它的或者向对应通过这种方式消除了组件的局部状态是的应用的整个状态可控官方同样推荐使用受控表单组件总结 首次发表在个人博客 受控组件 { this.setState({ value: e.target.val...
摘要:假如我们从后台拉取一个数据要填入输入框,那么必须得使用受控组件,因为非受控组件只能被用户输入。不影响正常输入填充该输入框的默认值,此时不显示内容。 网页中使用的form表单大家肯定都再熟悉不过了,它主要作用是用来收集和提交信息。React中的表单组件与我们普通的Html中的表单及其表现形式没有什么不同,所以如何使用表单我觉得再拿出来说可能是画蛇添足、毫无意义。不过再怎么样也不能辜负大家...
阅读 2626·2021-11-23 09:51
阅读 859·2021-09-24 10:37
阅读 3611·2021-09-02 15:15
阅读 1961·2019-08-30 13:03
阅读 1881·2019-08-29 15:41
阅读 2624·2019-08-29 14:12
阅读 1423·2019-08-29 11:19
阅读 3300·2019-08-26 13:39