# LESSON-18｜JavaScript 基礎：表單驗證與非同步概念

> 對象：完全沒有前端概念的入門者
>
> 時間：3 小時
>
> **本堂專屬焦點：**掌握表單驗證、preventDefault 與非同步狀態流程

## 1. 課程目標

完成一個不重新導覽頁面的聯絡表單：空白欄位由瀏覽器驗證，送出期間顯示 loading 並停用按鈕，最後顯示 success 或 error，且保留輸入內容。

## 2. 任務情境

聯絡表單不能直接刷新頁面消失，訪客需要知道送出中、成功或失敗的結果。

## 3. 先備知識與檔案結構

本堂不要求先會 Promise 或 API。只要知道如何在編輯器儲存檔案、用瀏覽器開啟 files/index.html、點擊連結與按右鍵開啟 DevTools。先從 examples/minimal.html 觀察，再進入 Starter；本堂核心只使用原生 HTML、CSS、JavaScript 與本地 mock，不連線也不需要 npm。

### 檔案地圖

- `examples/minimal.html`：只讀最小實驗：先觀察空白驗證、成功、失敗與按鈕恢復。
- `starter/files/index.html`：Starter 入口：說明本階段並連到 contact.html。
- `starter/files/contact.html`：Starter 表單：完成欄位、required、submit 與 status 的第一個 TODO。
- `starter/files/main.js`：Starter 腳本：先留下 submit handler 與非同步生命週期的 TODO。
- `checkpoint/files/main.js`：Checkpoint 腳本：完整流程只缺 submit callback 內的 preventDefault。
- `solution/files/main.js`：Solution 腳本：完成 mock Promise、try/catch/finally、disabled 與可恢復狀態。

## 授課補強｜180 分鐘教學節奏

**本堂專屬焦點：**掌握表單驗證、preventDefault 與非同步狀態流程

固定的是八段時間，變化的是本堂檔案、教師操作、學生產出與驗收證據。

| 時間 | 教學活動 | 教師交付 | 學生產出 |
| --- | --- | --- | --- |
| 0–10 分鐘 | 情境導入｜聯絡表單不能直接刷新頁面消失，訪客需要知道送出中、成功或失敗的結果。 | 展示 examples/minimal.html、contact.html、main.js、form、status 目前的狀態，先請學生預測「完成一個不重新導覽頁面的聯絡表單：空白欄位由瀏覽器驗證，送出期間顯示 loading 並停用按鈕，最後顯示 success 或 error，且保留輸入內容。」完成後會出現什麼畫面或資料結果。 | 說出本堂問題與完成目標。；完成後：知道要從 examples/minimal.html、contact.html、main.js、form、status 找到第一個可觀察結果。；驗收：以空白欄位的原生驗證、網址不變、idle→loading→success/error、loading 時 disabled、錯誤後可重試、輸入內容保留中的第一項作為導入前後的對照證據。 |
| 10–35 分鐘 | 概念建立｜form、label、required、submit、event、preventDefault、Promise、async/await、try/catch/finally、disabled 與 aria-live | 用 examples/minimal.html、contact.html、main.js、form、status 中的最小片段逐詞解釋 form、label、required、submit、event、preventDefault、Promise、async/await、try/catch/finally、disabled 與 aria-live，每介紹一個詞就回到瀏覽器或 DevTools 指出可觀察結果。 | 完成名詞對照表，並先回答一題預測題。；完成後：能用自己的話解釋 form、label、required、submit、event、preventDefault、Promise、async/await、try/catch/finally、disabled 與 aria-live。；驗收：口頭回答一題預測題，並指出空白欄位的原生驗證、網址不變、idle→loading→success/error、loading 時 disabled、錯誤後可重試、輸入內容保留中對應的畫面、Console、Network、DOM 或資料證據。 |
| 35–55 分鐘 | 教師示範｜先開啟本地 mock 表單，示範空白驗證，再用 submit handler 保留頁面，最後觀察 idle → loading → success/error 的狀態轉換。 | 從最小可執行範例開始，逐次修改 examples/minimal.html、contact.html、main.js、form、status；每次只改一個檢查點，先預測再重新整理或操作。 | 跟著完成一次修改，記下修改前後差異。；完成後：看到 空白欄位的原生驗證、網址不變、idle→loading→success/error、loading 時 disabled、錯誤後可重試、輸入內容保留 中至少一項可觀察結果。；驗收：保留一次修改前後畫面，並記下空白欄位的原生驗證、網址不變、idle→loading→success/error、loading 時 disabled、錯誤後可重試、輸入內容保留中至少兩項觀察結果。 |
| 55–95 分鐘 | 學生跟做｜完成一個不重新導覽頁面的聯絡表單：空白欄位由瀏覽器驗證，送出期間顯示 loading 並停用按鈕，最後顯示 success 或 error，且保留輸入內容。 | 將示範拆成 10–15 分鐘一個小檢查點，巡迴確認學生真的修改到 examples/minimal.html、contact.html、main.js、form、status，卡住時先要求說出症狀與證據。 | 在自己的專案完成：完成一個不重新導覽頁面的聯絡表單：空白欄位由瀏覽器驗證，送出期間顯示 loading 並停用按鈕，最後顯示 success 或 error，且保留輸入內容。；完成後：核心成果能在自己的專案重現，而不是只在講師畫面成功。；驗收：提交核心成果、修改檔案清單，以及空白欄位的原生驗證、網址不變、idle→loading→success/error、loading 時 disabled、錯誤後可重試、輸入內容保留中可重現的至少兩項證據。 |
| 95–110 分鐘 | 觀察與除錯｜故意把 preventDefault 放到 submit callback 外，或讓 catch/finally 沒有恢復按鈕，從 Console、網址、status 文字與 disabled 屬性找出原因。 | 故意重現「故意把 preventDefault 放到 submit callback 外，或讓 catch/finally 沒有恢復按鈕，從 Console、網址、status 文字與 disabled 屬性找出原因。」，示範從症狀、證據、假設、單一修正到重新驗證的除錯紀錄。 | 重現一個錯誤，再只修改一處並重新驗證。；完成後：能寫出症狀、原因、修正與修正後結果。；驗收：留下錯誤狀態與修正後狀態的對照，證明空白欄位的原生驗證、網址不變、idle→loading→success/error、loading 時 disabled、錯誤後可重試、輸入內容保留中的錯誤已消失。 |
| 110–155 分鐘 | 情境變體與獨立挑戰｜把姓名欄位改成預約需求表單，加入模擬失敗與重試；保留同一組狀態流程且不清空使用者輸入。 | 先圈出 examples/minimal.html、contact.html、main.js、form、status 中不變的技術結構，再示範如何完成「把姓名欄位改成預約需求表單，加入模擬失敗與重試；保留同一組狀態流程且不清空使用者輸入。」，最後請學生獨立完成同一變體。 | 完成變體：把姓名欄位改成預約需求表單，加入模擬失敗與重試；保留同一組狀態流程且不清空使用者輸入。；完成後：能說明哪些結構保留、哪些內容替換，且沒有引入超出本堂範圍的技術。；驗收：將變體結果與原始結果並列，附上檔案或操作位置與一次重新驗證紀錄。 |
| 155–175 分鐘 | 驗收、展示與討論｜空白欄位的原生驗證、網址不變、idle→loading→success/error、loading 時 disabled、錯誤後可重試、輸入內容保留 | 依 空白欄位的原生驗證、網址不變、idle→loading→success/error、loading 時 disabled、錯誤後可重試、輸入內容保留 逐項抽查學生的畫面、Console、Network、DOM 或資料與檔案，請學生先展示證據，再討論不同解法。 | 展示：空白欄位的原生驗證、網址不變、idle→loading→success/error、loading 時 disabled、錯誤後可重試、輸入內容保留；完成後：能回答「如果驗收失敗，下一個要查什麼？」。；驗收：留下可重現的驗收包：結果截圖或網址、檔案位置、檢查步驟與實際觀察。 |
| 175–180 分鐘 | 重點回顧與下堂銜接｜Vue CDN：下一堂會把同樣的狀態與畫面更新交給 Vue 管理。 | 用「概念、操作、證據」三問回看 examples/minimal.html、contact.html、main.js、form、status 的成果，再說明它如何接到「Vue CDN：下一堂會把同樣的狀態與畫面更新交給 Vue 管理。」。 | 寫下三句回顧：我理解了什麼、我做出了什麼、我用什麼證據確認；再預測下一堂會修改哪個檔案。；完成後：能用一句話說明本堂成果如何銜接：Vue CDN：下一堂會把同樣的狀態與畫面更新交給 Vue 管理。；驗收：講師抽問一位學生重述Vue CDN：下一堂會把同樣的狀態與畫面更新交給 Vue 管理。，並收回一項本堂驗收證據。 |

