图书管理

src / pages / BookAdd.js   // 图书添加页

  1. /**
  2. * 图书添加页面
  3. */
  4. import React from 'react';
  5. // 布局组件
  6. import HomeLayout from '../layouts/HomeLayout';
  7. // 编辑组件
  8. import BookEditor from '../components/BookEditor';
  9.  
  10. class BookAdd extends React.Component {
  11. render() {
  12. return (
  13. <HomeLayout title="添加图书">
  14. <BookEditor />
  15. </HomeLayout>
  16. );
  17. }
  18. }
  19.  
  20. export default BookAdd;

src / pages / BookList.js   // 图书列表页

  1. /**
  2. * 图书列表页面
  3. */
  4. import React from 'react';
  5. // 布局组件
  6. import HomeLayout from '../layouts/HomeLayout';
  7. // 引入 prop-types
  8. import PropTypes from 'prop-types';
  9.  
  10. class BookList extends React.Component {
  11. // 构造器
  12. constructor(props) {
  13. super(props);
  14. // 定义初始化状态
  15. this.state = {
  16. bookList: []
  17. };
  18. }
  19.  
  20. /**
  21. * 生命周期
  22. * componentWillMount
  23. * 组件初始化时只调用,以后组件更新不调用,整个生命周期只调用一次
  24. */
  25. componentWillMount(){
  26. // 请求数据
  27. fetch('http://localhost:8000/book')
  28. .then(res => res.json())
  29. .then(res => {
  30. /**
  31. * 成功的回调
  32. * 数据赋值
  33. */
  34. this.setState({
  35. bookList: res
  36. });
  37. });
  38. }
  39.  
  40. /**
  41. * 编辑
  42. */
  43. handleEdit(book){
  44. // 跳转编辑页面
  45. this.context.router.push('/book/edit/' + book.id);
  46. }
  47.  
  48. /**
  49. * 删除
  50. */
  51. handleDel(book){
  52. // 确认框
  53. const confirmed = window.confirm(`确认要删除书名 ${book.name} 吗?`);
  54. // 判断
  55. if(confirmed){
  56. // 执行删除数据操作
  57. fetch('http://localhost:8000/book/' + book.id, {
  58. method: 'delete'
  59. })
  60. .then(res => res.json())
  61. .then(res => {
  62. /**
  63. * 设置状态
  64. * array.filter
  65. * 把Array的某些元素过滤掉,然后返回剩下的元素
  66. */
  67. this.setState({
  68. bookList: this.state.bookList.filter(item => item.id !== book.id)
  69. });
  70. alert('删除用户成功');
  71. })
  72. .catch(err => {
  73. console.log(err);
  74. alert('删除用户失败');
  75. });
  76. }
  77. }
  78.  
  79. render() {
  80. // 定义变量
  81. const { bookList } = this.state;
  82.  
  83. return (
  84. <HomeLayout title="图书列表">
  85. <table>
  86. <thead>
  87. <tr>
  88. <th>图书ID</th>
  89. <th>图书名称</th>
  90. <th>价格</th>
  91. <th>操作</th>
  92. </tr>
  93. </thead>
  94.  
  95. <tbody>
  96. {
  97. bookList.map((book) => {
  98. return (
  99. <tr key={book.id}>
  100. <td>{book.id}</td>
  101. <td>{book.name}</td>
  102. <td>{book.price}</td>
  103. <td>
  104. <a onClick={() => this.handleEdit(book)}>编辑</a>
  105.  
  106. <a onClick={() => this.handleDel(book)}>删除</a>
  107. </td>
  108. </tr>
  109. );
  110. })
  111. }
  112. </tbody>
  113. </table>
  114. </HomeLayout>
  115. );
  116. }
  117. }
  118.  
  119. /**
  120. * 任何使用this.context.xxx的地方,必须在组件的contextTypes里定义对应的PropTypes
  121. */
  122. BookList.contextTypes = {
  123. router: PropTypes.object.isRequired
  124. };
  125.  
  126. export default BookList;

