iT邦幫忙

2024 iThome 鐵人賽

DAY 24
0
自我挑戰組

React 開發者的 TypeScript 探索之旅系列 第 24

【 Day 24 】加入編輯功能

  • 分享至 

  • xImage
  •  

本系列文章 GitHub

今天我們要來完成 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
)}

更新邏輯

TodoContext.tsx

編輯文字會需要 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
    ),
  }

Todo.tsx

建立提交函式:

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

我們接下來要實現 isFinished 的狀態切換。

TodoContext.tsx

首先,為 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
    ),
  }

Todo.tsx

使用 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 小技巧。


上一篇
【 Day 23 】使用 useContext、useReducer 優化資料管理(二)
下一篇
【 Day 25 】重構提示訊息
系列文
React 開發者的 TypeScript 探索之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言