今天我們要來完成 Todo List 的最後一個主要功能,也就是編輯功能。
這邊先來釐清需求:
預計的做法是點擊 Edit
按鈕之後,原先的 Todo
內容會切換為輸入框,而 Edit
按鈕會變成 Confirm
,若輸入欄位為空,則不更新狀態。
在做編輯功能之前,我們要稍微修改一下 TodoList
的程式碼,因為我們會需要原本的 title
,來當作編輯新標題的初始值,但是我們當初是以 <p>
來包裹 {todo.title}
,並作為 children
傳遞給 Todo
,這樣 Todo
收到的 children
資料型別會是 Object
,物件無法作為 <input>
的 value
值,因此我們需要刪除 TodoList
中的 <p>
,這樣傳入型別就會變成 String
:
<ul>
{todos.map((todo) => (
<li key={todo.id} className='list-none'>
<Todo
isFinished={todo.isFinished}
id={todo.id}
onDelete={onDeleteTodo}
>
{todo.title}
</Todo>
</li>
))}
</ul>
根據需求,我們需要一個狀態來改變按鈕的內容,以及它相應的功能搭配,由於這個狀態只會在 Todo.tsx
檔案中使用,因此這邊我們選擇使用 useState
來保存局部狀態,這樣可以有效管理按鈕狀態和切換邏輯:
const [isEditing, setIsEditing] = useState(false)
並加入切換狀態的函式:
const toggleEditHandler = () => {
setIsEditing(!isEditing)
}
修改前的按鈕結構如下:
<div className='flex gap-[16px]'>
<button>Edit</button>
<button
onClick={() => {
onDelete(id)
}}
>
Delete
</button>
</div>
將按鈕的文字改為條件式渲染,並為按鈕綁定點擊事件:
<div className='flex gap-[16px]'>
<button onClick={toggleEditHandler}>{isEditing ? 'Confirm' : 'Edit'}</button>
<button
onClick={() => {
onDelete(id)
}}
>
Delete
</button>
</div>
試著新增一筆 Todo
,並點擊 Edit
按鈕,它應該要能夠自由切換。
新增管理標題的狀態:
const [newTitle, setNewTitle] = useState(children)
新增監聽 input
內容變化事件函式,ChangeEvent
為 React 所提供,為 onChange
事件的型別,由於我們會需要透過 event.target.value
取得值,因此我們必須告訴 TypeScript,這是一個 HTMLInputElement
:
const changeTitleHandler = (event: ChangeEvent<HTMLInputElement>) => {
setNewTitle(event.target.value)
}
將 JSX
內容改為條件式渲染,並綁上事件函式:
{isEditing ? (
<input
type='text'
value={newTitle}
className='px-2 py-1'
onChange={changeTitleHandler}
/>
) : (
children
)}
這時候會出現報錯 Type 'ReactNode' is not assignable to type 'string | number | readonly string[] | undefined'.
,因此我們要檢查型別,以更嚴謹的方式來編寫:
{isEditing ? (
<input
type='text'
value={typeof newTitle === 'string' ? newTitle : ''}
className='px-2 py-1'
onChange={changeTitleHandler}
/>
) : (
children
)}
編輯文字會需要 id
比對,以及新的文字內容作為更新,因此 payload
需要包含這兩項內容的型別:
type ActionType =
| { type: 'ADD_TODO'; payload: string }
| { type: 'DELETE_TODO'; payload: number }
| { type: 'EDIT_TODO_TITLE'; payload: { id: number; title: string } }
在 reducer 內加入相應的邏輯:
case 'EDIT_TODO_TITLE':
return {
...state,
todos: state.todos.map((todo) =>
todo.id === action.payload.id
? { ...todo, title: action.payload.title }
: todo
),
}
建立提交函式:
const submitNewTitle = () => {
if (typeof newTitle === 'string' && newTitle.trim().length === 0) {
setNewTitle(children)
setIsEditing(false)
return
}
if (typeof newTitle === 'string' && newTitle.trim().length > 0) {
dispatch({
type: 'EDIT_TODO_TITLE',
payload: { id, title: newTitle },
})
}
}
在非編輯模式下執行提交函式,透過 useEffect
監管 isEditing
狀態,是為了確保每次用戶從編輯模式退出時,狀態能夠正確地提交,而不會造成重複提交:
useEffect(() => {
if (!isEditing) {
submitNewTitle()
}
}, [isEditing])
我們接下來要實現 isFinished
的狀態切換。
首先,為 action
定義型別,我們只會需要 id
,因此型別為 number
:
type ActionType =
| { type: 'ADD_TODO'; payload: string }
| { type: 'DELETE_TODO'; payload: number }
| { type: 'EDIT_TODO_TITLE'; payload: { id: number; title: string } }
| { type: 'TOGGLE_TODO_ISFINISHED'; payload: number }
於 reducer
內加入更新邏輯:
case 'TOGGLE_TODO_ISFINISHED':
return {
...state,
todos: state.todos.map((todo) =>
todo.id === action.payload
? { ...todo, isFinished: !todo.isFinished }
: todo
),
}
使用 dispatch
來更新 isFinished
狀態:
const checkboxHandler = () => {
dispatch({
type: 'TOGGLE_TODO_ISFINISHED',
payload: id,
})
}
將 checkbox
綁定到 checkboxHandler
:
<input type='checkbox' onChange={checkboxHandler} checked={isFinished} />
經過了這幾天的奮鬥,我們終於把 Todo List 的基本功能都完成了,也在這個實作過程中,認識了許多由 React 所提供的型別,在接下來的幾天,我們會再稍微對 Todo List 進行優化,並且探索 Todo List 沒有機會用到的 TypeScript 小技巧。