src / components / BookEditor.js   // 图书编辑组件

  1. /**
  2. * 图书编辑器组件
  3. */
  4. import React from 'react';
  5. import FormItem from '../components/FormItem'; // 或写成 ./FormItem
  6. // 高阶组件 formProvider表单验证
  7. import formProvider from '../utils/formProvider';
  8. // 引入 prop-types
  9. import PropTypes from 'prop-types';
  10.  
  11. class BookEditor extends React.Component {
  12. // 按钮提交事件
  13. handleSubmit(e){
  14. // 阻止表单submit事件自动跳转页面的动作
  15. e.preventDefault();
  16. // 定义常量
  17. const { form: { name, price, owner_id }, formValid, editTarget} = this.props; // 组件传值
  18. // 验证
  19. if(!formValid){
  20. alert('请填写正确的信息后重试');
  21. return;
  22. }
  23.  
  24. // 默认值
  25. let editType = '添加';
  26. let apiUrl = 'http://localhost:8000/book';
  27. let method = 'post';
  28. // 判断类型
  29. if(editTarget){
  30. editType = '编辑';
  31. apiUrl += '/' + editTarget.id;
  32. method = 'put';
  33. }
  34.  
  35. // 发送请求
  36. fetch(apiUrl, {
  37. method, // method: method 的简写
  38. // 使用fetch提交的json数据需要使用JSON.stringify转换为字符串
  39. body: JSON.stringify({
  40. name: name.value,
  41. price: price.value,
  42. owner_id: owner_id.value
  43. }),
  44. headers: {
  45. 'Content-Type': 'application/json'
  46. }
  47. })
  48. // 强制回调的数据格式为json
  49. .then((res) => res.json())
  50. // 成功的回调
  51. .then((res) => {
  52. // 当添加成功时,返回的json对象中应包含一个有效的id字段
  53. // 所以可以使用res.id来判断添加是否成功
  54. if(res.id){
  55. alert(editType + '添加图书成功!');
  56. this.context.router.push('/book/list'); // 跳转到用户列表页面
  57. return;
  58. }else{
  59. alert(editType + '添加图书失败!');
  60. }
  61. })
  62. // 失败的回调
  63. .catch((err) => console.error(err));
  64. }
  65.  
  66. // 生命周期--组件加载中
  67. componentWillMount(){
  68. const {editTarget, setFormValues} = this.props;
  69. if(editTarget){
  70. setFormValues(editTarget);
  71. }
  72. }
  73.  
  74. render() {
  75. // 定义常量
  76. const {form: {name, price, owner_id}, onFormChange} = this.props;
  77. return (
  78. <form onSubmit={(e) => this.handleSubmit(e)}>
  79. <FormItem label="书名:" valid={name.valid} error={name.error}>
  80. <input
  81. type="text"
  82. value={name.value}
  83. onChange={(e) => onFormChange('name', e.target.value)}/>
  84. </FormItem>
  85.  
  86. <FormItem label="价格:" valid={price.valid} error={price.error}>
  87. <input
  88. type="number"
  89. value={price.value || ''}
  90. onChange={(e) => onFormChange('price', e.target.value)}/>
  91. </FormItem>
  92.  
  93. <FormItem label="所有者:" valid={owner_id.valid} error={owner_id.error}>
  94. <input
  95. type="text"
  96. value={owner_id.value || ''}
  97. onChange={(e) => onFormChange('owner_id', e.target.value)}/>
  98. </FormItem>
  99. <br />
  100. <input type="submit" value="提交" />
  101. </form>
  102. );
  103. }
  104. }
  105.  
  106. // 必须给BookEditor定义一个包含router属性的contextTypes
  107. // 使得组件中可以通过this.context.router来使用React Router提供的方法
  108. BookEditor.contextTypes = {
  109. router: PropTypes.object.isRequired
  110. };
  111.  
  112. // 实例化
  113. BookEditor = formProvider({ // field 对象
  114. // 书名
  115. name: {
  116. defaultValue: '',
  117. rules: [
  118. {
  119. pattern: function (value) {
  120. return value.length > 0;
  121. },
  122. error: '请输入图书户名'
  123. },
  124. {
  125. pattern: /^.{1,10}$/,
  126. error: '图书名最多10个字符'
  127. }
  128. ]
  129. },
  130. // 价格
  131. price: {
  132. defaultValue: 0,
  133. rules: [
  134. {
  135. pattern: function(value){
  136. return value > 0;
  137. },
  138. error: '价格必须大于0'
  139. }
  140. ]
  141. },
  142. // 所有者
  143. owner_id: {
  144. defaultValue: '',
  145. rules: [
  146. {
  147. pattern: function (value) {
  148. return value > 0;
  149. },
  150. error: '请输入所有者名称'
  151. },
  152. {
  153. pattern: /^.{1,10}$/,
  154. error: '所有者名称最多10个字符'
  155. }
  156. ]
  157. }
  158. })(BookEditor);
  159.  
  160. export default BookEditor;

src / pages / BookEdit.js   // 图书编辑页

  1. /**
  2. * 编辑图书页面
  3. */
  4. import React from 'react';
  5. // 布局组件
  6. import HomeLayout from '../layouts/HomeLayout';
  7. // 引入 prop-types
  8. import PropTypes from 'prop-types';
  9. // 图书编辑器组件
  10. import BookEditor from '../components/BookEditor';
  11.  
  12. class BookEdit extends React.Component {
  13. // 构造器
  14. constructor(props) {
  15. super(props);
  16. // 定义初始化状态
  17. this.state = {
  18. book: null
  19. };
  20. }
  21.  
  22. // 生命周期--组件加载中
  23. componentWillMount(){
  24. // 定义常量
  25. const bookId = this.context.router.params.id;
  26. /**
  27. * 发送请求
  28. * 获取用户数据
  29. */
  30. fetch('http://localhost:8000/book/' + bookId)
  31. .then(res => res.json())
  32. .then(res => {
  33. this.setState({
  34. book: res
  35. });
  36. })
  37. }
  38.  
  39. render() {
  40. const {book} = this.state;
  41. return (
  42. <HomeLayout title="编辑图书">
  43. {
  44. book ? <BookEditor editTarget={book} /> : '加载中...'
  45. }
  46. </HomeLayout>
  47. );
  48. }
  49. }
  50.  
  51. BookEdit.contextTypes = {
  52. router: PropTypes.object.isRequired
  53. };
  54.  
  55. export default BookEdit;

项目结构:

自动完成组件

找了个例子看一下效果:

可以发现,这是一个包含一个输入框、一个下拉框的复合控件。

实现一个通用组件,在动手写代码之前我会做以下准备工作:

  1. 确定组件结构
  2. 观察组件逻辑
  3. 确定组件内部状态(state)
  4. 确定组件向外暴露的属性(props)

