2019年12月7日 星期六

React 練習 - ToDo List (待辦事項) 簡易實作

這篇要來記錄一下 React 的
ToDo List (待辦事項) 簡易實作(無用到 Redux 之類的)

首先先來看一下成果的樣子:

並將它分

接下來是需求說明:
  1. 有一個供使用者輸入的<input type="text"/> 框,使用者輸入待辦事項的文字後,
    按下Enter鍵(配合 <form>)或按下 "addToDo" 按鈕,可以加進下方的待辦事項顯示區域 (<ul><li>)。
    為了演示一下雙向資料流的實作,放了兩個待辦事項輸入框,
    在使用者在其中一個輸入框輸入資料時,另一個的輸入框會同步顯示輸入的待辦事項的內容。
  2. 使用者在輸入待辦事項的文字時,下方有一行文字可以顯示使用者正在輸入的文字。
  3. 下方的待辦事項清單中,各個待辦事項右方有一個 "Delete" 按鈕,按下可將待辦事項移除

再來把其需求分解成各個Component,如下圖所示:
  1. Main.jsx :
    統整所有 Component 的最上層 Component,也管理著 state。
  2. TextInput.jsx :
    供使用者輸入待辦事項的地方。
  3. TextDisplayer.jsx :
    顯示使用者正在輸入的待辦事項。
  4. ToDoList.jsx :
    顯示待辦事項例表的 Component,其內部用到了子 Component, "ToDoRow.jsx"。
  5. ToDoRow.jsx :
    顯示各個獨立待辦事項的 Component,包含著 Delete 等功能。



再來是檔案結構:


接著來說明演示各檔案內容:

package.json :

{
  "name": "reacttest",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.7.5",
    "@babel/preset-env": "^7.7.5",
    "@babel/preset-react": "^7.7.4",
    "babel-loader": "^8.0.6",
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10"
  },
  "dependencies": {
    "react": "^16.12.0",
    "react-dom": "^16.12.0"
  }
}

node.js 的設定檔,例出了此範例所有需安裝的東西,
因為 webpack 沒有用全局 (global) 安裝,
所以在 scripts 裡設定了 webpack 指令,
只要打上 npm run build 就等同執行 webpack 指令了。

/webpack.config.js :
var path = require('path');
module.exports = {
 entry : "./src/index.jsx",
 output : {
  path : path.resolve('./'),
  filename : "src/index_bundle.js"
 },
 watch : false,
 devtool : 'source-map',
 mode : "development", //"production",
 module : {
  rules : [ {
   test : /\.jsx?$/,
   use : {
    loader : 'babel-loader',
    options : {
     presets : [ '@babel/preset-react', '@babel/preset-env' ]
    }
   }
  }]
 }
}

webpacck 的設定檔,例出了基本的,使用的 loader,
其中可以看到,我們將 ./src/index.jsx 打包成 ./src/index_bundle.js

/index.html :
<html>
<head>

<title>Insert title here</title>
</head>
<body>

<div id="root">
</div>
<script src="index_bundle.js"></script>

</body>
</html>

index.html 中很簡單,<div id="root"> 是待辦事項元件的擺放點,
而 index_bundle.js 就是上述我們用 webpack 打包好的,待辦事項元件的程式。

index.js 只是單純的把 Main.jsx 的 Component 給 export 出去,
用來演示 webpack import 的特性,
因為對 webpack 的 import 語法來說,如果要 import 的 path 填資料夾的路徑的話,
會自己去搜尋資料夾內的 index.js 檔做 import。

/index.jsx :
import React from "react"
import ReactDOM from "react-dom"
import {Main} from "./components/main"

ReactDOM.render(<Main/>, document.getElementById('root'));
指定了在 index.html 中的 #root DOM 做為待辦事項元件的擺放點,
其中 Main 就是上述 Main.jsx 元件。

/src/components/main/index.js :
export * from "./Main.jsx"
index.js 中只是把 Main.jsx 中的元件 export 出去,
之所以要多放這個 index.js 是為了演示 webpack 的 import 特性,
如果webpack import path 是指向資料夾的話,
它會自己去尋找 index.js 去 import,
例如 /index.jsx 中只寫了
import {Main} from "./components/main"
就可以找到 ./components/main/index.js

/src/components/main/Main.jsx :
import React from "react"
import {TextDisplayer} from "./TextDisplayer.jsx"
import {TextInput} from "./TextInput.jsx"
import {ToDoList} from "./ToDoList.jsx"

class Main extends React.Component{
 constructor(props){
  super(props);
  
  this.state = {
   text : "",
   toDoList : [{id : new Date().getTime(), toDoText : "test"}] // [{id : new Date().getTime(),toDoText : ""}]
  };
  
  this.handleTextChanged = this.handleTextChanged.bind(this);
  this.addToDo = this.addToDo.bind(this);
  this.deleteToDo = this.deleteToDo.bind(this);
 }
 
 handleTextChanged(text){
  this.setState({text : text});
 }

