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 (
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的其他相關文章:
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 思考



沒有留言 :
張貼留言