组件结构

上面提了,这个组件由一个输入框和一个下拉框组成。

注意,这里的下拉框是一个“伪”下拉框,并不是指select与option。仔细看上面的动图,可以看得出来这个“伪”下拉框只是一个带边框的、位于输入框正下方的一个列表。

我们可以假设组件的结构是这样的:

  1. <div>
  2. <input type="text"/>
  3. <ul>
  4. <li>...</li>
  5. ...
  6. </ul>
  7. </div>

组件逻辑

观察动图,可以发现组件有以下行为:

  1. 未输入时,与普通输入框一致
  2. 输入改变时如果有建议的选项,则在下放显示出建议列表
  3. 建议列表可以使用键盘上下键进行选择,选择某一项时该项高亮显示,并且输入框的值变为该项的值
  4. 当移出列表(在第一项按上键或在最后一项按下键)时,输入框的值变为原来输入的值(图中的“as”)
  5. 按下回车键可以确定选择该项,列表消失
  6. 可以使用鼠标在列表中进行选择,鼠标移入的列表项高亮显示

组件内部状态

一个易用的通用组件应该对外隐藏只有内部使用的状态。使用React组件的state来维护组件的内部状态。

根据组件逻辑,我们可以确定自动完成组件需要这些内部状态:

  • 逻辑2|3|4:输入框中显示的值,默认为空字符串(displayValue)
  • 逻辑3|6:建议列表中高亮的项目,可以维护一个项目在列表中的索引,默认为-1(activeItemIndex)

组件暴露的属性

我们的目标是一个通用的组件,所以类似组件实际的值、推荐列表这样的状态,应该由组件的使用者来控制:

如上图,组件应向外暴露的属性有:

  • value:代表实际的值(不同于上面的displayValue表示显示的、临时的值,value表示的是最终的值)
  • options:代表当前组件的建议列表,为空数组时,建议列表隐藏
  • onValueChange:用于在输入值或确定选择了某一项时通知使用者的回调方法,使用者可以在这个回调方法中对options、value进行更新

实现

确定了组件结构、组件逻辑、内部状态和外部属性之后,就可以着手进行编码了:

/src/components下新建AutoComplete.js文件,写入组件的基本代码:

  1. /**
  2. * 自动完成组件
  3. */
  4. import React from 'react';
  5. // 引入 prop-types
  6. import PropTypes from 'prop-types';
  7.  
  8. class AutoComplete extends React.Component {
  9. // 构造器
  10. constructor(props) {
  11. super(props);
  12. // 定义初始化状态
  13. this.state = {
  14. displayValue: '',
  15. activeItemIndex: -1
  16. };
  17. }
  18.  
  19. // 渲染
  20. render() {
  21. const {displayValue, activeItemIndex} = this.state;
  22. // 组件传值
  23. const {value, options} = this.props;
  24. return (
  25. <div>
  26. <input value={value}/>
  27. {options.length > 0 && (
  28. <ul>
  29. {
  30. options.map((item, index) => {
  31. return (
  32. <li key={index}>
  33. {item.text || item}
  34. </li>
  35. );
  36. })
  37. }
  38. </ul>
  39. )}
  40. </div>
  41. );
  42. }
  43. }
  44.  
  45. // 通用组件最好写一下propTypes约束
  46. AutoComplete.propTypes = {
  47. value: PropTypes.string.isRequired, // 字符串
  48. options: PropTypes.array.isRequired, // 数组
  49. onValueChange: PropTypes.func.isRequired // 函数
  50. };
  51.  
  52. // 向外暴露
  53. export default AutoComplete;

为了方便调试,把BookEditor里的owner_id输入框换成AutoComplete,传入一些测试数据:

  1. ...
  2. import AutoComplete from './AutoComplete';
  3.  
  4. class BookEditor extends React.Component {
  5. ...
  6. render () {
  7. const {form: {name, price, owner_id}, onFormChange} = this.props;
  8. return (
  9. <form onSubmit={this.handleSubmit}>
  10. ...
  11. <FormItem label="所有者:" valid={owner_id.valid} error={owner_id.error}>
  12.  
  13. <AutoComplete
  14. value={owner_id.value ? owner_id.value + '' : ''}
  15. options={['10000(一韬)', '10001(张三)']}
  16. onValueChange={value => onFormChange('owner_id', value)}
  17. />
  18. </FormItem>
  19. </form>
  20. );
  21. }
  22. }
  23. ...

现在大概是这个样子:

有点怪,我们来给它加上样式。

新建/src/styles文件夹和auto-complete.less文件,写入代码:

  1. .wrapper {
  2. display: inline-block;
  3. position: relative;
  4. }
  5.  
  6. .options {
  7. margin: 0;
  8. padding: 0;
  9. list-style: none;
  10. top: 110%;
  11. left: 0;
  12. right: 0;
  13. position: absolute;
  14. box-shadow: 1px 1px 10px 0 rgba(0, 0, 0, .6);
  15.  
  16. > li {
  17. padding: 3px 6px;
  18.  
  19. &.active {
  20. background-color: #0094ff;
  21. color: white;
  22. }
  23. }
  24. }

