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;

项目结构:

自动完成组件

找了个例子看一下效果:

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

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

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

组件结构

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

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

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

<div>
  <input type="text"/>
  <ul>
    <li>...</li>
    ...
  </ul>
</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文件,写入组件的基本代码:

/**
 * 自动完成组件
 */
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;

.

原文地址:https://www.cnblogs.com/crazycode2/p/8543950.html