商用網站架設實務

LESSON-18 · 零基礎學生教材

JavaScript 基礎:表單驗證與非同步概念

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

先理解:這堂課解決什麼問題?

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

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

學習邊界:先完成「掌握表單驗證、preventDefault 與非同步狀態流程」,不提前引入尚未教過的框架或建置工具。

第一次上課環境:先建立表單與四個觀察位置

  1. 編輯器:只修改目前階段 files/index.htmlfiles/contact.htmlfiles/style.cssfiles/main.js;不要新增 Vue、npm 或真正 API。
  2. 瀏覽器:先開啟 examples/minimal.html,再開啟階段入口與 files/contact.html;每次儲存後重新整理。
  3. DevTools:Elements 看 form、label、required、status、data-state、aria-live、aria-busy 與 disabled;Console 看第一個錯誤;網址列確認 preventDefault 是否保留目前頁面。
  4. 操作習慣:先空白送出,再填寫固定測試資料,分別驗收 loading、success、error 與重試;每次只改一個檔案區塊。

先做最小實驗:先看見結果

這不是期末網站,而是一個可以直接看到本堂核心結果的最小實驗。先完成一次操作,再回到檔案和名詞卡對照。

開啟本堂最小實驗

  1. 入口:開啟 examples/minimal.html
  2. 操作:開啟 examples/minimal.html,先空白按「模擬成功送出」,再填寫三個欄位測試成功,最後按「模擬失敗」。觀察網址、status、disabled 與輸入內容。
  3. 預期:空白送出先出現瀏覽器原生驗證;有效送出依序看到 loading、success;模擬失敗看到 error,兩種結果都會在 finally 後恢復按鈕且保留輸入。
  4. 重設:重新整理 examples/minimal.html 回到 idle;這份示範只使用本機固定 mock,不連線也不修改正式資料。

學生練習流程

依序完成每一張卡;目前只聚焦一個步驟,先操作,再用可觀察證據確認結果。

  1. 1

    最小實驗

    先閱讀本頁的問題、檔案地圖、名詞卡與最小例子。
  2. 2

    Step 2

    自己回答互動問答 4 題,再展開解析。
  3. 3

    最小實驗 2

    開啟最小實驗,實際操作一次並記下證據。
  4. 4

    Starter

    把 Starter 複製到自己的工作資料夾,照 3 張微步驟卡逐一完成;每一步都要看到結果再繼續。
  5. 5

    Checkpoint

    前往 Checkpoint,只處理 1 個本堂核心缺口;先預測,再用指定證據驗收。
  6. 6

    Solution

    前往 Solution,先看逐檔差異與修改原因,再決定要不要把完整內容帶回自己的檔案。
  7. 7

    變體驗收

    完成情境變體與獨立挑戰,留下檔案位置、操作、結果與驗收證據,並閱讀下一堂銜接:Vue CDN:下一堂會把同樣的狀態與畫面更新交給 Vue 管理。

檔案地圖:每個位置負責什麼?

先不要急著背檔名;請把檔案位置和責任連起來。你在階段頁會看到同一份檔案的可執行版本。

先學會這些詞

先從名詞、英文與白話意思開始;每次只展開一張名詞卡,再對照正式定義、範例與驗收證據。專業術語語法檔案路徑會使用不同樣式。第一次遇到術語時,先回到這裡,不要靠猜。

本堂 13 個詞先選一組,再展開需要的名詞卡;每次只保留目前的學習焦點。

表單與事件

6 個詞
表單form 白話意思:收集使用者輸入,並把一組欄位視為同一次送出的 HTML 區塊。 展開查看正式定義、範例與驗收證據
正式定義
HTMLform 元素,包含控制項與 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:逐段讀碼與驗證