给AutoComplete.js加上className:

  1. /**
  2. * 自动完成组件
  3. */
  4. import React from 'react';
  5. // 引入 prop-types
  6. import PropTypes from 'prop-types';
  7. // 引入样式
  8. import '../styles/auto-complete.less';
  9.  
  10. class AutoComplete extends React.Component {
  11. // 构造器
  12. constructor(props) {
  13. super(props);
  14. // 定义初始化状态
  15. this.state = {
  16. displayValue: '',
  17. activeItemIndex: -1
  18. };
  19. }
  20.  
  21. // 渲染
  22. render() {
  23. const {displayValue, activeItemIndex} = this.state;
  24. // 组件传值
  25. const {value, options} = this.props;
  26. return (
  27. <div className="wrapper">
  28. <input value={displayValue || value}/>
  29. {options.length > 0 && (
  30. <ul className="options">
  31. {
  32. options.map((item, index) => {
  33. return (
  34. <li key={index} className={activeItemIndex === index ? 'active' : ''}>
  35. {item.text || item}
  36. </li>
  37. );
  38. })
  39. }
  40. </ul>
  41. )}
  42. </div>
  43. );
  44. }
  45. }
  46.  
  47. // 通用组件最好写一下propTypes约束
  48. AutoComplete.propTypes = {
  49. value: PropTypes.string.isRequired, // 字符串
  50. options: PropTypes.array.isRequired, // 数组
  51. onValueChange: PropTypes.func.isRequired // 函数
  52. };
  53.  
  54. // 向外暴露
  55. export default AutoComplete;

稍微顺眼一些了吧:

现在需要在AutoComplete中监听一些事件:

  • 输入框的onChange
  • 输入框的onKeyDown,用于对上下键、回车键进行监听处理
  • 列表项目的onClick
  • 列表项目的onMouseEnter,用于在鼠标移入时设置activeItemIndex
  • 列表的onMouseLeave,用户鼠标移出时重置activeItemIndex
  1. ...
  2. // 获得当前元素value值
  3. function getItemValue (item) {
  4. return item.value || item;
  5. }
  6.  
  7. class AutoComplete extends React.Component {
  8. // 构造器
  9. constructor(props) {
  10. super(props);
  11. // 定义初始化状态
  12. this.state = {
  13. displayValue: '',
  14. activeItemIndex: -1
  15. };
  16.  
  17. // 对上下键、回车键进行监听处理
  18. this.handleKeyDown = this.handleKeyDown.bind(this);
  19. // 对鼠标移出进行监听处理
  20. this.handleLeave = this.handleLeave.bind(this);
  21. }
  22.  
  23. // 处理输入框改变事件
  24. handleChange(value){
  25. //
  26. }
  27.  
  28. // 处理上下键、回车键点击事件
  29. handleKeyDown(e){
  30. //
  31. }
  32.  
  33. // 处理鼠标移入事件
  34. handleEnter(index){
  35. //
  36. }
  37.  
  38. // 处理鼠标移出事件
  39. handleLeave(){
  40. //
  41. }
  42.  
  43. // 渲染
  44. render() {
  45. const {displayValue, activeItemIndex} = this.state;
  46. // 组件传值
  47. const {value, options} = this.props;
  48. return (
  49. <div className="wrapper">
  50. <input
  51. value={displayValue || value}
  52. onChange={e => this.handleChange(e.target.value)}
  53. onKeyDown={this.handleKeyDown} />
  54. {options.length > 0 && (
  55. <ul className="options" onMouseLeave={this.handleLeave}>
  56. {
  57. options.map((item, index) => {
  58. return (
  59. <li
  60. key={index}
  61. className={activeItemIndex === index ? 'active' : ''}
  62. onMouseEnter={() => this.handleEnter(index)}
  63. onClick={() => this.handleChange(getItemValue(item))}
  64. >
  65. {item.text || item}
  66. </li>
  67. );
  68. })
  69. }
  70. </ul>
  71. )}
  72. </div>
  73. );
  74. }
  75. }
  76. ...

先来实现handleChange方法,handleChange方法用于在用户输入、选择列表项的时候重置内部状态(清空displayName、设置activeItemIndex为-1),并通过回调将新的值传递给组件使用者:

  1. ...
  2. handleChange (value) {
  3. this.setState({activeItemIndex: -1, displayValue: ''});
  4. this.props.onValueChange(value);
  5. }
  6. ...

然后是handleKeyDown方法,这个方法中需要判断当前按下的键是否为上下方向键或回车键,如果是上下方向键则根据方向设置当前被选中的列表项;如果是回车键并且当前有选中状态的列表项,则调用handleChange:

  1. ...
  2. handleKeyDown (e) {
  3. const {activeItemIndex} = this.state;
  4. const {options} = this.props;
  5.  
  6. switch (e.keyCode) {
  7. // 13为回车键的键码(keyCode)
  8. case 13: {
  9. // 判断是否有列表项处于选中状态
  10. if (activeItemIndex >= 0) {
  11. // 防止按下回车键后自动提交表单
  12. e.preventDefault();
  13. e.stopPropagation();
  14. this.handleChange(getItemValue(options[activeItemIndex]));
  15. }
  16. break;
  17. }
  18. // 38为上方向键,40为下方向键
  19. case 38:
  20. case 40: {
  21. e.preventDefault();
  22. // 使用moveItem方法对更新或取消选中项
  23. this.moveItem(e.keyCode === 38 ? 'up' : 'down');
  24. break;
  25. }
  26. }
  27. }
  28.  
  29. moveItem (direction) {
  30. const {activeItemIndex} = this.state;
  31. const {options} = this.props;
  32. const lastIndex = options.length - 1;
  33. let newIndex = -1;
  34.  
  35. // 计算新的activeItemIndex
  36. if (direction === 'up') {
  37. if (activeItemIndex === -1) {
  38. // 如果没有选中项则选择最后一项
  39. newIndex = lastIndex;
  40. } else {
  41. newIndex = activeItemIndex - 1;
  42. }
  43. } else {
  44. if (activeItemIndex < lastIndex) {
  45. newIndex = activeItemIndex + 1;
  46. }
  47. }
  48.  
  49. // 获取新的displayValue
  50. let newDisplayValue = '';
  51. if (newIndex >= 0) {
  52. newDisplayValue = getItemValue(options[newIndex]);
  53. }
  54.  
  55. // 更新状态
  56. this.setState({
  57. displayValue: newDisplayValue,
  58. activeItemIndex: newIndex
  59. });
  60. }
  61. ...

