react 項目實戰(八)圖書管理與自動完成

圖書管理

src / pages / BookAdd.js   // 圖書添加頁javascript

/**
 * 圖書添加頁面
 */
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   // 圖書列表頁css

/**
 * 圖書列表頁面
 */
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   // 圖書編輯組件java

/**
 * 圖書編輯器組件
 */
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   // 圖書編輯頁react

/**
 * 編輯圖書頁面
 */
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;

項目結構:json

自動完成組件

找了個例子看一下效果:api

能夠發現,這是一個包含一個輸入框、一個下拉框的複合控件。數組

實現一個通用組件,在動手寫代碼以前我會作如下準備工做:app

  1. 肯定組件結構
  2. 觀察組件邏輯
  3. 肯定組件內部狀態(state)
  4. 肯定組件向外暴露的屬性(props)

組件結構

上面提了,這個組件由一個輸入框和一個下拉框組成。less

注意,這裏的下拉框是一個「僞」下拉框,並非指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;

.

相關文章
相關標籤/搜索