接下來這一章節我們將會實作一些小東西,主要是要整合前面的知識點,否則單看知識而沒有任何一點輸出的話是沒有任何用處的,因此就讓我們準備練習一下吧。
首先這邊我用 TailwindCSS 做了一個 ToDoList 範本,而這個範本主要會預先載入以下 CDN
因此會建議你往下看之前,可以先 Fork 回去到你自己的 CodePen 中,等一下你才方便實作,這裡面也包含了基本 HTML 樣板
接下來我們將會實作幾個功能
+
」後出現在下方接下來就讓我們開始實作,首先先將基本的 React 建立出來,然後這邊可以先將 #root
裡面的元素先全部丟進去 React return
中,接著你必須修正幾個錯誤,否則會無法正常運作
class
屬於 JavaScript 保留字,因此 class
要改成 className
Expected corresponding JSX closing tag for <input>
,這個錯誤簡單來講就是 JSX 比較嚴謹,因此要調整 input
補上結尾標籤(也就是補上 /
)const App = () => {
return (
<div>
<div className="bg-indigo-500 p-5 h-screen">
<div className="max-w-[768px] m-auto bg-white p-5">
<h1 className="text-center text-2xl mb-4">React ToDoList</h1>
<div className="flex">
<input type="text" className="w-full rounded-l-lg border-l-2 border-y-2 border-indigo-300 pl-4 focus:outline-indigo-500 focus:outline-none focus:outline-offset-0" placeholder="請輸入你的代辦事項" />
<button className="w-[50px] h-[50px] border-0 bg-sky-500 hover:bg-sky-600 rounded-r-lg text-white transition duration-700">+</button>
</div>
<ul>
<li className="py-4">
<label>
<input type="checkbox" />
今天要洗碗
</label>
</li>
</ul>
<div className="flex justify-between items-center">
<p>
目前有 <span className="font-medium">1</span> 個事項待完成
</p>
<button type="button" className="bg-red-300 p-2 rounded-md hover:bg-red-400 transition duration-700">Clear All Task</button>
</div>
</div>
</div>
</div>
)
}
const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);
如果你有依照以上步驟調整,你會發現 React 已經可以正常運作了,接著我們就可以開始實作功能了
接下來要宣告一個初始資料狀態,是要拿來放 todo 新增的資料,這邊要注意的是初始資料會是一個陣列
const [ todoList, setTodoList ] = React.useState([]);
那麼接下來我們要先替幾個東西綁定事件,也就是點擊事件,第一個是「+
」按鈕,而這個事件我們先對應到一個函式叫做 addTodo
<button onClick={ addTodo } className="w-[50px] h-[50px] border-0 bg-sky-500 hover:bg-sky-600 rounded-r-lg text-white transition duration-700">+</button>
const addTodo = () => {
}
第二個則是 「Clear All Task
」 按鈕,對應的是 remoteAllTodo
<button onClick={ remoteAllTodo } type="button" className="bg-red-300 p-2 rounded-md hover:bg-red-400 transition duration-700">Clear All Todo</button>
const remoteAllTodo = () => {
}
第三個就是勾選 todo 狀態的 checkbox
,但是這邊我們先不綁事件,先把上面這兩個綁好就好,等一下後面再來一起處理。
第一個我們先來寫新增代辦的函式,當我們觸發 onClick 事件之後,要去取得 input
的 value 因此要稍微調整一下 input
給予一個 id
<input id="todoInput" type="text" className="w-full rounded-l-lg border-l-2 border-y-2 border-indigo-300 pl-4 focus:outline-indigo-500 focus:outline-none focus:outline-offset-0" placeholder="請輸入你的代辦事項" />
接著改寫一下 addTodo
函式
const addTodo = (event) => {
const value = document.querySelector('#todoInput').value;
console.log(value);
};
接下來你在 input
隨便輸入並按下「+
」 就可以看到我們正確取得 input
的值了。
當我們取得值之後,接下來就要將值推進去到 setTodoList
中,首先第一個會是解構原本資料的物件資料,第二個我們則要寫入另一個物件
const addTodo = () => {
const input = document.querySelector('#todoInput');
setTodoList([
...todoList,
{
name: input.value
}
])
};
之所以要先解構原本資料是因為我們不想要直接覆蓋掉原本的資料,而是要將新的資料推進去,這樣才不會造成資料的遺失,因此才會需要先解構原本的資料,接著再推進去新的資料。
接下來我們也要寫入代辦新增的時間,時間最簡單方式就是使用 Date.now()
,剛好這個 Date.now()
非常適合作為一個 Key
使用,因此就會直接將它作為一個 Key
使用以及最後補個這個代辦事項的狀態(已完成與未完成)
const addTodo = (event) => {
const input = document.querySelector('#todoInput');
setTodoList([
...todoList,
{
id: Date.now(),
name: input.value,
status: false,
}
])
};
最後別忘記要清空 input 的欄位,畢竟新增成功就要清空
const addTodo = (event) => {
const input = document.querySelector('#todoInput');
setTodoList([
...todoList,
{
id: Date.now(),
name: input.value,
status: false,
}
])
input.value = '';
};
接下來就是要渲染我們儲存 todoList
的代辦事項資料,如果是在 Vue 裡面我們會使用 v-for
來渲染畫面,而在 React 則是使用 map
並重新組合成一個 HTML 回傳給 JSX 來渲染資料
因此要將這一塊
<ul>
<li className="py-4">
<label>
<input type="checkbox" class="mr-2" />
今天要洗碗
</label>
</li>
</ul>
改成使用 map
組合 DOM,記得不要忘記 key
的存在,(記得補一個 checked
)
<ul>
{
todoList.map((todo) => (
<li className="py-4" key={ todo.id }>
<label>
<input type="checkbox" class="mr-2" checked={ todo.status }/>
{ todo.name }
</label>
</li>
))
}
</ul>
你可能會想說為什麼使用 map
就可以正常渲染資料?在前面章節我們有說過 JSX 本身會針對陣列展開 (Spread),而 map
會回傳一個陣列,因此就會正常渲染資料。
接著這邊你也可以順便把下方這一段改一下
<p>
目前有 <span className="font-medium">1</span> 個事項待完成
</p>
改成以下這樣就可以了
<p>
目前有 <span className="font-medium">{ todoList.length }</span> 個事項待完成
</p>
基本上到了這一步驟之後,你就可以開始試著新增一個代辦事項,然後你也可以在下方看到資料被渲染出來囉。
那麼接下來我們要增加當使用者勾選任務後,會補上一個刪除線的效果,如同底下
這是一條刪除線
而判斷方式非常簡單,由於我們是使用 TailwindCSS 來實作,因此在 className
上面補上一個判斷,當 state
為 true
的話,就會自動加上 line-through
樣式即可。
只是在開始之前我們要先針對 label
增加一個屬性,也就是 data-*
並傳入 key
,這個我們稍後會使用到
<ul>
{
todoList.map((todo) => (
<li className="py-4" key={ todo.id }>
<label className={ todo.status ? 'line-through' : ''}>
<input type="checkbox" data-id={ todo.id } className="mr-2" checked={ todo.status }/>
{ todo.name }
</label>
</li>
))
}
</ul>
這時候你新增每一筆資料就都可以看到 Li
多了一個 data-id
的屬性。
接下來也要針對 label
綁定一個事件,也就是 onClick
事件,對應到 updateTodo
方法
<ul>
{
todoList.map((todo) => (
<li className="py-4" key={ todo.id } data-id={ todo.id } >
<label className={ todo.status ? 'line-through' : ''} >
<input onClick={ updateTodo } type="checkbox" className="mr-2" data-id={ todo.id } checked={ todo.status }/>
{ todo.name }
</label>
</li>
))
}
</ul>
而 updateTodo
會透過 event.target
去尋找 data-*
然後用這種方式去找出資料中符合 ID 的部分,並將該陣列資料轉換為 false
or true
const updateTodo = (event) => {
const { id } = event.target.dataset;
const newTodoList = todoList.map((todo) => {
if(todo.id === Number(id)) {
todo.status = !todo.status;
}
return todo;
});
setTodoList([ ...newTodoList ]);
}
由於我們這邊解構出來的 dataset
都是一個字串,因此要記得補上 Number 做型別轉換。
接著這時候你可以嘗試輸入代辦事項並新增代辦,然後在打開 Console 應該會發現這個錯誤
Warning: You provided a
checked
prop to a form field without anonChange
handler. This will render a read-only field. If the field should be mutable usedefaultChecked
. Otherwise, set eitheronChange
orreadOnly
.
而這有幾種解決方式,將原本的 checked
改成 defaultChecked
,或是在 input
上面增加一個 onChange
事件,這邊我們選擇後者。
<ul>
{
todoList.map((todo) => (
<li className="py-4" key={ todo.id } data-id={ todo.id } >
<label className={ todo.status ? 'line-through' : ''} >
<input onChange={ updateTodo } type="checkbox" className="mr-2" data-id={ todo.id } checked={ todo.status }/>
{ todo.name }
</label>
</li>
))
}
</ul>
這樣子就不會再出現該警告訊息了。
恭喜你做到這邊的時候就已經將大部分功能給完成了!
所以最後就是單純的去撰寫 remoteAllTodo
裡面的功能,而這也其實非常簡單,因為我們只是要將代辦事項清空,因此只需要這樣寫即可
const remoteAllTodo = () => {
setTodoList([]);
};
非常簡單吧?你的第一個簡單版 React TodoList 就這樣完成了。
這邊也提供完整版 TodoList CodePen 的連結。
除此之外我也提供一版 Vue 版本的 TodoList CodePen 可以讓你比較一下兩者的差異。
本文將會同步更新到我的部落格