表单说明
- 有以下示例代码:
import React, { Component } from "react";
class App extends Component {
state = {
msg: "hello world",
};
render() {
return (
<div>
<input type="text" value={this.state.msg} />
</div>
);
}
}
export default App;
- 通过运行后我们可以在浏览器的
consle
控制台找到React给予我们的提示:
- 通过上述的警告提示,我们可以得知,在React中并不存在类似于Vue的双向数据绑定操作。此处需要注意以下几点:
1. Vue中的`v-model`是语法糖
2. 在React里使用的是**单向**数据流
- 由于在React里数据流是单向的,所以我们就必须得考虑一个问题:怎么获取用户在表单中输入的数据呢?解决办法:
3. 给表单项添加`onChange`事件【受控组件】
4. 给表单项的value/checked,设置成defaultValue/defaultChecked【非受控组件】
- React推荐我们使用受控组件。
受控组件(推荐)
1. 在HTML中:
- 表单元素(如`input`、`textarea`、`select`)通常自己维护state,并且根据用户输入进行更新
2. 在React中:
- 可改变状态通常保存在组件的state属性中,并且只能通过使用`setState()`来更新
3. 两者结合:
- 使React的state成为唯一数据源,React组件控制着用户输入过程中表单发送的操作
- 将
state
与表单项中的value
值绑定在一起,由state
的值来控制表单元素的值,称为受控组件。
绑定步骤:
1. 在state中添加一个状态,作为表单元素的value值
2. 给表单元素绑定change事件,将表单元素的值设置为state的值
所谓受控组件,顾名思义就是受React控制的组件。其特点是将state在表单中的更新,通过onChange事件来进行操作。
- 受控组件必须要求表单项中要有俩个组合的情况:
1. 针对普通的文本框、多行文本框等:
- value属性+onChange事件类型
2. 针对单选按钮组/复选框:
- checked属性+onChange事件类型
- 每次onChange事件的触发都会重新渲染页面
- 在受控组件上指定 value 的 prop 会阻止用户更改输入。
- 如果你指定了 value,但输入仍可编辑,则可能是你意外地将value 设置为 undefined 或 null
<input value={null} />
案例分析
state中约定一个用户的基本信息,要求将信息展示在表单中,允许被用户修改,点击提交按钮获取用户修改后的最新的信息(state中的)
import React, { Component } from "react";
class App extends Component {
// 初始化用户信息(项目中的话信息是来自于ajax)
state = {
userInfo: {
uname: "zhangsan",
mail: "zhangsan@1000phone.com",
// 社交属性性别
gender: "女",
hobbies: ["1", "2"],
},
};
render() {
// 先结构用户信息,用起来方便
let { uname, mail, gender, hobbies } = this.state.userInfo;
return (
<div>
<div>
昵称:
<input type="text" name="uname" value={uname} onChange={this.vModel.bind(this)} />
</div>
<div>
邮箱:
<input type="text" name="mail" value={mail} onChange={this.vModel.bind(this)} />
</div>
<div>
性别:
<input type="radio" name="gender" value="男" checked={gender === "男" ? true : false} onChange={this.vModel.bind(this)} />男
<input type="radio" name="gender" value="女" checked={gender === "女" ? true : false} onChange={this.vModel.bind(this)} />女
</div>
<div>
爱好:
<input type="checkbox" name="hobbies" value="1" checked={hobbies.includes("1") ? true : false} onChange={this.vModel.bind(this)} />
吃饭
<input type="checkbox" name="hobbies" value="2" checked={hobbies.includes("2") ? true : false} onChange={this.vModel.bind(this)} />
睡觉
<input type="checkbox" name="hobbies" value="3" checked={hobbies.includes("3") ? true : false} onChange={this.vModel.bind(this)} />
撸代码
</div>
<button onClick={this.submit.bind(this)}>提交</button>
</div>
);
}
// 修改用户名
// chgUname(e) {
// // 修改state中的数据
// // 获取最新的数据
// let val = e.target.value;
// // 获取要改的属性名
// let name = e.target.name;
// // 修改数据
// this.setState((state) => {
// // 对象有一个特点:引用传值
// state.userInfo[name] = val;
// return state;
// });
// }
// // 修改邮箱
// chgMail(e) {
// let val = e.target.value;
// // 获取要改的属性名
// let name = e.target.name;
// // 修改数据
// this.setState((state) => {
// // 对象有一个特点:引用传值
// state.userInfo[name] = val;
// return state;
// });
// }
// 封装受控组件的表单数据与state的绑定关系
vModel(e) {
// 修改state中的数据
// 获取最新的数据
let val = e.target.value;
// 获取要改的属性名
let name = e.target.name;
// 修改数据
this.setState((state) => {
// 判断是否是复选框的处理
if (e.target.type === "checkbox") {
// 多选
// console.log('走了多选');
// 思路:先去判断当前的值在原来的数据中是否存在,存在则移除,不存在则添加
let index = state.userInfo[name].indexOf(val);
if (index > -1) {
state.userInfo[name].splice(index, 1);
} else {
state.userInfo[name].push(val);
}
} else {
// 对象有一个特点:引用传值
state.userInfo[name] = val;
}
return state;
});
}
// 提交表单
submit() {
console.log(this.state.userInfo);
}
}
export default App;
- 运行结果:
- 注意点:
如果需要将原来应该分别处理的事件合在一起去写的话,
一定需要多传递一个参数用于记录当前修改的是state中的哪个值,
此时可以选择一些方案的任意一种:
1. 通过相同 name 属性值去辨别,给每个表单项设置与 state 中相同的 key 名的 name 值,然后通过事件对象去获取
2. 可以直接在事件绑定的位置传递标记,例如:
onChange = {this.changeHandler.bind(this,'username')}
------------------------------------------
表单项中表单类型为 checkbox 的比较特殊,与其他类型的不同,需要特殊处理(取反操作,而其余的表单项是来什么值用什么值)
input 标签
- 由于在表单元素上设置了
value
属性,因此显示的值将始终为this.state.value
,这使得react的state成为唯一数据源。 - 由于
handlechange
在每次按键时都会执行并更新React的state,因此显示的值将随用户输入而更新 - 对于受控组件来说,输入的值始终由React的state驱动。
- 你也可以将value传递给其他UI元素,或者通过其他事件处理函数重置,但这意味着你需要写更多的代码
- 例如:在表单提交是打印出名字
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};
}
handleChange=(event)=> {
this.setState({value: event.target.value});
}
handleSubmit=(event)=> {
alert('提交的名字: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
名字:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="提交" />
</form>
);
}
}
- 当需要处理多个
input
元素时, - 我们可以给每个元素添加
name
属性, - 并让处理函数根据
event.target.name
的值选择要执行的操作。
class Reservation extends React.Component {
constructor(props) {
super(props);
this.state = {
isGoing: true,
numberOfGuests: 2
};
}
handleInputChange=(event)=> {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
this.setState({
[name]: value
});
}
render() {
return (
<form>
<label>
参与:
<input
name="isGoing"
type="checkbox"
checked={this.state.isGoing}
onChange={this.handleInputChange} />
</label>
<br />
<label>
来宾人数:
<input
name="numberOfGuests"
type="number"
value={this.state.numberOfGuests}
onChange={this.handleInputChange} />
</label>
</form>
);
}
}
- 运行结果:
textare 标签
- 在HTML中,
textarea
元素通过其子元素定义其文本
<textarea>
你好, 这是在 text area 里的文本
</textarea>
- 而在React中,
textarea
使用value
属性替代。 - 这样使得使用
textarea
的表单和使用单行input的表单非常类似
class EssayForm extends React.Component {
constructor(props) {
super(props);
this.state = {
value: '请撰写一篇关于你喜欢的 DOM 元素的文章.'
};
}
handleChange=(event)=> {
this.setState({value: event.target.value});
}
handleSubmit=(event)=> {
alert('提交的文章: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
文章:
<textarea value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="提交" />
</form>
);
}
}
-
运行结果:
-
注意:
this.state.value
初始化与构造函数中,因此文本域默认有初始值
select 标签
- 在HTML中,
select
创建下拉列表标签。 - 例如:
<select>
<option value="grapefruit">葡萄柚</option>
<option value="lime">酸橙</option>
<option selected value="coconut">椰子</option>
<option value="mango">芒果</option>
</select>
- 由于
selected
属性的缘故,椰子选项默认别选中 - React并不会使用
selected
属性,而是在根select
标签上使用value
属性。 - 这在受控组件中更便捷,因为你只需要在根标签中更新他
class FlavorForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: 'coconut'};
}
handleChange=(event)=> {
this.setState({value: event.target.value});
}
handleSubmit=(event)=> {
alert('你喜欢的风味是: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
选择你喜欢的风味:
<select value={this.state.value} onChange={this.handleChange}>
<option value="grapefruit">葡萄柚</option>
<option value="lime">酸橙</option>
<option value="coconut">椰子</option>
<option value="mango">芒果</option>
</select>
</label>
<input type="submit" value="提交" />
</form>
);
}
}
-
运行结果:
-
总的来说,这使得,
<input type="text">
,<textarea>
和<select>
之类的标签都非常相似 -
它们都接受一个
value
属性,你可以使用它来实现受控组件。
- 你可以在数组传递到
value
属性中,以支持在select
标签中选择多个选项 <select multiple={true} value={['B', 'C']}>
class MulFlavorForm extends React.Component {
constructor(props) {
super(props);
this.state = {
value: "coconut",
arr: [],
options: [
{ value: "grapefruit", label: "葡萄柚" },
{ value: "lime", label: "酸橙" },
{ value: "coconut", label: "椰子" },
{ value: "mango", label: "芒果" }
]
};
}
handleChange=(e)=>{
let idx = this.state.arr.findIndex(item=>{
return item === e.target.value
})
if (idx >= 0) {
this.state.arr.splice(idx,1);
} else {
this.state.arr.push(e.target.value);
}
let arr = this.state.arr;
this.setState({arr});
}
render() {
return (
<div>
<select multiple={true} value={this.state.arr} onChange={this.handleChange}>
{this.state.options.map((item,index) => {
return <option value={item.value} key={index}>{item.label}</option>;
})}
</select>
</div>
);
}
}
- 运行结果
非受控组件
- 非受控组件:指的是不受react控制的组件
- 注意点:
1. 关于ref:
它是react中用户获取组件/元素对象的一个对象,
在使用时需要通过 createRef() 进行创建
然后将创建好可变数据绑定到组件/元素上
后续根据元素的类型获取到对应信息:
- 如果元素是html标签的话,则获取到的是dom信息
- 如果元素是组件标签的话,则获取到的是该组件对应的组件实例(整个实例,是一个对象)
2. 非受控组件在实际表单处理应用中,获取到的都是dom对象,也就是说非受控组件基本使用的都是dom操作
3. 非受控组件操作的时候需要配合:
defaultValue 和 defaultChecked 属性来实现,
该操作不会像受控组件一样,每次改变数据导致页面组件重新渲染
4. 在使用ref的时候如果一个ref要对应多个表单项(如单选按钮组/复选框),
则不能给每个元素都加ref,需要给其公共父去加
因为非受控组件将真实数据储存在 DOM 节点中,这时表单数据将交由 DOM 节点来处理。需要借助
ref
对象,来实现对表单项的操作,来从 DOM 节点中获取表单数据。
使用步骤:
1. 调用React.createRef()方法创建ref对象
2. 将创建好的ref对象添加到文本框中
3. 通过ref对象获取到文本框的值
- 一般表单项少的时候可以考虑使用非受控组件。
class NameForm extends React.Component {
constructor(props) {
super(props);
this.input = React.createRef();
}
handleSubmit=(event) =>{
alert('A name was submitted: ' + this.input.current.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" ref={this.input} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
案例分析
约定一个用户的基本信息,要求将信息展示在表单中,允许被用户修改,点击提交按钮获取用户修改后的最新的信息
import React, { Component, createRef } from "react";
class App extends Component {
constructor(props) {
super(props);
// 创建ref对象
this.ref_uname = createRef();
this.ref_mail = createRef();
this.ref_gender = createRef();
this.ref_hobbies = createRef();
// ref对象是后续获取表单最新数据的唯一途径
// 建议性操作,将所有的ref丢到一个数组中(后续可以循环取值)
this.refArr = [this.ref_uname, this.ref_mail, this.ref_gender, this.ref_hobbies];
}
// 初始化用户信息(项目中的话信息是来自于ajax)
state = {
userInfo: {
uname: "zhangsan",
mail: "zhangsan@1000phone.com",
// 社交属性性别
gender: "女",
hobbies: ["1", "2"],
},
};
render() {
console.log("走了render");
// 先结构用户信息,用起来方便
let { uname, mail, gender, hobbies } = this.state.userInfo;
return (
<div>
<div>
昵称:
<input type="text" ref={this.ref_uname} name="uname" defaultValue={uname} />
</div>
<div>
邮箱:
<input type="text" ref={this.ref_mail} name="mail" defaultValue={mail} />
</div>
<div ref={this.ref_gender}>
性别:
<input type="radio" name="gender" value="男" defaultChecked={gender === "男" ? true : false} />男
<input type="radio" name="gender" value="女" defaultChecked={gender === "女" ? true : false} />女
</div>
<div ref={this.ref_hobbies}>
爱好:
<input type="checkbox" name="hobbies" value="1" defaultChecked={hobbies.includes("1") ? true : false} />
吃饭
<input type="checkbox" name="hobbies" value="2" defaultChecked={hobbies.includes("2") ? true : false} />
睡觉
<input type="checkbox" name="hobbies" value="3" defaultChecked={hobbies.includes("3") ? true : false} />
撸代码
</div>
<button onClick={this.submit.bind(this)}>提交</button>
</div>
);
}
// 提交:在点击之后获取最新的表单数据
submit() {
// 用于保存所有的数据,此处不能使用state及setState(一旦使用就是受控组件)
let data = { userInfo: {} };
// console.log(this.ref_uname.current);
// 循环所有的ref逐个进行处理
this.refArr.forEach((ref) => {
// 获取到当前的dom对象
let obj = ref.current;
// 判断是否存在多个子
if (obj.children.length > 0) {
// 多个元素(还需要判断是单选还是复选)
for (var i = 0; i < obj.children.length; i++) {
if (obj.children[i].type === "radio" && obj.children[i].checked) {
// 将单选按钮组中选中的那个值赋给data
data.userInfo[obj.children[i].name] = obj.children[i].value;
}
if (obj.children[i].type === "checkbox" && obj.children[i].checked) {
// 将多选框中选中的值保留
if (!data.userInfo[obj.children[i].name]) {
data.userInfo[obj.children[i].name] = [];
}
data.userInfo[obj.children[i].name].push(obj.children[i].value);
}
}
} else {
// 单个元素
let name = obj.name;
let val = obj.value;
data.userInfo[name] = val;
}
});
// 最终的data就是需要提交的数据
console.log(data);
}
}
export default App;
默认值
非受控组件不能给表单项加
value
属性,一旦加了它就不是非受控组件了,它就成了受控组件了。如果有默认值请通过defaultValue
属性进行输出。
- 在 React 渲染生命周期时,表单元素上的
value
将会覆盖 DOM 节点中的值, - 在非受控组件中,你经常希望 React 能赋予组件一个初始值,但是不去控制后续的更新
- 在这种情况下, 你可以指定一个
defaultValue
属性,而不是value
。
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input
defaultValue="Bob"
type="text"
ref={this.input} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
- 同样,
<input type="checkbox">
和<input type="radio">
支持defaultChecked
,<select>
和<textarea>
支持defaultValue
。
文件输入
- 在 HTML 中,
<input type="file">
允许用户从存储设备中选择一个或多个文件,将其上传到服务器,或通过使用 JavaScript 的 File API 进行控制。 - 在 React 中,
<input type="file">
始终是一个非受控组件,因为它的值只能由用户设置,而不能通过代码控制。 - 您应该使用 File API 与文件进行交互
import React, { Component } from "react";
class App extends Component {
fileRef = React.createRef()
imgRef = React.createRef()
getData = () => {
const file = this.fileRef.current.files[0]
const reader = new FileReader()
reader.readAsDataURL(file)
const _this = this
// 注意this的指向
reader.onload = function () {
_this.imgRef.current.src = this.result
}
}
render() {
return (
<div>
<input type="file" ref={ this.fileRef }/>
<button onClick = { this.getData }>预览图片</button>
<img src="" ref={ this.imgRef } alt=""/>
</div>
)
}
}
export default App;
- 运行结果: