【React全家桶入门之九】图书管理与自动完成

来源:互联网 发布:双色球 知乎 编辑:程序博客网 时间:2024/06/10 15:11

图书管理

还记得搭建项目的时候在db.json文件里写的book吗?

来回顾一下book这个“数据表”的结构:

{  ...  "book": [    {      "id": 10000,      "name": "前端从入门到精通",      "price": 9990,      "owner_id": 10000    },    {      "id": 10001,      "name": "Java从入门到放弃",      "price": 1990,      "owner_id": 10001    }  ]}

除了id外,每本书都包含name、price和owner_id三个字段。

有了之前实现用户管理的经验,我们很快就可以实现图书的增删改查,两者的代码基本相同,此处不再赘述(代码请查看项目代码的C09_book版本)。

关于【用户管理】与【图书管理】之间相同的代码,可以使用更高层次的高阶组件来实现组件的配置化(留个坑抽空填)。若你有更好的想法,欢迎在博客中留言与我进一步探讨。

图书的添加/编辑有一个owner_id用来代表图书的“所有者”,它的值是用户表中一个用户的id,一个用户能拥有多本书,一本书只能被一个用户拥有,这是一个一对多的关系。

上面提到的代码中只是简单地提供了一个输入框来接收输入的用户ID,这样的体验并不够好,我们来实现一个通用的自动完成组件。

自动完成组件

自动完成(英语:Auto-Complete)功能,指用户在输入一个字符串的部分内容时,就提供下拉菜单自动推荐相关常用字符串供用户选择以快速输入的一项功能特性。

找了个例子看一下效果:

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

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

  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';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加上className:

import React, { PropTypes } from 'react';import style from '../styles/auto-complete.less';class AutoComplete extends React.Component {  ...  render () {    const {displayValue, activeItemIndex} = this.state;    const {value, options} = this.props;    return (      <div className={style.wrapper}>        <input value={displayValue || value}/>        {options.length > 0 && (          <ul className={style.options}>            {              options.map((item, index) => {                return (                  <li key={index} className={activeItemIndex === index ? style.active : ''}>                    {item.text || item}                  </li>                );              })            }          </ul>        )}      </div>    );  }}

稍微顺眼一些了吧:

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

  • 输入框的onChange
  • 输入框的onKeyDown,用于对上下键、回车键进行监听处理
  • 列表项目的onClick
  • 列表项目的onMouseEnter,用于在鼠标移入时设置activeItemIndex
  • 列表的onMouseLeave,用户鼠标移出时重置activeItemIndex
...function getItemValue (item) {  return item.value || item;}class AutoComplete extends React.Component {  constructor (props) {    ...    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={style.wrapper}>        <input          value={displayValue || value}          onChange={e => this.handleChange(e.target.value)}          onKeyDown={this.handleKeyDown}        />        {options.length > 0 && (          <ul className={style.options} onMouseLeave={this.handleLeave}>            {              options.map((item, index) => {                return (                  <li                    key={index}                    className={index === activeItemIndex ? style.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: ''});  }  ...

看一下效果:

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

但是如果吧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:3000/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>    );  }}...

看一下最后的样子:

0 0