## 4. 觀念拆解

本堂第一次出現的詞，先從「白話 → 正式定義 → 何時使用 → 最小例子 → 可觀察證據 → 混淆概念」建立完整理解。

### 表單（form）

- **白話：**收集使用者輸入，並把一組欄位視為同一次送出的 HTML 區塊。
- **正式定義：**HTML 的 form 元素，包含控制項與 submit 行為；瀏覽器會依欄位規則進行原生驗證。
- **使用時機：**需要收集姓名、Email、需求或預約資料時。
- **最小範例：**`<form id="contact-form">...</form>`。
- **觀察證據：**Elements 看到欄位都位於同一個 form 內，空白送出會出現瀏覽器驗證提示。
- **容易混淆：**和普通 div 混淆；div 只是分組，form 有欄位送出與驗證語意。

### label 對應（label association）

- **白話：**欄位名稱和輸入框要互相連得起來。
- **正式定義：**以 label 的 for 屬性對應控制項的 id，建立可點擊、可讀取的欄位關聯。
- **使用時機：**希望點擊「姓名」文字就能把焦點放進正確欄位時。
- **最小範例：**`<label for="name">姓名</label><input id="name">`。
- **觀察證據：**點擊文字後 input 取得焦點，Elements 的 for 與 id 完全相同。
- **容易混淆：**和 name 混淆；name 是送出資料的欄位名稱，不負責 label 的關聯。

### required（required attribute）

- **白話：**告訴瀏覽器這個欄位不能空白。
- **正式定義：**HTML constraint validation 使用的布林屬性；控制項沒有值時，瀏覽器會阻止有效的 submit 流程。
- **使用時機：**姓名、Email 或需求是送出表單的必要資料時。
- **最小範例：**`<input id="name" name="name" required>`。
- **觀察證據：**空白按送出時，瀏覽器顯示原生驗證提示，頁面沒有進入成功狀態。
- **容易混淆：**和 placeholder 混淆；placeholder 只是提示文字，不會要求欄位有值。

### submit 事件（submit event）

- **白話：**使用者要求送出整份表單的那一刻。
- **正式定義：**瀏覽器在 form 嘗試送出時派送的事件；有效欄位通過原生驗證後，JavaScript 可以監聽它。
- **使用時機：**要在送出前阻止頁面導覽、檢查資料或開始非同步工作時。
- **最小範例：**`form.addEventListener("submit", handleSubmit)`。
- **觀察證據：**有效送出時 handler 被呼叫，網址仍留在目前頁面，status 開始更新。
- **容易混淆：**和 button 的 click 混淆；submit 是整份表單的流程，也能由鍵盤 Enter 觸發。

### 事件物件（event object）

- **白話：**瀏覽器交給 handler 的這次事件資料。
- **正式定義：**事件處理函式收到的物件，包含目前事件與可呼叫的控制方法，例如 preventDefault。
- **使用時機：**需要取消預設行為，或知道是哪個事件被觸發時。
- **最小範例：**`form.addEventListener("submit", (event) => { ... })`。
- **觀察證據：**在 submit callback 內使用 event，且 Console 沒有未宣告變數錯誤。
- **容易混淆：**和事件名稱混淆；submit 是事件類型，event 是這一次發生的物件。

