react 项目实战(八)图书管理与自动完成
图书管理
src / pages / BookAdd.js // 图书添加页
/**
* 图书添加页面
*/
import React from 'react';
// 布局组件
import HomeLayout from '../layouts/HomeLayout';
// 编辑组件
import BookEditor from '../components/BookEditor'; class BookAdd extends React.Component {
render() {
return (
<HomeLayout title="添加图书">
<BookEditor />
</HomeLayout>
);
}
} export default BookAdd;
src / pages / BookList.js // 图书列表页
/**
* 图书列表页面
*/
import React from 'react';
// 布局组件
import HomeLayout from '../layouts/HomeLayout';
// 引入 prop-types
import PropTypes from 'prop-types'; class BookList extends React.Component {
// 构造器
constructor(props) {
super(props);
// 定义初始化状态
this.state = {
bookList: []
};
} /**
* 生命周期
* componentWillMount
* 组件初始化时只调用,以后组件更新不调用,整个生命周期只调用一次
*/
componentWillMount(){
// 请求数据
fetch('http://localhost:8000/book')
.then(res => res.json())
.then(res => {
/**
* 成功的回调
* 数据赋值
*/
this.setState({
bookList: res
});
});
} /**
* 编辑
*/
handleEdit(book){
// 跳转编辑页面
this.context.router.push('/book/edit/' + book.id);
} /**
* 删除
*/
handleDel(book){
// 确认框
const confirmed = window.confirm(`确认要删除书名 ${book.name} 吗?`);
// 判断
if(confirmed){
// 执行删除数据操作
fetch('http://localhost:8000/book/' + book.id, {
method: 'delete'
})
.then(res => res.json())
.then(res => {
/**
* 设置状态
* array.filter
* 把Array的某些元素过滤掉,然后返回剩下的元素
*/
this.setState({
bookList: this.state.bookList.filter(item => item.id !== book.id)
});
alert('删除用户成功');
})
.catch(err => {
console.log(err);
alert('删除用户失败');
});
}
} render() {
// 定义变量
const { bookList } = this.state; return (
<HomeLayout title="图书列表">
<table>
<thead>
<tr>
<th>图书ID</th>
<th>图书名称</th>
<th>价格</th>
<th>操作</th>
</tr>
</thead> <tbody>
{
bookList.map((book) => {
return (
<tr key={book.id}>
<td>{book.id}</td>
<td>{book.name}</td>
<td>{book.price}</td>
<td>
<a onClick={() => this.handleEdit(book)}>编辑</a>
<a onClick={() => this.handleDel(book)}>删除</a>
</td>
</tr>
);
})
}
</tbody>
</table>
</HomeLayout>
);
}
} /**
* 任何使用this.context.xxx的地方,必须在组件的contextTypes里定义对应的PropTypes
*/
BookList.contextTypes = {
router: PropTypes.object.isRequired
}; export default BookList;
src / components / BookEditor.js // 图书编辑组件
/**
* 图书编辑器组件
*/
import React from 'react';
import FormItem from '../components/FormItem'; // 或写成 ./FormItem
// 高阶组件 formProvider表单验证
import formProvider from '../utils/formProvider';
// 引入 prop-types
import PropTypes from 'prop-types'; class BookEditor extends React.Component {
// 按钮提交事件
handleSubmit(e){
// 阻止表单submit事件自动跳转页面的动作
e.preventDefault();
// 定义常量
const { form: { name, price, owner_id }, formValid, editTarget} = this.props; // 组件传值
// 验证
if(!formValid){
alert('请填写正确的信息后重试');
return;
} // 默认值
let editType = '添加';
let apiUrl = 'http://localhost:8000/book';
let method = 'post';
// 判断类型
if(editTarget){
editType = '编辑';
apiUrl += '/' + editTarget.id;
method = 'put';
} // 发送请求
fetch(apiUrl, {
method, // method: method 的简写
// 使用fetch提交的json数据需要使用JSON.stringify转换为字符串
body: JSON.stringify({
name: name.value,
price: price.value,
owner_id: owner_id.value
}),
headers: {
'Content-Type': 'application/json'
}
})
// 强制回调的数据格式为json
.then((res) => res.json())
// 成功的回调
.then((res) => {
// 当添加成功时,返回的json对象中应包含一个有效的id字段
// 所以可以使用res.id来判断添加是否成功
if(res.id){
alert(editType + '添加图书成功!');
this.context.router.push('/book/list'); // 跳转到用户列表页面
return;
}else{
alert(editType + '添加图书失败!');
}
})
// 失败的回调
.catch((err) => console.error(err));
} // 生命周期--组件加载中
componentWillMount(){
const {editTarget, setFormValues} = this.props;
if(editTarget){
setFormValues(editTarget);
}
} render() {
// 定义常量
const {form: {name, price, owner_id}, onFormChange} = this.props;
return (
<form onSubmit={(e) => this.handleSubmit(e)}>
<FormItem label="书名:" valid={name.valid} error={name.error}>
<input
type="text"
value={name.value}
onChange={(e) => onFormChange('name', e.target.value)}/>
</FormItem> <FormItem label="价格:" valid={price.valid} error={price.error}>
<input
type="number"
value={price.value || ''}
onChange={(e) => onFormChange('price', e.target.value)}/>
</FormItem> <FormItem label="所有者:" valid={owner_id.valid} error={owner_id.error}>
<input
type="text"
value={owner_id.value || ''}
onChange={(e) => onFormChange('owner_id', e.target.value)}/>
</FormItem>
<br />
<input type="submit" value="提交" />
</form>
);
}
} // 必须给BookEditor定义一个包含router属性的contextTypes
// 使得组件中可以通过this.context.router来使用React Router提供的方法
BookEditor.contextTypes = {
router: PropTypes.object.isRequired
}; // 实例化
BookEditor = formProvider({ // field 对象
// 书名
name: {
defaultValue: '',
rules: [
{
pattern: function (value) {
return value.length > 0;
},
error: '请输入图书户名'
},
{
pattern: /^.{1,10}$/,
error: '图书名最多10个字符'
}
]
},
// 价格
price: {
defaultValue: 0,
rules: [
{
pattern: function(value){
return value > 0;
},
error: '价格必须大于0'
}
]
},
// 所有者
owner_id: {
defaultValue: '',
rules: [
{
pattern: function (value) {
return value > 0;
},
error: '请输入所有者名称'
},
{
pattern: /^.{1,10}$/,
error: '所有者名称最多10个字符'
}
]
}
})(BookEditor); export default BookEditor;
src / pages / BookEdit.js // 图书编辑页
/**
* 编辑图书页面
*/
import React from 'react';
// 布局组件
import HomeLayout from '../layouts/HomeLayout';
// 引入 prop-types
import PropTypes from 'prop-types';
// 图书编辑器组件
import BookEditor from '../components/BookEditor'; class BookEdit extends React.Component {
// 构造器
constructor(props) {
super(props);
// 定义初始化状态
this.state = {
book: null
};
} // 生命周期--组件加载中
componentWillMount(){
// 定义常量
const bookId = this.context.router.params.id;
/**
* 发送请求
* 获取用户数据
*/
fetch('http://localhost:8000/book/' + bookId)
.then(res => res.json())
.then(res => {
this.setState({
book: res
});
})
} render() {
const {book} = this.state;
return (
<HomeLayout title="编辑图书">
{
book ? <BookEditor editTarget={book} /> : '加载中...'
}
</HomeLayout>
);
}
} BookEdit.contextTypes = {
router: PropTypes.object.isRequired
}; export default BookEdit;
项目结构:
自动完成组件
找了个例子看一下效果:
可以发现,这是一个包含一个输入框、一个下拉框的复合控件。
实现一个通用组件,在动手写代码之前我会做以下准备工作:
- 确定组件结构
- 观察组件逻辑
- 确定组件内部状态(state)
- 确定组件向外暴露的属性(props)
组件结构
上面提了,这个组件由一个输入框和一个下拉框组成。
注意,这里的下拉框是一个“伪”下拉框,并不是指select与option。仔细看上面的动图,可以看得出来这个“伪”下拉框只是一个带边框的、位于输入框正下方的一个列表。
我们可以假设组件的结构是这样的:
<div>
<input type="text"/>
<ul>
<li>...</li>
...
</ul>
</div>
组件逻辑
观察动图,可以发现组件有以下行为:
- 未输入时,与普通输入框一致
- 输入改变时如果有建议的选项,则在下放显示出建议列表
- 建议列表可以使用键盘上下键进行选择,选择某一项时该项高亮显示,并且输入框的值变为该项的值
- 当移出列表(在第一项按上键或在最后一项按下键)时,输入框的值变为原来输入的值(图中的“as”)
- 按下回车键可以确定选择该项,列表消失
- 可以使用鼠标在列表中进行选择,鼠标移入的列表项高亮显示
组件内部状态
一个易用的通用组件应该对外隐藏只有内部使用的状态。使用React组件的state来维护组件的内部状态。
根据组件逻辑,我们可以确定自动完成组件需要这些内部状态:
- 逻辑2|3|4:输入框中显示的值,默认为空字符串(displayValue)
- 逻辑3|6:建议列表中高亮的项目,可以维护一个项目在列表中的索引,默认为-1(activeItemIndex)
组件暴露的属性
我们的目标是一个通用的组件,所以类似组件实际的值、推荐列表这样的状态,应该由组件的使用者来控制:
如上图,组件应向外暴露的属性有:
- value:代表实际的值(不同于上面的displayValue表示显示的、临时的值,value表示的是最终的值)
- options:代表当前组件的建议列表,为空数组时,建议列表隐藏
- onValueChange:用于在输入值或确定选择了某一项时通知使用者的回调方法,使用者可以在这个回调方法中对options、value进行更新
实现
确定了组件结构、组件逻辑、内部状态和外部属性之后,就可以着手进行编码了:
在/src/components
下新建AutoComplete.js文件,写入组件的基本代码:
/**
* 自动完成组件
*/
import React from 'react';
// 引入 prop-types
import PropTypes from 'prop-types'; class AutoComplete extends React.Component {
// 构造器
constructor(props) {
super(props);
// 定义初始化状态
this.state = {
displayValue: '',
activeItemIndex: -1
};
} // 渲染
render() {
const {displayValue, activeItemIndex} = this.state;
// 组件传值
const {value, options} = this.props;
return (
<div>
<input value={value}/>
{options.length > 0 && (
<ul>
{
options.map((item, index) => {
return (
<li key={index}>
{item.text || item}
</li>
);
})
}
</ul>
)}
</div>
);
}
} // 通用组件最好写一下propTypes约束
AutoComplete.propTypes = {
value: PropTypes.string.isRequired, // 字符串
options: PropTypes.array.isRequired, // 数组
onValueChange: PropTypes.func.isRequired // 函数
}; // 向外暴露
export default AutoComplete;
为了方便调试,把BookEditor里的owner_id输入框换成AutoComplete,传入一些测试数据:
...
import AutoComplete from './AutoComplete'; class BookEditor extends React.Component {
...
render () {
const {form: {name, price, owner_id}, onFormChange} = this.props;
return (
<form onSubmit={this.handleSubmit}>
...
<FormItem label="所有者:" valid={owner_id.valid} error={owner_id.error}> <AutoComplete
value={owner_id.value ? owner_id.value + '' : ''}
options={['10000(一韬)', '10001(张三)']}
onValueChange={value => onFormChange('owner_id', value)}
/>
</FormItem>
</form>
);
}
}
...
现在大概是这个样子:
有点怪,我们来给它加上样式。
新建/src/styles
文件夹和auto-complete.less
文件,写入代码:
.wrapper {
display: inline-block;
position: relative;
} .options {
margin: 0;
padding: 0;
list-style: none;
top: 110%;
left: 0;
right: 0;
position: absolute;
box-shadow: 1px 1px 10px 0 rgba(0, 0, 0, .6); > li {
padding: 3px 6px; &.active {
background-color: #0094ff;
color: white;
}
}
}
给AutoComplete.js加上className:
/**
* 自动完成组件
*/
import React from 'react';
// 引入 prop-types
import PropTypes from 'prop-types';
// 引入样式
import '../styles/auto-complete.less'; class AutoComplete extends React.Component {
// 构造器
constructor(props) {
super(props);
// 定义初始化状态
this.state = {
displayValue: '',
activeItemIndex: -1
};
} // 渲染
render() {
const {displayValue, activeItemIndex} = this.state;
// 组件传值
const {value, options} = this.props;
return (
<div className="wrapper">
<input value={displayValue || value}/>
{options.length > 0 && (
<ul className="options">
{
options.map((item, index) => {
return (
<li key={index} className={activeItemIndex === index ? 'active' : ''}>
{item.text || item}
</li>
);
})
}
</ul>
)}
</div>
);
}
} // 通用组件最好写一下propTypes约束
AutoComplete.propTypes = {
value: PropTypes.string.isRequired, // 字符串
options: PropTypes.array.isRequired, // 数组
onValueChange: PropTypes.func.isRequired // 函数
}; // 向外暴露
export default AutoComplete;
稍微顺眼一些了吧:
现在需要在AutoComplete中监听一些事件:
- 输入框的onChange
- 输入框的onKeyDown,用于对上下键、回车键进行监听处理
- 列表项目的onClick
- 列表项目的onMouseEnter,用于在鼠标移入时设置activeItemIndex
- 列表的onMouseLeave,用户鼠标移出时重置activeItemIndex
...
// 获得当前元素value值
function getItemValue (item) {
return item.value || item;
} class AutoComplete extends React.Component {
// 构造器
constructor(props) {
super(props);
// 定义初始化状态
this.state = {
displayValue: '',
activeItemIndex: -1
}; // 对上下键、回车键进行监听处理
this.handleKeyDown = this.handleKeyDown.bind(this);
// 对鼠标移出进行监听处理
this.handleLeave = this.handleLeave.bind(this);
} // 处理输入框改变事件
handleChange(value){
//
} // 处理上下键、回车键点击事件
handleKeyDown(e){
//
} // 处理鼠标移入事件
handleEnter(index){
//
} // 处理鼠标移出事件
handleLeave(){
//
} // 渲染
render() {
const {displayValue, activeItemIndex} = this.state;
// 组件传值
const {value, options} = this.props;
return (
<div className="wrapper">
<input
value={displayValue || value}
onChange={e => this.handleChange(e.target.value)}
onKeyDown={this.handleKeyDown} />
{options.length > 0 && (
<ul className="options" onMouseLeave={this.handleLeave}>
{
options.map((item, index) => {
return (
<li
key={index}
className={activeItemIndex === index ? 'active' : ''}
onMouseEnter={() => this.handleEnter(index)}
onClick={() => this.handleChange(getItemValue(item))}
>
{item.text || item}
</li>
);
})
}
</ul>
)}
</div>
);
}
}
...
先来实现handleChange方法,handleChange方法用于在用户输入、选择列表项的时候重置内部状态(清空displayName、设置activeItemIndex为-1),并通过回调将新的值传递给组件使用者:
...
handleChange (value) {
this.setState({activeItemIndex: -1, displayValue: ''});
this.props.onValueChange(value);
}
...
然后是handleKeyDown方法,这个方法中需要判断当前按下的键是否为上下方向键或回车键,如果是上下方向键则根据方向设置当前被选中的列表项;如果是回车键并且当前有选中状态的列表项,则调用handleChange:
...
handleKeyDown (e) {
const {activeItemIndex} = this.state;
const {options} = this.props; switch (e.keyCode) {
// 13为回车键的键码(keyCode)
case 13: {
// 判断是否有列表项处于选中状态
if (activeItemIndex >= 0) {
// 防止按下回车键后自动提交表单
e.preventDefault();
e.stopPropagation();
this.handleChange(getItemValue(options[activeItemIndex]));
}
break;
}
// 38为上方向键,40为下方向键
case 38:
case 40: {
e.preventDefault();
// 使用moveItem方法对更新或取消选中项
this.moveItem(e.keyCode === 38 ? 'up' : 'down');
break;
}
}
} moveItem (direction) {
const {activeItemIndex} = this.state;
const {options} = this.props;
const lastIndex = options.length - 1;
let newIndex = -1; // 计算新的activeItemIndex
if (direction === 'up') {
if (activeItemIndex === -1) {
// 如果没有选中项则选择最后一项
newIndex = lastIndex;
} else {
newIndex = activeItemIndex - 1;
}
} else {
if (activeItemIndex < lastIndex) {
newIndex = activeItemIndex + 1;
}
} // 获取新的displayValue
let newDisplayValue = '';
if (newIndex >= 0) {
newDisplayValue = getItemValue(options[newIndex]);
} // 更新状态
this.setState({
displayValue: newDisplayValue,
activeItemIndex: newIndex
});
}
...
handleEnter和handleLeave方法比较简单:
...
handleEnter (index) {
const currentItem = this.props.options[index];
this.setState({activeItemIndex: index, displayValue: getItemValue(currentItem)});
} handleLeave () {
this.setState({activeItemIndex: -1, displayValue: ''});
}
...
看一下效果:
完整的代码:
src / components / AutoComplete.js
/**
* 自动完成组件
*/
import React from 'react';
// 引入 prop-types
import PropTypes from 'prop-types';
// 引入样式
import '../styles/auto-complete.less'; // 获得当前元素value值
function getItemValue (item) {
return item.value || item;
} class AutoComplete extends React.Component {
// 构造器
constructor(props) {
super(props);
// 定义初始化状态
this.state = {
displayValue: '',
activeItemIndex: -1
}; // 对上下键、回车键进行监听处理
this.handleKeyDown = this.handleKeyDown.bind(this);
// 对鼠标移出进行监听处理
this.handleLeave = this.handleLeave.bind(this);
} // 处理输入框改变事件
handleChange(value){
// 选择列表项的时候重置内部状态
this.setState({
activeItemIndex: -1,
displayValue: ''
});
// 通过回调将新的值传递给组件使用者
this.props.onValueChange(value);
} // 处理上下键、回车键点击事件
handleKeyDown(e){
const {activeItemIndex} = this.state;
const {options} = this.props; /**
* 判断键码
*/
switch (e.keyCode) {
// 13为回车键的键码(keyCode)
case 13: {
// 判断是否有列表项处于选中状态
if(activeItemIndex >= 0){
// 防止按下回车键后自动提交表单
e.preventDefault();
e.stopPropagation();
// 输入框改变事件
this.handleChange(getItemValue(options[activeItemIndex]));
}
break;
}
// 38为上方向键,40为下方向键
case 38:
case 40: {
e.preventDefault();
// 使用moveItem方法对更新或取消选中项
this.moveItem(e.keyCode === 38 ? 'up' : 'down');
break;
}
default: {
//
}
}
} // 使用moveItem方法对更新或取消选中项
moveItem(direction){
const {activeItemIndex} = this.state;
const {options} = this.props;
const lastIndex = options.length - 1;
let newIndex = -1; // 计算新的activeItemIndex
if(direction === 'up'){ // 点击上方向键
if(activeItemIndex === -1){
// 如果没有选中项则选择最后一项
newIndex = lastIndex;
}else{
newIndex = activeItemIndex - 1;
}
}else{ // 点击下方向键
if(activeItemIndex < lastIndex){
newIndex = activeItemIndex + 1;
}
} // 获取新的displayValue
let newDisplayValue = '';
if(newIndex >= 0){
newDisplayValue = getItemValue(options[newIndex]);
} // 更新状态
this.setState({
displayValue: newDisplayValue,
activeItemIndex: newIndex
});
} // 处理鼠标移入事件
handleEnter(index){
const currentItem = this.props.options[index];
this.setState({
activeItemIndex: index,
displayValue: getItemValue(currentItem)
});
} // 处理鼠标移出事件
handleLeave(){
this.setState({
activeItemIndex: -1,
displayValue: ''
});
} // 渲染
render() {
const {displayValue, activeItemIndex} = this.state;
// 组件传值
const {value, options} = this.props;
return (
<div className="wrapper">
<input
value={displayValue || value}
onChange={e => this.handleChange(e.target.value)}
onKeyDown={this.handleKeyDown} />
{options.length > 0 && (
<ul className="options" onMouseLeave={this.handleLeave}>
{
options.map((item, index) => {
return (
<li
key={index}
className={activeItemIndex === index ? 'active' : ''}
onMouseEnter={() => this.handleEnter(index)}
onClick={() => this.handleChange(getItemValue(item))}
>
{item.text || item}
</li>
);
})
}
</ul>
)}
</div>
);
}
} // 通用组件最好写一下propTypes约束
AutoComplete.propTypes = {
value: PropTypes.string.isRequired, // 字符串
options: PropTypes.array.isRequired, // 数组
onValueChange: PropTypes.func.isRequired // 函数
}; // 向外暴露
export default AutoComplete;
基本上已经实现了自动完成组件,但是从图中可以发现选择后的值把用户名也带上了。
但是如果吧options中的用户名去掉,这个自动完成也就没有什么意义了,我们来把BookEditor中传入的options改一改:
...
<AutoComplete
value={owner_id.value ? owner_id.value + '' : ''}
options={[{text: '10000(一韬)', value: 10000}, {text: '10001(张三)', value: 10001}]}
onValueChange={value => onFormChange('owner_id', value)}
/>
...
刷新看一看,已经达到了我们期望的效果:
有时候我们显示的值并不一定是我们想要得到的值,这也是为什么我在组件的代码里有一个getItemValue方法了。
调用接口获取建议列表
也许有人要问了,这个建议列表为什么一直存在?
这是因为我们为了方便测试给了一个固定的options值,现在来完善一下,修改BookEditor.js:
import React from 'react';
import FormItem from './FormItem';
import AutoComplete from './AutoComplete';
import formProvider from '../utils/formProvider'; class BookEditor extends React.Component {
constructor (props) {
super(props);
this.state = {
recommendUsers: []
};
...
}
...
getRecommendUsers (partialUserId) {
fetch('http://localhost:8000/user?id_like=' + partialUserId)
.then((res) => res.json())
.then((res) => {
if (res.length === 1 && res[0].id === partialUserId) {
// 如果结果只有1条且id与输入的id一致,说明输入的id已经完整了,没必要再设置建议列表
return;
} // 设置建议列表
this.setState({
recommendUsers: res.map((user) => {
return {
text: `${user.id}(${user.name})`,
value: user.id
};
})
});
});
} timer = 0;
handleOwnerIdChange (value) {
this.props.onFormChange('owner_id', value);
this.setState({recommendUsers: []}); // 使用“节流”的方式进行请求,防止用户输入的过程中过多地发送请求
if (this.timer) {
clearTimeout(this.timer);
} if (value) {
// 200毫秒内只会发送1次请求
this.timer = setTimeout(() => {
// 真正的请求方法
this.getRecommendUsers(value);
this.timer = 0;
}, 200);
}
} render () {
const {recommendUsers} = this.state;
const {form: {name, price, owner_id}, onFormChange} = this.props;
return (
<form onSubmit={this.handleSubmit}>
...
<FormItem label="所有者:" valid={owner_id.valid} error={owner_id.error}>
<AutoComplete
value={owner_id.value ? owner_id.value + '' : ''}
options={recommendUsers}
onValueChange={value => this.handleOwnerIdChange(value)}
/>
</FormItem>
...
</form>
);
}
}
...
看一下最后的样子:
完整的代码:
src / components / BookEditor.js
/**
* 图书编辑器组件
*/
import React from 'react';
import FormItem from '../components/FormItem'; // 或写成 ./FormItem
// 高阶组件 formProvider表单验证
import formProvider from '../utils/formProvider';
// 引入 prop-types
import PropTypes from 'prop-types';
// 引入自动完成组件
import AutoComplete from './AutoComplete'; class BookEditor extends React.Component {
// 构造器
constructor(props) {
super(props); this.state = {
recommendUsers: []
};
} // 获取推荐用户信息
getRecommendUsers (partialUserId) {
// 请求数据
fetch('http://localhost:8000/user?id_like=' + partialUserId)
.then((res) => res.json())
.then((res) => {
if(res.length === 1 && res[0].id === partialUserId){
// 如果结果只有1条且id与输入的id一致,说明输入的id已经完整了,没必要再设置建议列表
return;
} // 设置建议列表
this.setState({
recommendUsers: res.map((user) => {
return {
text: `${user.id}(${user.name})`,
value: user.id
}
})
});
})
} // 计时器
timer = 0;
handleOwnerIdChange(value){
this.props.onFormChange('owner_id', value);
this.setState({
recommendUsers: []
}); // 使用"节流"的方式进行请求,防止用户输入的过程中过多地发送请求
if(this.timer){
// 清除计时器
clearTimeout(this.timer);
} if(value){
// 200毫秒内只会发送1次请求
this.timer = setTimeout(() => {
// 真正的请求方法
this.getRecommendUsers(value);
this.timer = 0;
}, 200);
}
} // 按钮提交事件
handleSubmit(e){
// 阻止表单submit事件自动跳转页面的动作
e.preventDefault();
// 定义常量
const { form: { name, price, owner_id }, formValid, editTarget} = this.props; // 组件传值
// 验证
if(!formValid){
alert('请填写正确的信息后重试');
return;
} // 默认值
let editType = '添加';
let apiUrl = 'http://localhost:8000/book';
let method = 'post';
// 判断类型
if(editTarget){
editType = '编辑';
apiUrl += '/' + editTarget.id;
method = 'put';
} // 发送请求
fetch(apiUrl, {
method, // method: method 的简写
// 使用fetch提交的json数据需要使用JSON.stringify转换为字符串
body: JSON.stringify({
name: name.value,
price: price.value,
owner_id: owner_id.value
}),
headers: {
'Content-Type': 'application/json'
}
})
// 强制回调的数据格式为json
.then((res) => res.json())
// 成功的回调
.then((res) => {
// 当添加成功时,返回的json对象中应包含一个有效的id字段
// 所以可以使用res.id来判断添加是否成功
if(res.id){
alert(editType + '添加图书成功!');
this.context.router.push('/book/list'); // 跳转到用户列表页面
return;
}else{
alert(editType + '添加图书失败!');
}
})
// 失败的回调
.catch((err) => console.error(err));
} // 生命周期--组件加载中
componentWillMount(){
const {editTarget, setFormValues} = this.props;
if(editTarget){
setFormValues(editTarget);
}
} render() {
// 定义常量
const {recommendUsers} = this.state;
const {form: {name, price, owner_id}, onFormChange} = this.props;
return (
<form onSubmit={(e) => this.handleSubmit(e)}>
<FormItem label="书名:" valid={name.valid} error={name.error}>
<input
type="text"
value={name.value}
onChange={(e) => onFormChange('name', e.target.value)}/>
</FormItem> <FormItem label="价格:" valid={price.valid} error={price.error}>
<input
type="number"
value={price.value || ''}
onChange={(e) => onFormChange('price', e.target.value)}/>
</FormItem> <FormItem label="所有者:" valid={owner_id.valid} error={owner_id.error}>
<AutoComplete
value={owner_id.value ? owner_id.value + '' : ''}
options={recommendUsers}
onValueChange={value => this.handleOwnerIdChange(value)} />
</FormItem>
<br />
<input type="submit" value="提交" />
</form>
);
}
} // 必须给BookEditor定义一个包含router属性的contextTypes
// 使得组件中可以通过this.context.router来使用React Router提供的方法
BookEditor.contextTypes = {
router: PropTypes.object.isRequired
}; // 实例化
BookEditor = formProvider({ // field 对象
// 书名
name: {
defaultValue: '',
rules: [
{
pattern: function (value) {
return value.length > 0;
},
error: '请输入图书户名'
},
{
pattern: /^.{1,10}$/,
error: '图书名最多10个字符'
}
]
},
// 价格
price: {
defaultValue: 0,
rules: [
{
pattern: function(value){
return value > 0;
},
error: '价格必须大于0'
}
]
},
// 所有者
owner_id: {
defaultValue: '',
rules: [
{
pattern: function (value) {
return value > 0;
},
error: '请输入所有者名称'
},
{
pattern: /^.{1,10}$/,
error: '所有者名称最多10个字符'
}
]
}
})(BookEditor); export default BookEditor;
.
react 项目实战(八)图书管理与自动完成的更多相关文章
- React项目实战:react-redux-router基本原理
React相关 React 是一个采用声明式,高效而且灵活的用来构建用户界面的框架. JSX 本质上来讲,JSX 只是为React.createElement(component, props, .. ...
- react 项目实战(十)引入AntDesign组件库
本篇带你使用 AntDesign 组件库为我们的系统换上产品级的UI! 安装组件库 在项目目录下执行:npm i antd@3.3.0 -S 或 yarn add antd 安装组件包 执行:npm ...
- 《Node+MongoDB+React 项目实战开发》已出版
前言 从深圳回长沙已经快4个月了,除了把车开熟练了外,并没有什么值得一提的,长沙这边要么就是连续下一个月雨,要么就是连续一个月高温暴晒,上班更是没啥子意思,长沙这边的公司和深圳落差挺大的,薪资也是断崖 ...
- asp.net core react 项目实战(一)
asp.net-core-react asp.net core react 简介 开发依赖环境 .NET Core SDK (reflecting any global.json): Version: ...
- struts2+hibernate 项目实战:图书管理系统
经典项目,练手必备. 图书管理系统 需求分析(大致,并不专业):1.需要有用户管理: 1.1 用户注册: 1.2 用户登录: 1.3 用户信息修改: 1.4 用户修改密码: 2.需要有书本管理: 2. ...
- react 项目实战(九)登录与身份认证
SPA的鉴权方式和传统的web应用不同:由于页面的渲染不再依赖服务端,与服务端的交互都通过接口来完成,而REASTful风格的接口提倡无状态(state less),通常不使用cookie和sessi ...
- react 项目实战(四)组件化表单/表单控件 高阶组件
高阶组件:formProvider 高阶组件就是返回组件的组件(函数) 为什么要通过一个组件去返回另一个组件? 使用高阶组件可以在不修改原组件代码的情况下,修改原组件的行为或增强功能. 我们现在已经有 ...
- react 项目实战(七)用户编辑与删除
添加操作列 编辑与删除功能都是针对已存在的某一个用户执行的操作,所以在用户列表中需要再加一个“操作”列来展现[编辑]与[删除]这两个按钮. 修改/src/pages/UserList.js文件,添加方 ...
- react 项目实战(三)表单验证
我们需要记录每一个字段当前的有效状态,有效时隐藏错误信息,无效时显示错误信息. 而这个有效/无效,可以在表单值改变的时候进行判断. 我们对/src/pages/UserAdd.js进行修改: 首先修改 ...
随机推荐
- 【C++】朝花夕拾——表达式树
表达式树: 叶子是操作数,其余结点为操作符,是二叉树的其中一种应用 ====================我是分割线====================== 一棵表达式树如下图: 若是对它做中序 ...
- swift class extension 与继承
1.扩展中无法继承重写已有函数,不能添加函数. Extensions can add new functionality to a type, but they cannot override exi ...
- flask的基本搭建
from flask import Flask app = Flask(__name__) @app.route("/")def index(): return "ok& ...
- BEGIN - 开始一个事务块
SYNOPSIS BEGIN [ WORK | TRANSACTION ] DESCRIPTION 描述 BEGIN 初始化一个事务块, 也就是说所有 BEGIN 命令后的用户语句都将在一个事务里面执 ...
- bat copy
@echo off regedit /s %~dp0regedit.reg //注册注册表xcopy "D: ...
- CAD控件,CAD插件使用教程:Android开发使用控件--开发环境的搭建
Android开发使用控件入门--环境搭建 2014-12-24 09:57 14人阅读 评论(0) 收藏 编辑 删除 CAD控件.CAD三维控件,手机 ...
- 对比props
1.在组件中data返回数组对象 2.在父级作用域中写入 (1)prop传值 <btn-grp :buttons="buttons"></btn-grp> ...
- B4. Concurrent JVM 锁机制(synchronized)
[概述] JVM 通过 synchronized 关键字提供锁,用于在线程同步中保证线程安全. [synchronized 实现原理] synchronized 可以用于代码块或者方法中,产生同步代码 ...
- 【牛客小白月赛6】 C 桃花 - 树上最长路
题目地址:https://www.nowcoder.com/acm/contest/136/C dfs找出最长路和次长路,将两个结果相加再加上起点即可: #include<iostream> ...
- Ubuntu终端常用快捷键汇总
Ubuntu终端常用的快捷键 - 转自- 博客园 http://www.cnblogs.com/nucdy/p/5251659.html Ubuntu中的许多操作在终端(Terminal)中十分 ...