LESSON-18 · 零基礎學生教材
JavaScript 基礎:表單驗證與非同步概念
完成一個不重新導覽頁面的聯絡表單:空白欄位由瀏覽器驗證,送出期間顯示 loading 並停用按鈕,最後顯示 success 或 error,且保留輸入內容。
先理解:這堂課解決什麼問題?
聯絡表單不能直接刷新頁面消失,訪客需要知道送出中、成功或失敗的結果。
本堂要完成:完成一個不重新導覽頁面的聯絡表單:空白欄位由瀏覽器驗證,送出期間顯示 loading 並停用按鈕,最後顯示 success 或 error,且保留輸入內容。
學習邊界:先完成「掌握表單驗證、preventDefault 與非同步狀態流程」,不提前引入尚未教過的框架或建置工具。
第一次上課環境:先建立表單與四個觀察位置
- 編輯器:只修改目前階段
files/index.html、files/contact.html、files/style.css與files/main.js;不要新增 Vue、npm 或真正 API。 - 瀏覽器:先開啟
examples/minimal.html,再開啟階段入口與files/contact.html;每次儲存後重新整理。 - DevTools:Elements 看 form、label、required、status、data-state、aria-live、aria-busy 與 disabled;Console 看第一個錯誤;網址列確認 preventDefault 是否保留目前頁面。
- 操作習慣:先空白送出,再填寫固定測試資料,分別驗收 loading、success、error 與重試;每次只改一個檔案區塊。
先做最小實驗:先看見結果
這不是期末網站,而是一個可以直接看到本堂核心結果的最小實驗。先完成一次操作,再回到檔案和名詞卡對照。
- 入口:開啟
examples/minimal.html。 - 操作:開啟
examples/minimal.html,先空白按「模擬成功送出」,再填寫三個欄位測試成功,最後按「模擬失敗」。觀察網址、status、disabled 與輸入內容。 - 預期:空白送出先出現瀏覽器原生驗證;有效送出依序看到 loading、success;模擬失敗看到 error,兩種結果都會在 finally 後恢復按鈕且保留輸入。
- 重設:重新整理
examples/minimal.html回到 idle;這份示範只使用本機固定 mock,不連線也不修改正式資料。
學生練習流程
依序完成每一張卡;目前只聚焦一個步驟,先操作,再用可觀察證據確認結果。
-
1
最小實驗
先閱讀本頁的問題、檔案地圖、名詞卡與最小例子。 -
2
Step 2
自己回答互動問答 4 題,再展開解析。 -
3
最小實驗 2
開啟最小實驗,實際操作一次並記下證據。 -
4
Starter
把 Starter 複製到自己的工作資料夾,照 3 張微步驟卡逐一完成;每一步都要看到結果再繼續。 -
5
Checkpoint
前往 Checkpoint,只處理 1 個本堂核心缺口;先預測,再用指定證據驗收。 -
6
Solution
前往 Solution,先看逐檔差異與修改原因,再決定要不要把完整內容帶回自己的檔案。 -
7
變體驗收
完成情境變體與獨立挑戰,留下檔案位置、操作、結果與驗收證據,並閱讀下一堂銜接:Vue CDN:下一堂會把同樣的狀態與畫面更新交給 Vue 管理。
檔案地圖:每個位置負責什麼?
先不要急著背檔名;請把檔案位置和責任連起來。你在階段頁會看到同一份檔案的可執行版本。
examples/minimal.html只讀最小實驗:先觀察空白驗證、成功、失敗與按鈕恢復。starter/files/index.htmlStarter 入口:說明本階段並連到contact.html。starter/files/contact.htmlStarter 表單:完成欄位、required、submit 與 status 的第一個 TODO。starter/files/main.jsStarter 腳本:先留下 submit handler 與非同步生命週期的 TODO。checkpoint/files/main.jsCheckpoint 腳本:完整流程只缺 submit callback 內的preventDefault。solution/files/main.jsSolution 腳本:完成 mock Promise、try/catch/finally、disabled 與可恢復狀態。
先學會這些詞
先從名詞、英文與白話意思開始;每次只展開一張名詞卡,再對照正式定義、範例與驗收證據。專業術語、語法、檔案路徑會使用不同樣式。第一次遇到術語時,先回到這裡,不要靠猜。
表單與事件
6 個詞表單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 的關聯。
requiredrequired 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 是這一次發生的物件。
preventDefaultEvent.preventDefault() 白話意思:阻止瀏覽器原本準備做的動作。 展開查看正式定義、範例與驗收證據
- 正式定義
- 取消事件目標的預設行為;在表單 submit handler 內可避免頁面依 action 重新導覽。
- 什麼時候用
- 想由 JavaScript 接手表單流程,保留目前畫面與輸入內容時。
- 最小範例
- event.preventDefault();。
- 可觀察證據
- 有效送出後網址不變,status 能留在目前頁面並顯示 loading。
- 容易混淆
- 和 stopPropagation 混淆;
preventDefault取消預設行為,stopPropagation 控制事件傳播。
非同步流程
4 個詞PromisePromise 白話意思:代表一件稍後才會完成或失敗的工作。 展開查看正式定義、範例與驗收證據
- 正式定義
- 表示非同步工作的未來結果,狀態會從 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 不會讓等待消失,只是提供可讀的等待寫法。
awaitawait expression 白話意思:在 async 函式中等待一個 Promise 的結果。 展開查看正式定義、範例與驗收證據
- 正式定義
- 暫停目前 async 函式後續程式,直到 Promise fulfilled 或 rejected;不會阻塞整個瀏覽器。
- 什麼時候用
- success 或 error 必須等 mock/請求完成後才能決定時。
- 最小範例
- await mockSubmit();。
- 可觀察證據
- loading 先出現,Promise 完成後才進入 success 或 catch 的 error。
- 容易混淆
- 和 setTimeout 混淆;await 等待 Promise,不能獨立放在普通函式裡。
try/catch/finallytry/catch/finally 白話意思:把成功、失敗與一定要收尾的工作分開。 展開查看正式定義、範例與驗收證據
- 正式定義
- try 執行可能失敗的程式,catch 處理 rejected/例外,finally 無論結果如何都會執行收尾。
- 什麼時候用
- loading 結束後一定要恢復按鈕,成功與失敗都要顯示狀態時。
- 最小範例
- try { await mockSubmit(); } catch { ... } finally { setLoading(false); }。
- 可觀察證據
- 成功與失敗都會從 disabled 回到可操作,輸入內容仍保留。
- 容易混淆
- 和只寫 then 混淆;finally 是清理共同狀態,不是成功專用分支。
狀態與回饋
3 個詞狀態UI state 白話意思:描述表單目前正在什麼階段。 展開查看正式定義、範例與驗收證據
- 正式定義
- 用資料表示畫面流程目前屬於 idle、loading、success 或 error 等互斥狀態。
- 什麼時候用
- 需要讓使用者知道尚未送出、等待中、成功或失敗時。
- 最小範例
- status.dataset.state = "loading";。
- 可觀察證據
- Elements 的 data-state、status 文字與按鈕 disabled 會依流程改變。
- 容易混淆
- 和單純顏色混淆;顏色是呈現,state 是程式與畫面共同遵守的資料。
disableddisabled attribute 白話意思:暫時不讓控制項被操作。 展開查看正式定義、範例與驗收證據
- 正式定義
- HTML 控制項的布林屬性;存在時,使用者不能操作該控制項,瀏覽器也會呈現停用狀態。
- 什麼時候用
- 非同步送出尚未完成,避免重複送出或同時觸發兩條流程時。
- 最小範例
- submitButton.disabled = true;。
- 可觀察證據
- loading 期間按鈕不能再次觸發,finally 後回到可操作。
- 容易混淆
- 和 CSS opacity 混淆;opacity 只改外觀,disabled 才真正改變控制項行為。
aria-livearia-live attribute 白話意思:告訴輔助技術這段文字更新時要被注意。 展開查看正式定義、範例與驗收證據
- 正式定義
- ARIA live region 屬性,讓動態更新的內容能被輔助技術依 politeness 等級播報。
- 什麼時候用
- loading、success、error 等狀態文字會在不重新整理頁面時改變時。
- 最小範例
- <p id="status" role="status" aria-live="polite">idle</p>。
- 可觀察證據
- Elements 看到 status 的語意屬性,狀態更新時仍保留同一個 DOM 位置。
- 容易混淆
- 和 alert 混淆;polite 狀態通常不會搶斷使用者目前正在聽或做的事情。
最小例子:先看懂,再複製
這段只保留本堂第一個微成果。先預測結果,再逐行讀;完整檔案請開啟下方最小實驗。
<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>Live Lab:逐段讀碼與驗證
每一步只回答五件事:為什麼做、改哪裡、會看到什麼、用什麼證據確認、卡住先查什麼。
-
Step 1
先建立能被瀏覽器理解的表單
- 目的
- 先把輸入欄位、label 關係、required 與 status 位置建立好,讓後面的 JavaScript 有明確目標。
- 修改位置
starter/files/contact.html的 #contact-form
建立姓名、Email、需求三個欄位,讓每個 label 對應 id,並加入 required、submit button 與 role=status。- 預期結果
- 空白按送出時出現瀏覽器原生驗證;點擊 label 會把焦點移到對應欄位。
- 驗收證據
- 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
用querySelector找到 form,使用addEventListener監聽 submit,並讓 handler 收到 event。- 預期結果
- 有效送出時 callback 被呼叫;尚未加入
preventDefault前,仍能觀察瀏覽器原本的導覽行為。 - 驗收證據
- Console/網址列與 submit callback 的執行位置;沒有把事件物件寫在 callback 外。
- 卡住先查
- 確認 selector 是 #contact-form,第二個參數是函式,且 event 只出現在 callback 參數範圍內。
-
Step 3
用
preventDefault保留目前頁面- 目的
- 取消表單預設導覽,讓狀態文字、輸入內容與後續非同步流程留在目前頁面。
- 修改位置
checkpoint/files/main.js的 handleSubmit
在 submit callback 收到的 event 上呼叫preventDefault。- 預期結果
- 有效送出後網址不變,頁面可以繼續顯示 loading 與結果。
- 驗收證據
- 網址列沒有新增 query string,submit handler 仍能更新 #status。
- 卡住先查
- 只看 handleSubmit 的大括號內,確認 event.
preventDefault() 位於 callback 的第一個有效步驟。
-
Step 4
先讓等待中的 loading 可被看見
- 目的
- 非同步工作尚未完成時,使用者仍需要知道程式正在處理,並且不能重複觸發送出。
- 修改位置
starter/files/main.js的 status 與按鈕控制
建立 setLoading 與狀態更新,讓 loading 時 submit/failure 控制項 disabled。- 預期結果
- 按下送出後先看到 loading,兩個按鈕暫時不能操作,form 的 aria-busy 是 true。
- 驗收證據
- status 的 data-state、form 的 aria-busy 與按鈕的 disabled 屬性。
- 卡住先查
- 只檢查 loading 設定是否早於 mockSubmit,以及 setLoading(false) 是否留在結果收尾位置。
-
Step 5
用 Promise 與
async/await等待 mock 結果- 目的
- 把等待工作、成功分支與錯誤分支接成一條可讀的非同步流程。
- 修改位置
starter/files/main.js的 mockSubmit 與 runSubmission
用 setTimeout 建立本地 mock Promise,在 async 函式中 await,並以try/catch/finally分開 success、error 與收尾。- 預期結果
- 先看到 loading,成功進入 success,失敗進入 error;兩條路徑最後都恢復按鈕。
- 驗收證據
- status 文字時間順序、Console 無錯誤、
success/error的 data-state 與 finally 後的 disabled。 - 卡住先查
- 先只看 mockSubmit 是否真的
returnPromise,再看 await 是否位於 async 函式內。
-
Step 6
用失敗與重試驗收整段生命週期
- 目的
- 確認錯誤不是只顯示一行紅字,而是能恢復操作、保留輸入並讓學生說出證據。
- 修改位置
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。
- 卡住先查
- 先重現一條路徑:填寫一筆固定資料,只觀察 status 與按鈕,不同時修改多個檔案。
讀碼順序:先找入口,再用畫面、Elements、Console、網址列、status 的 data-state、按鈕 disabled 與 aria-busy 交叉驗證;本堂核心是表單事件如何進入可恢復的非同步狀態流程。
互動問答
先回答目前這一題,再展開解析;完成後按下一題,讓每一次回答都回到檔案或瀏覽器完成驗證。
Q1概念理解 「表單(form)」在本堂要解決什麼問題? 本題線索:表單;回到名詞卡與 Elements 看到欄位都位於同一個 form 內,空白送出會出現瀏覽器驗證提示。 展開解析收合解析
白話解析:
收集使用者輸入,並把一組欄位視為同一次送出的 HTML 區塊。 正式來說,HTML 的 form 元素,包含控制項與 submit 行為;瀏覽器會依欄位規則進行原生驗證。
對照位置:
回到名詞卡「表單」,再看 Elements 看到欄位都位於同一個 form 內,空白送出會出現瀏覽器驗證提示。
預期觀察:
你應該能用自己的話說出:需要收集姓名、Email、需求或預約資料時。
常見錯誤:
不要只回答「它是表單」;那是重複名詞,不是說明責任。
立即驗證:
開啟 examples/minimal.html,依序做「開啟 examples/minimal.html,先空白按「模擬成功送出」,再填寫三個欄位測試成功,最後按「模擬失敗」。觀察網址、status、disabled 與輸入內容。」並記下畫面或工具證據。
Q2程式碼閱讀
修改指定規則後,你預測畫面會看到什麼?
本題線索:starter/files/contact.html 的 #contact-form;要修改:建立姓名、Email、需求三個欄位,讓每個 label 對應 id,並加入 required、submit button 與 role=status。;觀察:Elements 的 form/label/input 巢狀關係、for/id 對應、原生驗證提示與 #status。
展開解析收合解析
白話解析:
先看檔案責任,再預測結果。這一步的目的:先把輸入欄位、label 關係、required 與 status 位置建立好,讓後面的 JavaScript 有明確目標。
對照位置:
對照 Live Lab Step 1,位置是 starter/files/contact.html 的 #contact-form;範例內容:<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 會把焦點移到對應欄位。
常見錯誤:
如果只改了檔案但沒有結果,先不要重寫全部;只搜尋 contact.html 的 #contact-form、required、for、id、type=submit 與 #status。
立即驗證:
實際操作後檢查 Elements 的 form/label/input 巢狀關係、for/id 對應、原生驗證提示與 #status。。
Q3概念比較
「表單」和「label 對應」在本堂的責任有什麼不同?
本題線索:比較 表單 與 label 對應;操作位置:starter/files/main.js 的 form submit handler
展開解析收合解析
白話解析:
表單:收集使用者輸入,並把一組欄位視為同一次送出的 HTML 區塊。;label 對應:欄位名稱和輸入框要互相連得起來。
對照位置:
對照兩張名詞卡的正式定義:HTML 的 form 元素,包含控制項與 submit 行為;瀏覽器會依欄位規則進行原生驗證。/以 label 的 for 屬性對應控制項的 id,建立可點擊、可讀取的欄位關聯。
預期觀察:
你應該能指出兩者分別出現在哪個檔案或工具,以及哪一個結果會改變。
常見錯誤:
不要把「改變畫面」當成所有技術的責任;先說清楚誰負責結構、呈現、行為或驗收。
立即驗證:
在 starter/files/main.js 的 form submit handler 做「用 querySelector 找到 form,使用 addEventListener 監聽 submit,並讓 handler 收到 event。」,再用 Console/網址列與 submit callback 的執行位置;沒有把事件物件寫在 callback 外。 比較前後差異。
Q4除錯驗收
如果完成操作後結果不對,你會先從哪一個證據開始查?
本題線索:examples/minimal.html、solution/files/contact.html、solution/files/main.js;第一個檢查位置:網址、Elements 的 status/aria-live/data-state、按鈕 disabled、欄位 value、390px 與鍵盤 focus。
展開解析收合解析
白話解析:
先描述症狀,再提出一個最小假設。本堂常見錯誤是:看到 error 就重新整理,或只看顏色沒有檢查 data-state、disabled 與輸入內容。
對照位置:
對照最後一段 Live Lab:examples/minimal.html、solution/files/contact.html、solution/files/main.js;檢查方式:先重現一條路徑:填寫一筆固定資料,只觀察 status 與按鈕,不同時修改多個檔案。
預期觀察:
你要能指出一個具體位置,而不是一次修改很多檔案。
常見錯誤:
不要先清快取、重裝工具或複製 Solution;先查看第一個可觀察錯誤。
立即驗證:
重新操作並留下「症狀、證據、假設、單一修正、結果」五項紀錄。
常見誤解
- 看到畫面沒有變化,不代表程式沒執行;先確認檔案、路徑、元素與狀態證據。
- 能複製 Solution 不代表理解;請先說出修改哪個檔案、預期哪個結果、用什麼證據確認。
- 本堂只使用原生 HTML、CSS、JavaScript、瀏覽器 DevTools 與本地 mock Promise;不加入 Vue、fetch、API、伺服器驗證或建置工具。
本堂任務與驗收
完成一個不重新導覽頁面的聯絡表單:空白欄位由瀏覽器驗證,送出期間顯示 loading 並停用按鈕,最後顯示 success 或 error,且保留輸入內容。
- 能用 空白欄位的原生驗證 留下可重現證據。
- 能用 網址不變 留下可重現證據。
- 能用 idle→loading→
success/error留下可重現證據。 - 能用 loading 時 disabled 留下可重現證據。
- 能用 錯誤後可重試 留下可重現證據。
- 能用 輸入內容保留 留下可重現證據。
- Starter、Checkpoint、Solution 的入口都能開啟並知道三者差異。
- 遇到問題時能先寫出症狀,再檢查一個最小假設。
官方延伸閱讀
下載講師版 Markdown:需要授課或離線查閱時使用,內容包含完整教學節奏。