### preventDefault（Event.preventDefault()）

- **白話：**阻止瀏覽器原本準備做的動作。
- **正式定義：**取消事件目標的預設行為；在表單 submit handler 內可避免頁面依 action 重新導覽。
- **使用時機：**想由 JavaScript 接手表單流程，保留目前畫面與輸入內容時。
- **最小範例：**`event.preventDefault();`。
- **觀察證據：**有效送出後網址不變，status 能留在目前頁面並顯示 loading。
- **容易混淆：**和 stopPropagation 混淆；preventDefault 取消預設行為，stopPropagation 控制事件傳播。

### Promise（Promise）

- **白話：**代表一件稍後才會完成或失敗的工作。
- **正式定義：**表示非同步工作的未來結果，狀態會從 pending 轉成 fulfilled 或 rejected。
- **使用時機：**mock 送出、fetch 或其他需要等待回應的工作時。
- **最小範例：**`return new Promise((resolve) => window.setTimeout(resolve, 300));`。
- **觀察證據：**先看到 loading，等待後才看到 success 或 error，而不是同步立刻完成。
- **容易混淆：**和 setTimeout 混淆；setTimeout 只是排程時間，Promise 才表示這項工作的結果。

### async 函式（async function）

- **白話：**讓函式可以用 await 等待 Promise。
- **正式定義：**被 async 宣告的函式一定回傳 Promise，函式內可以使用 await 等待非同步結果。
- **使用時機：**submit handler 需要依序等待 mock 或 API 結果時。
- **最小範例：**`async function handleSubmit(event) { ... }`。
- **觀察證據：**呼叫後可以等待結果，錯誤能在 try/catch 中被處理。
- **容易混淆：**和同步 function 混淆；async 不會讓等待消失，只是提供可讀的等待寫法。

### await（await expression）

- **白話：**在 async 函式中等待一個 Promise 的結果。
- **正式定義：**暫停目前 async 函式後續程式，直到 Promise fulfilled 或 rejected；不會阻塞整個瀏覽器。
- **使用時機：**success 或 error 必須等 mock／請求完成後才能決定時。
- **最小範例：**`await mockSubmit();`。
- **觀察證據：**loading 先出現，Promise 完成後才進入 success 或 catch 的 error。
- **容易混淆：**和 setTimeout 混淆；await 等待 Promise，不能獨立放在普通函式裡。

### try/catch/finally（try/catch/finally）

- **白話：**把成功、失敗與一定要收尾的工作分開。
- **正式定義：**try 執行可能失敗的程式，catch 處理 rejected／例外，finally 無論結果如何都會執行收尾。
- **使用時機：**loading 結束後一定要恢復按鈕，成功與失敗都要顯示狀態時。
- **最小範例：**`try { await mockSubmit(); } catch { ... } finally { setLoading(false); }`。
- **觀察證據：**成功與失敗都會從 disabled 回到可操作，輸入內容仍保留。
- **容易混淆：**和只寫 then 混淆；finally 是清理共同狀態，不是成功專用分支。

### 狀態（UI state）

- **白話：**描述表單目前正在什麼階段。
- **正式定義：**用資料表示畫面流程目前屬於 idle、loading、success 或 error 等互斥狀態。
- **使用時機：**需要讓使用者知道尚未送出、等待中、成功或失敗時。
- **最小範例：**`status.dataset.state = "loading";`。
- **觀察證據：**Elements 的 data-state、status 文字與按鈕 disabled 會依流程改變。
- **容易混淆：**和單純顏色混淆；顏色是呈現，state 是程式與畫面共同遵守的資料。

### disabled（disabled attribute）