handleEnter和handleLeave方法比较简单:

  1. ...
  2. handleEnter (index) {
  3. const currentItem = this.props.options[index];
  4. this.setState({activeItemIndex: index, displayValue: getItemValue(currentItem)});
  5. }
  6.  
  7. handleLeave () {
  8. this.setState({activeItemIndex: -1, displayValue: ''});
  9. }
  10. ...

看一下效果:

完整的代码:

src / components / AutoComplete.js

  1. /**
  2. * 自动完成组件
  3. */
  4. import React from 'react';
  5. // 引入 prop-types
  6. import PropTypes from 'prop-types';
  7. // 引入样式
  8. import '../styles/auto-complete.less';
  9.  
  10. // 获得当前元素value值
  11. function getItemValue (item) {
  12. return item.value || item;
  13. }
  14.  
  15. class AutoComplete extends React.Component {
  16. // 构造器
  17. constructor(props) {
  18. super(props);
  19. // 定义初始化状态
  20. this.state = {
  21. displayValue: '',
  22. activeItemIndex: -1
  23. };
  24.  
  25. // 对上下键、回车键进行监听处理
  26. this.handleKeyDown = this.handleKeyDown.bind(this);
  27. // 对鼠标移出进行监听处理
  28. this.handleLeave = this.handleLeave.bind(this);
  29. }
  30.  
  31. // 处理输入框改变事件
  32. handleChange(value){
  33. // 选择列表项的时候重置内部状态
  34. this.setState({
  35. activeItemIndex: -1,
  36. displayValue: ''
  37. });
  38. // 通过回调将新的值传递给组件使用者
  39. this.props.onValueChange(value);
  40. }
  41.  
  42. // 处理上下键、回车键点击事件
  43. handleKeyDown(e){
  44. const {activeItemIndex} = this.state;
  45. const {options} = this.props;
  46.  
  47. /**
  48. * 判断键码
  49. */
  50. switch (e.keyCode) {
  51. // 13为回车键的键码(keyCode)
  52. case 13: {
  53. // 判断是否有列表项处于选中状态
  54. if(activeItemIndex >= 0){
  55. // 防止按下回车键后自动提交表单
  56. e.preventDefault();
  57. e.stopPropagation();
  58. // 输入框改变事件
  59. this.handleChange(getItemValue(options[activeItemIndex]));
  60. }
  61. break;
  62. }
  63. // 38为上方向键,40为下方向键
  64. case 38:
  65. case 40: {
  66. e.preventDefault();
  67. // 使用moveItem方法对更新或取消选中项
  68. this.moveItem(e.keyCode === 38 ? 'up' : 'down');
  69. break;
  70. }
  71. default: {
  72. //
  73. }
  74. }
  75. }
  76.  
  77. // 使用moveItem方法对更新或取消选中项
  78. moveItem(direction){
  79. const {activeItemIndex} = this.state;
  80. const {options} = this.props;
  81. const lastIndex = options.length - 1;
  82. let newIndex = -1;
  83.  
  84. // 计算新的activeItemIndex
  85. if(direction === 'up'){ // 点击上方向键
  86. if(activeItemIndex === -1){
  87. // 如果没有选中项则选择最后一项
  88. newIndex = lastIndex;
  89. }else{
  90. newIndex = activeItemIndex - 1;
  91. }
  92. }else{ // 点击下方向键
  93. if(activeItemIndex < lastIndex){
  94. newIndex = activeItemIndex + 1;
  95. }
  96. }
  97.  
  98. // 获取新的displayValue
  99. let newDisplayValue = '';
  100. if(newIndex >= 0){
  101. newDisplayValue = getItemValue(options[newIndex]);
  102. }
  103.  
  104. // 更新状态
  105. this.setState({
  106. displayValue: newDisplayValue,
  107. activeItemIndex: newIndex
  108. });
  109. }
  110.  
  111. // 处理鼠标移入事件
  112. handleEnter(index){
  113. const currentItem = this.props.options[index];
  114. this.setState({
  115. activeItemIndex: index,
  116. displayValue: getItemValue(currentItem)
  117. });
  118. }
  119.  
  120. // 处理鼠标移出事件
  121. handleLeave(){
  122. this.setState({
  123. activeItemIndex: -1,
  124. displayValue: ''
  125. });
  126. }
  127.  
  128. // 渲染
  129. render() {
  130. const {displayValue, activeItemIndex} = this.state;
  131. // 组件传值
  132. const {value, options} = this.props;
  133. return (
  134. <div className="wrapper">
  135. <input
  136. value={displayValue || value}
  137. onChange={e => this.handleChange(e.target.value)}
  138. onKeyDown={this.handleKeyDown} />
  139. {options.length > 0 && (
  140. <ul className="options" onMouseLeave={this.handleLeave}>
  141. {
  142. options.map((item, index) => {
  143. return (
  144. <li
  145. key={index}
  146. className={activeItemIndex === index ? 'active' : ''}
  147. onMouseEnter={() => this.handleEnter(index)}
  148. onClick={() => this.handleChange(getItemValue(item))}
  149. >
  150. {item.text || item}
  151. </li>
  152. );
  153. })
  154. }
  155. </ul>
  156. )}
  157. </div>
  158. );
  159. }
  160. }
  161.  
  162. // 通用组件最好写一下propTypes约束
  163. AutoComplete.propTypes = {
  164. value: PropTypes.string.isRequired, // 字符串
  165. options: PropTypes.array.isRequired, // 数组
  166. onValueChange: PropTypes.func.isRequired // 函数
  167. };
  168.  
  169. // 向外暴露
  170. export default AutoComplete;