每一步只回答五件事:為什麼做、改哪裡、會看到什麼、用什麼證據確認、卡住先查什麼。

  1. Step 1

    先建立能被瀏覽器理解的表單

    目的
    先把輸入欄位、label 關係、required 與 status 位置建立好,讓後面的 JavaScript 有明確目標。
    修改位置
    starter/files/contact.html 的 #contact-form
    建立姓名、Email、需求三個欄位,讓每個 label 對應 id,並加入 required、submit button 與 role=status。
    預期結果
    空白按送出時出現瀏覽器原生驗證;點擊 label 會把焦點移到對應欄位。
    驗收證據
    Elementsform/label/input 巢狀關係、for/id 對應、原生驗證提示與 #status。
    卡住先查
    只搜尋 contact.html 的 #contact-form、required、for、id、type=submit 與 #status。
  2. Step 2

    找到 submit 事件事件物件

    目的
    表單送出和 JavaScript handler 接起來,先理解事件何時發生、函式收到什麼。
    修改位置
    starter/files/main.jsform submit handler
    querySelector 找到 form,使用 addEventListener 監聽 submit,並讓 handler 收到 event。
    預期結果
    有效送出時 callback 被呼叫;尚未加入 preventDefault 前,仍能觀察瀏覽器原本的導覽行為。
    驗收證據
    Console/網址列與 submit callback 的執行位置;沒有把事件物件寫在 callback 外。
    卡住先查
    確認 selector 是 #contact-form,第二個參數是函式,且 event 只出現在 callback 參數範圍內。
  3. Step 3

    preventDefault 保留目前頁面

    目的
    取消表單預設導覽,讓狀態文字、輸入內容與後續非同步流程留在目前頁面。
    修改位置
    checkpoint/files/main.js 的 handleSubmit
    在 submit callback 收到的 event 上呼叫 preventDefault
    預期結果
    有效送出後網址不變,頁面可以繼續顯示 loading 與結果。
    驗收證據
    網址列沒有新增 query string,submit handler 仍能更新 #status。
    卡住先查
    只看 handleSubmit 的大括號內,確認 event.preventDefault() 位於 callback 的第一個有效步驟。
  4. 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) 是否留在結果收尾位置。
  5. Step 5

    Promiseasync/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 是否真的 return Promise,再看 await 是否位於 async 函式內。
  6. Step 6

    用失敗與重試驗收整段生命週期

    目的
    確認錯誤不是只顯示一行紅字,而是能恢復操作、保留輸入並讓學生說出證據。
    修改位置
    examples/minimal.htmlsolution/files/contact.htmlsolution/files/main.js
    先填寫欄位,分別測試成功與失敗,觀察 status、disabledaria-live、aria-busy、網址與輸入內容。
    預期結果
    失敗後輸入仍在、按鈕恢復可操作、重試可成功;390px 與 Tab 操作仍能看見焦點和狀態
    驗收證據
    網址、Elements 的 status/aria-live/data-state、按鈕 disabled、欄位 value、390px 與鍵盤 focus
    卡住先查
    先重現一條路徑:填寫一筆固定資料,只觀察 status 與按鈕,不同時修改多個檔案。
操作層級Live Lab 段落導覽:切換六個實驗步驟

讀碼順序:先找入口,再用畫面、ElementsConsole、網址列、status 的 data-state、按鈕 disabled 與 aria-busy 交叉驗證;本堂核心是表單事件如何進入可恢復的非同步狀態流程。

互動問答

先回答目前這一題,再展開解析;完成後按下一題,讓每一次回答都回到檔案或瀏覽器完成驗證。

Q1概念理解 表單form)」在本堂要解決什麼問題? 本題線索:表單;回到名詞卡與 Elements 看到欄位都位於同一個 form 內,空白送出會出現瀏覽器驗證提示。 展開解析收合解析

白話解析:

收集使用者輸入,並把一組欄位視為同一次送出的 HTML 區塊。 正式來說,HTMLform 元素,包含控制項與 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。;觀察:Elementsform/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。

立即驗證:

實際操作後檢查 Elementsform/label/input 巢狀關係、for/id 對應、原生驗證提示與 #status。。

Q3概念比較 表單」和「label 對應」在本堂的責任有什麼不同? 本題線索:比較 表單label 對應;操作位置:starter/files/main.jsform submit handler 展開解析收合解析

白話解析:

表單:收集使用者輸入,並把一組欄位視為同一次送出的 HTML 區塊。;label 對應:欄位名稱和輸入框要互相連得起來。

對照位置:

對照兩張名詞卡的正式定義:HTMLform 元素,包含控制項與 submit 行為;瀏覽器會依欄位規則進行原生驗證。/以 label 的 for 屬性對應控制項的 id,建立可點擊、可讀取的欄位關聯。

預期觀察:

你應該能指出兩者分別出現在哪個檔案或工具,以及哪一個結果會改變。

常見錯誤:

不要把「改變畫面」當成所有技術的責任;先說清楚誰負責結構、呈現、行為或驗收。

立即驗證:

starter/files/main.jsform submit handler 做「用 querySelector 找到 form,使用 addEventListener 監聽 submit,並讓 handler 收到 event。」,再用 Console/網址列與 submit callback 的執行位置;沒有把事件物件寫在 callback 外。 比較前後差異。

Q4除錯驗收 如果完成操作後結果不對,你會先從哪一個證據開始查? 本題線索:examples/minimal.htmlsolution/files/contact.htmlsolution/files/main.js;第一個檢查位置:網址、Elements 的 status/aria-live/data-state、按鈕 disabled、欄位 value、390px 與鍵盤 focus 展開解析收合解析

白話解析:

先描述症狀,再提出一個最小假設。本堂常見錯誤是:看到 error 就重新整理,或只看顏色沒有檢查 data-state、disabled 與輸入內容。

對照位置:

對照最後一段 Live Lab:examples/minimal.htmlsolution/files/contact.htmlsolution/files/main.js;檢查方式:先重現一條路徑:填寫一筆固定資料,只觀察 status 與按鈕,不同時修改多個檔案。

預期觀察:

你要能指出一個具體位置,而不是一次修改很多檔案。

常見錯誤:

不要先清快取、重裝工具或複製 Solution;先查看第一個可觀察錯誤。

立即驗證:

重新操作並留下「症狀、證據、假設、單一修正、結果」五項紀錄。

常見誤解

本堂任務與驗收

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

官方延伸閱讀

下載講師版 Markdown:需要授課或離線查閱時使用,內容包含完整教學節奏。