- **白話：**暫時不讓控制項被操作。
- **正式定義：**HTML 控制項的布林屬性；存在時，使用者不能操作該控制項，瀏覽器也會呈現停用狀態。
- **使用時機：**非同步送出尚未完成，避免重複送出或同時觸發兩條流程時。
- **最小範例：**`submitButton.disabled = true;`。
- **觀察證據：**loading 期間按鈕不能再次觸發，finally 後回到可操作。
- **容易混淆：**和 CSS opacity 混淆；opacity 只改外觀，disabled 才真正改變控制項行為。

### aria-live（aria-live attribute）

- **白話：**告訴輔助技術這段文字更新時要被注意。
- **正式定義：**ARIA live region 屬性，讓動態更新的內容能被輔助技術依 politeness 等級播報。
- **使用時機：**loading、success、error 等狀態文字會在不重新整理頁面時改變時。
- **最小範例：**`<p id="status" role="status" aria-live="polite">idle</p>`。
- **觀察證據：**Elements 看到 status 的語意屬性，狀態更新時仍保留同一個 DOM 位置。
- **容易混淆：**和 alert 混淆；polite 狀態通常不會搶斷使用者目前正在聽或做的事情。

## 5. Live Lab

### 最小可執行實驗

- **入口：**`examples/minimal.html`
- **操作：**開啟 examples/minimal.html，先空白按「模擬成功送出」，再填寫三個欄位測試成功，最後按「模擬失敗」。觀察網址、status、disabled 與輸入內容。
- **預期結果：**空白送出先出現瀏覽器原生驗證；有效送出依序看到 loading、success；模擬失敗看到 error，兩種結果都會在 finally 後恢復按鈕且保留輸入。
- **重設方式：**重新整理 examples/minimal.html 回到 idle；這份示範只使用本機固定 mock，不連線也不修改正式資料。
- **第一個檢查位置：**先看 Console 第一個紅色錯誤，再用 Elements 檢查 #async-form、#status 的 data-state、aria-busy 與兩個按鈕的 disabled。

### Step 1：先建立能被瀏覽器理解的表單

- **教學目的：**先把輸入欄位、label 關係、required 與 status 位置建立好，讓後面的 JavaScript 有明確目標。
- **講師白話解釋：**先請學生說出預測，再說明這一步只處理一個責任。
- **精確檔案與區塊：**starter/files/contact.html 的 #contact-form
- **修改前：**表單欄位與狀態區仍有 TODO，瀏覽器還不能完整驗證這組輸入。
- **本次只修改：**建立姓名、Email、需求三個欄位，讓每個 label 對應 id，並加入 required、submit button 與 role=status。
- **程式碼或操作：**

  ```text
  <form id="contact-form" action="contact.html" method="get">
    <label for="name">姓名</label>
    <input id="name" name="name" required>
    <label for="email">Email</label>
    <input id="email" name="email" type="email" required>
    <label for="message">需求</label>
    <textarea id="message" name="message" required></textarea>
    <button id="submit-button" data-submit-control type="submit">送出</button>
    <p id="status" role="status" aria-live="polite">idle：尚未送出</p>
  </form>
  ```
- **預期結果：**空白按送出時出現瀏覽器原生驗證；點擊 label 會把焦點移到對應欄位。
- **學生可能的錯誤答案：**只放 placeholder，或把 label 的 for 寫成 name 而不是 input 的 id。
- **講師追問：**required 和 placeholder 的責任一樣嗎？如果學生按 Enter，哪個元素代表整份表單的送出？
- **Elements／Console／網址列／status／disabled／aria-busy 驗收：**Elements 的 form／label／input 巢狀關係、for/id 對應、原生驗證提示與 #status。
- **卡住時的單一檢查：**只搜尋 contact.html 的 #contact-form、required、for、id、type=submit 與 #status。

### Step 2：找到 submit 事件與事件物件