基本上已经实现了自动完成组件,但是从图中可以发现选择后的值把用户名也带上了。

但是如果吧options中的用户名去掉,这个自动完成也就没有什么意义了,我们来把BookEditor中传入的options改一改:

  1. ...
  2. <AutoComplete
  3. value={owner_id.value ? owner_id.value + '' : ''}
  4. options={[{text: '10000(一韬)', value: 10000}, {text: '10001(张三)', value: 10001}]}
  5. onValueChange={value => onFormChange('owner_id', value)}
  6. />
  7. ...

刷新看一看,已经达到了我们期望的效果:

有时候我们显示的值并不一定是我们想要得到的值,这也是为什么我在组件的代码里有一个getItemValue方法了。

调用接口获取建议列表

也许有人要问了,这个建议列表为什么一直存在?

这是因为我们为了方便测试给了一个固定的options值,现在来完善一下,修改BookEditor.js:

  1. import React from 'react';
  2. import FormItem from './FormItem';
  3. import AutoComplete from './AutoComplete';
  4. import formProvider from '../utils/formProvider';
  5.  
  6. class BookEditor extends React.Component {
  7. constructor (props) {
  8. super(props);
  9. this.state = {
  10. recommendUsers: []
  11. };
  12. ...
  13. }
  14. ...
  15. getRecommendUsers (partialUserId) {
  16. fetch('http://localhost:8000/user?id_like=' + partialUserId)
  17. .then((res) => res.json())
  18. .then((res) => {
  19. if (res.length === 1 && res[0].id === partialUserId) {
  20. // 如果结果只有1条且id与输入的id一致,说明输入的id已经完整了,没必要再设置建议列表
  21. return;
  22. }
  23.  
  24. // 设置建议列表
  25. this.setState({
  26. recommendUsers: res.map((user) => {
  27. return {
  28. text: `${user.id}(${user.name})`,
  29. value: user.id
  30. };
  31. })
  32. });
  33. });
  34. }
  35.  
  36. timer = 0;
  37. handleOwnerIdChange (value) {
  38. this.props.onFormChange('owner_id', value);
  39. this.setState({recommendUsers: []});
  40.  
  41. // 使用“节流”的方式进行请求,防止用户输入的过程中过多地发送请求
  42. if (this.timer) {
  43. clearTimeout(this.timer);
  44. }
  45.  
  46. if (value) {
  47. // 200毫秒内只会发送1次请求
  48. this.timer = setTimeout(() => {
  49. // 真正的请求方法
  50. this.getRecommendUsers(value);
  51. this.timer = 0;
  52. }, 200);
  53. }
  54. }
  55.  
  56. render () {
  57. const {recommendUsers} = this.state;
  58. const {form: {name, price, owner_id}, onFormChange} = this.props;
  59. return (
  60. <form onSubmit={this.handleSubmit}>
  61. ...
  62. <FormItem label="所有者:" valid={owner_id.valid} error={owner_id.error}>
  63. <AutoComplete
  64. value={owner_id.value ? owner_id.value + '' : ''}
  65. options={recommendUsers}
  66. onValueChange={value => this.handleOwnerIdChange(value)}
  67. />
  68. </FormItem>
  69. ...
  70. </form>
  71. );
  72. }
  73. }
  74. ...

看一下最后的样子:

完整的代码:

