ToDo List (待辦事項) 簡易實作(無用到 Redux 之類的)
首先先來看一下成果的樣子:
並將它分
接下來是需求說明:
- 有一個供使用者輸入的<input type="text"/> 框,使用者輸入待辦事項的文字後,
按下Enter鍵(配合 <form>)或按下 "addToDo" 按鈕,可以加進下方的待辦事項顯示區域 (<ul><li>)。
為了演示一下雙向資料流的實作,放了兩個待辦事項輸入框,
在使用者在其中一個輸入框輸入資料時,另一個的輸入框會同步顯示輸入的待辦事項的內容。 - 使用者在輸入待辦事項的文字時,下方有一行文字可以顯示使用者正在輸入的文字。
- 下方的待辦事項清單中,各個待辦事項右方有一個 "Delete" 按鈕,按下可將待辦事項移除
再來把其需求分解成各個Component,如下圖所示:
- Main.jsx :
統整所有 Component 的最上層 Component,也管理著 state。 - TextInput.jsx :
供使用者輸入待辦事項的地方。 - TextDisplayer.jsx :
顯示使用者正在輸入的待辦事項。 - ToDoList.jsx :
顯示待辦事項例表的 Component,其內部用到了子 Component, "ToDoRow.jsx"。 - 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 (TextDisplayer 是此例中最簡單的元件,所做的事就只有把傳進的 text 顯示出來。Text you typed : {this.props.text}); } } export {TextDisplayer}
/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的其他相關文章:
React - Redux + Hook 練習 - ToDo List (待辦事項) 簡易實作
參考資料:
參考資料:
- React 系列一 之 TodoList
- Day 16: React篇: TextInput程式
- Fragments
- Parse Error: Adjacent JSX elements must be wrapped in an enclosing tag
- How to fix ' Support for the experimental syntax 'exportDefaultFrom' isn't currently enabled' in node
- [筆記][React]React的目錄結構篇
- [筆記][React]從零到一的webpack開發環境(2)-React開發篇
- 用 React 思考
沒有留言 :
張貼留言