- **教學目的：**把表單送出和 JavaScript handler 接起來，先理解事件何時發生、函式收到什麼。
- **講師白話解釋：**先請學生說出預測，再說明這一步只處理一個責任。
- **精確檔案與區塊：**starter/files/main.js 的 form submit handler
- **修改前：**表單仍由瀏覽器直接依 action 送出，JavaScript 尚未接手。
- **本次只修改：**用 querySelector 找到 form，使用 addEventListener 監聽 submit，並讓 handler 收到 event。
- **程式碼或操作：**

  ```text
  const form = document.querySelector("#contact-form");
  form.addEventListener("submit", (event) => {
    // 下一步會在這裡處理預設行為
  });
  ```
- **預期結果：**有效送出時 callback 被呼叫；尚未加入 preventDefault 前，仍能觀察瀏覽器原本的導覽行為。
- **學生可能的錯誤答案：**只監聽送出按鈕 click，或把 handleSubmit() 直接呼叫後交給 addEventListener。
- **講師追問：**為什麼鍵盤 Enter 也應該能走 submit？event 是事件名稱，還是這一次事件的資料？
- **Elements／Console／網址列／status／disabled／aria-busy 驗收：**Console／網址列與 submit callback 的執行位置；沒有把事件物件寫在 callback 外。
- **卡住時的單一檢查：**確認 selector 是 #contact-form，第二個參數是函式，且 event 只出現在 callback 參數範圍內。

### Step 3：用 preventDefault 保留目前頁面

- **教學目的：**取消表單預設導覽，讓狀態文字、輸入內容與後續非同步流程留在目前頁面。
- **講師白話解釋：**先請學生說出預測，再說明這一步只處理一個責任。
- **精確檔案與區塊：**checkpoint/files/main.js 的 handleSubmit
- **修改前：**submit handler 已存在，但有效送出仍會沿用 form 的 action／method。
- **本次只修改：**在 submit callback 收到的 event 上呼叫 preventDefault。
- **程式碼或操作：**

  ```text
  form.addEventListener("submit", (event) => {
    event.preventDefault();
    runSubmission(false);
  });
  ```
- **預期結果：**有效送出後網址不變，頁面可以繼續顯示 loading 與結果。
- **學生可能的錯誤答案：**把 event.preventDefault() 寫在 callback 外，或只阻止按鈕 click 而沒有阻止 form submit。
- **講師追問：**preventDefault 取消的是哪個預設行為？它和把 button 改成 type=button 是同一種做法嗎？
- **Elements／Console／網址列／status／disabled／aria-busy 驗收：**網址列沒有新增 query string，submit handler 仍能更新 #status。
- **卡住時的單一檢查：**只看 handleSubmit 的大括號內，確認 event.preventDefault() 位於 callback 的第一個有效步驟。

### Step 4：先讓等待中的 loading 可被看見

- **教學目的：**非同步工作尚未完成時，使用者仍需要知道程式正在處理，並且不能重複觸發送出。
- **講師白話解釋：**先請學生說出預測，再說明這一步只處理一個責任。
- **精確檔案與區塊：**starter/files/main.js 的 status 與按鈕控制
- **修改前：**送出後沒有 loading，按鈕也仍然可以連續觸發。
- **本次只修改：**建立 setLoading 與狀態更新，讓 loading 時 submit／failure 控制項 disabled。
- **程式碼或操作：**

  ```text
  function setLoading(isLoading) {
    controls.forEach((control) => {
      control.disabled = isLoading;
    });
    form.setAttribute("aria-busy", String(isLoading));
  }
  
  setStatus("正在等待 mock 結果", "loading");
  setLoading(true);
  ```
- **預期結果：**按下送出後先看到 loading，兩個按鈕暫時不能操作，form 的 aria-busy 是 true。
- **學生可能的錯誤答案：**只把按鈕變淡，或在 Promise 完成前又把 disabled 改回 false。
- **講師追問：**disabled 是外觀還是行為？如果沒有 finally，哪一條路徑可能讓按鈕永遠不能按？
- **Elements／Console／網址列／status／disabled／aria-busy 驗收：**status 的 data-state、form 的 aria-busy 與按鈕的 disabled 屬性。
- **卡住時的單一檢查：**只檢查 loading 設定是否早於 mockSubmit，以及 setLoading(false) 是否留在結果收尾位置。