src / components / BookEditor.js

  1. /**
  2. * 图书编辑器组件
  3. */
  4. import React from 'react';
  5. import FormItem from '../components/FormItem'; // 或写成 ./FormItem
  6. // 高阶组件 formProvider表单验证
  7. import formProvider from '../utils/formProvider';
  8. // 引入 prop-types
  9. import PropTypes from 'prop-types';
  10. // 引入自动完成组件
  11. import AutoComplete from './AutoComplete';
  12.  
  13. class BookEditor extends React.Component {
  14. // 构造器
  15. constructor(props) {
  16. super(props);
  17.  
  18. this.state = {
  19. recommendUsers: []
  20. };
  21. }
  22.  
  23. // 获取推荐用户信息
  24. getRecommendUsers (partialUserId) {
  25. // 请求数据
  26. fetch('http://localhost:8000/user?id_like=' + partialUserId)
  27. .then((res) => res.json())
  28. .then((res) => {
  29. if(res.length === 1 && res[0].id === partialUserId){
  30. // 如果结果只有1条且id与输入的id一致,说明输入的id已经完整了,没必要再设置建议列表
  31. return;
  32. }
  33.  
  34. // 设置建议列表
  35. this.setState({
  36. recommendUsers: res.map((user) => {
  37. return {
  38. text: `${user.id}(${user.name})`,
  39. value: user.id
  40. }
  41. })
  42. });
  43. })
  44. }
  45.  
  46. // 计时器
  47. timer = 0;
  48. handleOwnerIdChange(value){
  49. this.props.onFormChange('owner_id', value);
  50. this.setState({
  51. recommendUsers: []
  52. });
  53.  
  54. // 使用"节流"的方式进行请求,防止用户输入的过程中过多地发送请求
  55. if(this.timer){
  56. // 清除计时器
  57. clearTimeout(this.timer);
  58. }
  59.  
  60. if(value){
  61. // 200毫秒内只会发送1次请求
  62. this.timer = setTimeout(() => {
  63. // 真正的请求方法
  64. this.getRecommendUsers(value);
  65. this.timer = 0;
  66. }, 200);
  67. }
  68. }
  69.  
  70. // 按钮提交事件
  71. handleSubmit(e){
  72. // 阻止表单submit事件自动跳转页面的动作
  73. e.preventDefault();
  74. // 定义常量
  75. const { form: { name, price, owner_id }, formValid, editTarget} = this.props; // 组件传值
  76. // 验证
  77. if(!formValid){
  78. alert('请填写正确的信息后重试');
  79. return;
  80. }
  81.  
  82. // 默认值
  83. let editType = '添加';
  84. let apiUrl = 'http://localhost:8000/book';
  85. let method = 'post';
  86. // 判断类型
  87. if(editTarget){
  88. editType = '编辑';
  89. apiUrl += '/' + editTarget.id;
  90. method = 'put';
  91. }
  92.  
  93. // 发送请求
  94. fetch(apiUrl, {
  95. method, // method: method 的简写
  96. // 使用fetch提交的json数据需要使用JSON.stringify转换为字符串
  97. body: JSON.stringify({
  98. name: name.value,
  99. price: price.value,
  100. owner_id: owner_id.value
  101. }),
  102. headers: {
  103. 'Content-Type': 'application/json'
  104. }
  105. })
  106. // 强制回调的数据格式为json
  107. .then((res) => res.json())
  108. // 成功的回调
  109. .then((res) => {
  110. // 当添加成功时,返回的json对象中应包含一个有效的id字段
  111. // 所以可以使用res.id来判断添加是否成功
  112. if(res.id){
  113. alert(editType + '添加图书成功!');
  114. this.context.router.push('/book/list'); // 跳转到用户列表页面
  115. return;
  116. }else{
  117. alert(editType + '添加图书失败!');
  118. }
  119. })
  120. // 失败的回调
  121. .catch((err) => console.error(err));
  122. }
  123.  
  124. // 生命周期--组件加载中
  125. componentWillMount(){
  126. const {editTarget, setFormValues} = this.props;
  127. if(editTarget){
  128. setFormValues(editTarget);
  129. }
  130. }
  131.  
  132. render() {
  133. // 定义常量
  134. const {recommendUsers} = this.state;
  135. const {form: {name, price, owner_id}, onFormChange} = this.props;
  136. return (
  137. <form onSubmit={(e) => this.handleSubmit(e)}>
  138. <FormItem label="书名:" valid={name.valid} error={name.error}>
  139. <input
  140. type="text"
  141. value={name.value}
  142. onChange={(e) => onFormChange('name', e.target.value)}/>
  143. </FormItem>
  144.  
  145. <FormItem label="价格:" valid={price.valid} error={price.error}>
  146. <input
  147. type="number"
  148. value={price.value || ''}
  149. onChange={(e) => onFormChange('price', e.target.value)}/>
  150. </FormItem>
  151.  
  152. <FormItem label="所有者:" valid={owner_id.valid} error={owner_id.error}>
  153. <AutoComplete
  154. value={owner_id.value ? owner_id.value + '' : ''}
  155. options={recommendUsers}
  156. onValueChange={value => this.handleOwnerIdChange(value)} />
  157. </FormItem>
  158. <br />
  159. <input type="submit" value="提交" />
  160. </form>
  161. );
  162. }
  163. }
  164.  
  165. // 必须给BookEditor定义一个包含router属性的contextTypes
  166. // 使得组件中可以通过this.context.router来使用React Router提供的方法
  167. BookEditor.contextTypes = {
  168. router: PropTypes.object.isRequired
  169. };
  170.  
  171. // 实例化
  172. BookEditor = formProvider({ // field 对象
  173. // 书名
  174. name: {
  175. defaultValue: '',
  176. rules: [
  177. {
  178. pattern: function (value) {
  179. return value.length > 0;
  180. },
  181. error: '请输入图书户名'
  182. },
  183. {
  184. pattern: /^.{1,10}$/,
  185. error: '图书名最多10个字符'
  186. }
  187. ]
  188. },
  189. // 价格
  190. price: {
  191. defaultValue: 0,
  192. rules: [
  193. {
  194. pattern: function(value){
  195. return value > 0;
  196. },
  197. error: '价格必须大于0'
  198. }
  199. ]
  200. },
  201. // 所有者
  202. owner_id: {
  203. defaultValue: '',
  204. rules: [
  205. {
  206. pattern: function (value) {
  207. return value > 0;
  208. },
  209. error: '请输入所有者名称'
  210. },
  211. {
  212. pattern: /^.{1,10}$/,
  213. error: '所有者名称最多10个字符'
  214. }
  215. ]
  216. }
  217. })(BookEditor);
  218.  
  219. export default BookEditor;