 addToDo(){
  if (this.state.text.trim()){
   var updatedToDoList = this.state.toDoList.concat([{
     id : new Date().getTime(), 
     toDoText : this.state.text
   }]);

   this.setState({
    text : "",
    toDoList : updatedToDoList
   });
  }
 }

 deleteToDo(toDoToDelete){
  var updatedToDoList = this.state.toDoList.filter(function(toDo){
    return toDo != toDoToDelete;
  });

  this.setState({
   toDoList : updatedToDoList
  });
 }
 
    render(){
        return (
         <React.Fragment>
          <TextInput text={this.state.text} handleTextChanged={this.handleTextChanged} addToDo={this.addToDo}/>
          <TextInput text={this.state.text} handleTextChanged={this.handleTextChanged} addToDo={this.addToDo}/>
          <TextDisplayer text={this.state.text}/>
    <ToDoList toDoList={this.state.toDoList} deleteToDo={this.deleteToDo}/>
         </React.Fragment>
     );
    }
}

export {Main}
Main.jsx 中整合了 TextInput, TextDisplayer 和 <ToDoList> (內含 ToDoRow DoRow 元件) 三個元件,
並在其中管理 state,state 裡管理著"輸入框 (text)" 裡的字和"待辦事項清單 (toDoList)",

handleTextChanged() 用來更新 state 裡的 text,
addToDo() 和 deleteToDo() 用來新增及刪除待辦事項,
最後在render() 中,將所需的 state, 各 function 傳給各元件。
可以注意到在render()中使用了<React.gragment>來當做根節點,
原因是因為 React 在 render() 的 return 值中,有規定只能返回一個根節點 (即一定要有一個節點在最外層,且只能有一個),當有多個並列節點想要返回時,
可以用一個作用為不產生任何 DOM 的 <react.gragment>  (也可以簡化寫成 <>) 來包住。

/src/components/main/TextDisplayer.jsx:
import React from "react"

class TextDisplayer extends React.Component{
    render(){
        return (
Text you typed : {this.props.text}
); } } export {TextDisplayer}
TextDisplayer 是此例中最簡單的元件,所做的事就只有把傳進的 text 顯示出來。

/src/components/main/TextInput.jsx :


import React from "react"

class TextInput extends React.Component{
 constructor(props){
  super(props);
  
  this.onHandleTextChanged = this.onHandleTextChanged.bind(this);
  this.onHandleAddToDo = this.onHandleAddToDo.bind(this);
 }
 
 onHandleTextChanged(e){
  this.props.handleTextChanged(e.target.value);
 }

 onHandleAddToDo(e){
  this.props.addToDo();
  e.preventDefault();
 }

 render(){
  return (
   <div>
    <form onSubmit={this.onHandleAddToDo}>
     <input type="text" value={this.props.text} onChange={this.onHandleTextChanged} />
     <input type="submit" value="addToDo"/>
    </form>
   </div>
  );
 }
}

export {TextInput}
在 TextInput 元件中,準備了一個 <form>,且綁定了 onSubmit 來呼叫 Main.jsx 從上層傳進來的 addToDo(),
<form> 裡面了放了 <input> 來讓使用者輸入待辦事項,綁定 onChange() 來呼叫 Main.jsx 從上層傳進來的 handleTextChanged(),
並且在 <input> 的 value 中,把上層 Main.jsx 傳進來的 state.text 寫進去。

/src/components/main/ToDoList.jsx :
import React from "react"
import {ToDoRow} from "./ToDoRow.jsx"

class ToDoList extends React.Component{ 
 render(){
  var deleteToDo = this.props.deleteToDo;
  return (
   <ul>
    {  
     this.props.toDoList.map(function(toDo){
      return (
       <ToDoRow toDo={toDo} key={toDo.id} deleteToDo={deleteToDo}/>
      );
     })
    }
   </ul>
  );
 }
}

export {ToDoList}
在 ToDoList 元件中,做的事只有依照傳進來的 toDoList (是個Array) 去產生多個 ToDoRow 元件,
並且把上層傳進來的 deleteToDo() 傳給各 ToDoRow 元件中。

/src/components/main/ToDoRow.jsx :
import React from "react"
class ToDoRow extends React.Component{
 constructor(props){
  super(props);

  this.handleDeleteToDo = this.handleDeleteToDo.bind(this);
 }

 handleDeleteToDo(){
  this.props.deleteToDo(this.props.toDo);
 }

 render(){
  return (
   <li>
    {this.props.toDo.toDoText}
    <button onClick={this.handleDeleteToDo}>Delete</button>
   </li>
  );
 }
}

export {ToDoRow}
在 ToDoRow 元件中,會去顯示傳進來的 toDo.toDoText,
並且在 Delete 用的按鈕上綁定 onClick 去呼叫上層傳進來的 deleteToDo()。

原碼下載 :
ReactTest.7z

本Blog的其他相關文章:

沒有留言 :

張貼留言