### Step 5：用 Promise 與 async/await 等待 mock 結果

- **教學目的：**把等待工作、成功分支與錯誤分支接成一條可讀的非同步流程。
- **講師白話解釋：**先請學生說出預測，再說明這一步只處理一個責任。
- **精確檔案與區塊：**starter/files/main.js 的 mockSubmit 與 runSubmission
- **修改前：**只有 loading，還沒有代表稍後結果的 Promise，也沒有 catch/finally。
- **本次只修改：**用 setTimeout 建立本地 mock Promise，在 async 函式中 await，並以 try/catch/finally 分開 success、error 與收尾。
- **程式碼或操作：**

  ```text
  function mockSubmit(shouldFail) {
    return new Promise((resolve, reject) => {
      window.setTimeout(() => shouldFail ? reject(new Error("mock")) : resolve(), 350);
    });
  }
  
  async function runSubmission(shouldFail) {
    try {
      await mockSubmit(shouldFail);
      setStatus("測試詢問已收到", "success");
    } catch {
      setStatus("模擬失敗，請重試", "error");
    } finally {
      setLoading(false);
    }
  }
  ```
- **預期結果：**先看到 loading，成功進入 success，失敗進入 error；兩條路徑最後都恢復按鈕。
- **學生可能的錯誤答案：**把 success 放在 await 前，或只寫 catch 沒有 finally，導致錯誤後按鈕無法恢復。
- **講師追問：**Promise pending 時畫面應該顯示什麼？try、catch、finally 哪一段不論成功失敗都要執行？
- **Elements／Console／網址列／status／disabled／aria-busy 驗收：**status 文字時間順序、Console 無錯誤、success/error 的 data-state 與 finally 後的 disabled。
- **卡住時的單一檢查：**先只看 mockSubmit 是否真的 return Promise，再看 await 是否位於 async 函式內。

### Step 6：用失敗與重試驗收整段生命週期

- **教學目的：**確認錯誤不是只顯示一行紅字，而是能恢復操作、保留輸入並讓學生說出證據。
- **講師白話解釋：**先請學生說出預測，再說明這一步只處理一個責任。
- **精確檔案與區塊：**examples/minimal.html、solution/files/contact.html、solution/files/main.js
- **修改前：**成功路徑完成後，還要用模擬失敗與窄版／鍵盤重新驗收。
- **本次只修改：**先填寫欄位，分別測試成功與失敗，觀察 status、disabled、aria-live、aria-busy、網址與輸入內容。
- **程式碼或操作：**

  ```text
  填寫姓名／Email／需求 → 模擬失敗 → 確認 error → 再次送出 → 確認 success
  ```
- **預期結果：**失敗後輸入仍在、按鈕恢復可操作、重試可成功；390px 與 Tab 操作仍能看見焦點和狀態。
- **學生可能的錯誤答案：**看到 error 就重新整理，或只看顏色沒有檢查 data-state、disabled 與輸入內容。
- **講師追問：**如果使用者輸入的需求在 error 後消失，這是 UX 還是資料流程問題？你要在哪裡留下證據？
- **Elements／Console／網址列／status／disabled／aria-busy 驗收：**網址、Elements 的 status／aria-live／data-state、按鈕 disabled、欄位 value、390px 與鍵盤 focus。
- **卡住時的單一檢查：**先重現一條路徑：填寫一筆固定資料，只觀察 status 與按鈕，不同時修改多個檔案。

### Live Lab 與階段任務對照