.

react 项目实战(八)图书管理与自动完成的更多相关文章

  1. React项目实战:react-redux-router基本原理

    React相关 React 是一个采用声明式,高效而且灵活的用来构建用户界面的框架. JSX 本质上来讲,JSX 只是为React.createElement(component, props, .. ...

  2. react 项目实战(十)引入AntDesign组件库

    本篇带你使用 AntDesign 组件库为我们的系统换上产品级的UI! 安装组件库 在项目目录下执行:npm i antd@3.3.0 -S 或 yarn add antd 安装组件包 执行:npm ...

  3. 《Node+MongoDB+React 项目实战开发》已出版

    前言 从深圳回长沙已经快4个月了,除了把车开熟练了外,并没有什么值得一提的,长沙这边要么就是连续下一个月雨,要么就是连续一个月高温暴晒,上班更是没啥子意思,长沙这边的公司和深圳落差挺大的,薪资也是断崖 ...

  4. asp.net core react 项目实战(一)

    asp.net-core-react asp.net core react 简介 开发依赖环境 .NET Core SDK (reflecting any global.json): Version: ...

  5. struts2+hibernate 项目实战:图书管理系统

    经典项目,练手必备. 图书管理系统 需求分析(大致,并不专业):1.需要有用户管理: 1.1 用户注册: 1.2 用户登录: 1.3 用户信息修改: 1.4 用户修改密码: 2.需要有书本管理: 2. ...

  6. react 项目实战(九)登录与身份认证

    SPA的鉴权方式和传统的web应用不同:由于页面的渲染不再依赖服务端,与服务端的交互都通过接口来完成,而REASTful风格的接口提倡无状态(state less),通常不使用cookie和sessi ...

  7. react 项目实战(四)组件化表单/表单控件 高阶组件

    高阶组件:formProvider 高阶组件就是返回组件的组件(函数) 为什么要通过一个组件去返回另一个组件? 使用高阶组件可以在不修改原组件代码的情况下,修改原组件的行为或增强功能. 我们现在已经有 ...

  8. react 项目实战(七)用户编辑与删除

    添加操作列 编辑与删除功能都是针对已存在的某一个用户执行的操作,所以在用户列表中需要再加一个“操作”列来展现[编辑]与[删除]这两个按钮. 修改/src/pages/UserList.js文件,添加方 ...

  9. react 项目实战(三)表单验证

    我们需要记录每一个字段当前的有效状态,有效时隐藏错误信息,无效时显示错误信息. 而这个有效/无效,可以在表单值改变的时候进行判断. 我们对/src/pages/UserAdd.js进行修改: 首先修改 ...

随机推荐

  1. pringBoot Controller接收参数的几种常用方式

    第一类:请求路径参数1.@PathVariable 获取路径参数.即url/{id}这种形式.2.@RequestParam 获取查询参数.即url?name=这种形式例子 GEThttp://loc ...

  2. vim里面搜索字符串

    直接在命令模式/+字符串就能搜索到,查找下一个,按“n”

  3. UTF-8,UTF-16

    UTF是 Unicode Translation Format,即把Unicode转做某种格式的意思. 在Unicode基本多文种平面定义的字符(无论是拉丁字母.汉字或其他文字或符号),一律使用2字节 ...

  4. sql 触发器 针对一张表数据写入 另一张表 的增删改

    ALTER TRIGGER [dbo].[tri_test2] ON [dbo].[student] for INSERT,DELETE,UPDATEAS BEGIN if not exists (s ...

  5. promise的简单使用

    var p = new Promise(function (resolve,reject) { /*setTimeout(function () { resolve('success') },3000 ...

  6. mac apache 配置

    mac系统自带apache这无疑给广大的开发朋友提供了便利,接下来是针对其中的一些说明 一.自带apache相关命令 1. sudo apachectl start 启动服务,需要权限,就是你计算机的 ...

  7. 合并多个MP4文件

    把多个MP4文件连接起来的方法与音频文件不太一样,比较有效的方法是: $ cat mylist.txt file '/path/to/file1' file '/path/to/file2' file ...

  8. python3.x Day5 面向对象

    类:类是指:对具有相同属性的事物的抽象.蓝图.原型.在类中定义了这些事物都具备的属性和共同的方法. 对象:一个对象就是一个类实例化以后的实例,一个类必须经过实例化后才能在程序中被使用,一个类可以实例化 ...

  9. 树莓派-3 启用root

    默认是user: pi,  password: raspberry 通过如下设置root密码并启用 pi@raspberrypi:~ $ sudo passwd root Enter new UNIX ...

  10. PHP读取超大的excel文件数据的方案

    场景和痛点 说明 今天因为一个老同学找我,说自己公司的物流业务都是现在用excel处理,按月因为数据量大,一个excel差不多有百万数据,文件有接近100M,打开和搜索就相当的慢 联想到场景:要导入数 ...