- Step 1 對應 **starter-1**：contact.html｜contact-form fields and status；學生停點：contact.html 有一個表單、三個帶 label 的 required 欄位、submit button 與 #status；空白送出會先出現原生驗證提示。；證據：Elements 的 form／label／input／textarea 巢狀關係、for 與 id 對照、#status 的 role 與空白送出驗證提示。
- Step 2 對應 **starter-2**：main.js｜submit callback；學生停點：submit callback 會被觸發並開始本地 mock 的 loading 流程；Checkpoint 前仍會沿用瀏覽器預設送出。；證據：填妥欄位後送出，觀察網址導覽與程式碼中的 submit callback；不要把表單欄位內容寫入 Console。
- Step 3 對應 **checkpoint-1**：main.js｜submit callback；學生停點：送出後網址不變，status 能依序顯示 loading 與 mock success/error，輸入內容仍保留。；證據：Network 沒有新的 document 導覽、status 與按鈕 disabled 的時間順序可觀察。
- Step 4 對應 **starter-3**：main.js｜async lifecycle；學生停點：送出後 status 先顯示 loading 並停用控制項；本地 mock Promise resolve 時顯示 success，reject 時顯示 error，finally 讓控制項恢復且不清空輸入。；證據：status 的時間順序、控制項 disabled 前後狀態、data-state、aria-busy 與輸入欄位在結果後仍保留。
- Step 5 對應 **starter-3**：main.js｜async lifecycle；學生停點：送出後 status 先顯示 loading 並停用控制項；本地 mock Promise resolve 時顯示 success，reject 時顯示 error，finally 讓控制項恢復且不清空輸入。；證據：status 的時間順序、控制項 disabled 前後狀態、data-state、aria-busy 與輸入欄位在結果後仍保留。
- Step 6 對應 **final-acceptance**：examples/minimal.html、solution/files/contact.html、solution/files/main.js｜先填寫欄位，分別測試成功與失敗，觀察 status、disabled、aria-live、aria-busy、網址與輸入內容。；學生停點：失敗後輸入仍在、按鈕恢復可操作、重試可成功；390px 與 Tab 操作仍能看見焦點和狀態。；證據：網址、Elements 的 status／aria-live／data-state、按鈕 disabled、欄位 value、390px 與鍵盤 focus。

## 6. 觀察與除錯

教師先示範故意錯誤，再要求學生依序寫下「症狀 → 證據 → 假設 → 單一修正 → 重新驗證」。本堂常見錯誤是：故意把 preventDefault 放到 submit callback 外，或讓 catch/finally 沒有恢復按鈕，從 Console、網址、status 文字與 disabled 屬性找出原因。

## 7. 學生任務

### 教師完整示範

示範 先開啟本地 mock 表單，示範空白驗證，再用 submit handler 保留頁面，最後觀察 idle → loading → success/error 的狀態轉換。；每次只修改一個檔案或區塊，先讓學生預測，再立即回到畫面、Elements、Console、網址列、status、disabled 與 aria-busy。

### 學生跟做

把教師示範拆成三到四個 10–15 分鐘微任務：先完成入口，再完成核心修改，接著重現錯誤，最後留下驗收證據。學生必須在自己的資料夾重現，不以講師畫面成功為完成。

### 情境變體

把姓名欄位改成預約需求表單，加入模擬失敗與重試；保留同一組狀態流程且不清空使用者輸入。

### 獨立挑戰

不看 Solution，將同一套 mock 狀態流程改成預約表單；加入一個清楚的失敗／重試入口，保留使用者輸入、鍵盤 focus、aria-live 與 loading 時 disabled。

### 驗收證據

空白欄位的原生驗證、網址不變、idle→loading→success/error、loading 時 disabled、錯誤後可重試、輸入內容保留。學生需提交修改檔案清單、操作步驟、預期結果、實際觀察與一項可重現證據。

### 離場前回顧

請學生回答：我理解了什麼？我做出了什麼？我用什麼證據確認？下一堂會接著處理什麼？

下堂銜接：Vue CDN：下一堂會把同樣的狀態與畫面更新交給 Vue 管理。

### 官方來源補充

- [MDN：Client-side form validation](https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms/Form_validation)
- [MDN：Event.preventDefault()](https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault)
- [MDN：async function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function)
- [MDN：Promises](https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Async_JS/Promises)
- [MDN：aria-live](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-live)
