這一課完成後:你會使用 addEventListener() 處理 click、input、submit,讀取表單值、使用 event object,並理解 preventDefault() 在阻止瀏覽器預設行為時的用途。

事件的核心:某件事發生了

瀏覽器裡一直都在發生事件。使用者點了一個按鈕、在輸入框打字、送出表單、按下鍵盤、移動滑鼠,甚至頁面本身載入完成,都可以形成事件。

某件事發生 → Browser 建立 event → listener 被呼叫 → JavaScript 執行

事件讓網頁從「載入後自己跑完」變成「等使用者做事再回應」。

第一個 click event

HTML:

<button id="change-message" type="button">改變訊息</button>
<p id="message">還沒有被點擊。</p>

JavaScript:

const button = document.querySelector("#change-message");
const message = document.querySelector("#message");

button.addEventListener("click", () => {
  message.textContent = "按鈕剛剛被點了。";
});

把它拆開:

button → 要監聽誰
"click" → 要監聽哪種事件
() => { ... } → 事件發生時執行什麼

addEventListener() 不會立刻執行函式。它只是先註冊:「如果之後發生 click,就執行這段。」

不要把函式直接呼叫掉

假設:

function changeMessage() {
  message.textContent = "Changed";
}

正確註冊:

button.addEventListener("click", changeMessage);

初學者很常誤寫:

button.addEventListener("click", changeMessage());

後者會在程式跑到那一行時就先呼叫 changeMessage(),而不是等 click 才呼叫。

changeMessage → 把函式交給 listener
changeMessage() → 現在立刻執行函式

event object:瀏覽器會把事件資訊交給你

button.addEventListener("click", (event) => {
  console.log(event);
});

event 是瀏覽器建立的物件,裡面描述這次事件。

例如:

button.addEventListener("click", (event) => {
  console.log(event.type);
  console.log(event.target);
});

event.type 可能是 click;event.target 則是實際觸發事件的元素。

變數名稱可以不是 event,很多人也會寫 e,但初學時寫完整一點比較容易讀。

input event:使用者每次改輸入內容都會觸發

HTML:

<label for="name-input">你的名字</label>
<input id="name-input" type="text" />
<p id="preview">你好。</p>

JavaScript:

const input = document.querySelector("#name-input");
const preview = document.querySelector("#preview");

input.addEventListener("input", () => {
  preview.textContent = `你好,${input.value}`;
});

value 是表單元素目前的值。使用者每打或刪一個字,input 事件通常都會再觸發一次。

使用者輸入 → input event → 讀取 input.value → 更新 DOM

也可以從 event.target 讀值

input.addEventListener("input", (event) => {
  preview.textContent = `你好,${event.target.value}`;
});

如果 handler 就是專門處理這個 input,這種寫法也很常見。

目前先知道兩種都可以,不用急著追求「唯一正確寫法」。

空字串也要處理

如果使用者把輸入框刪空,上面的程式會顯示:

你好,

可以加入條件:

input.addEventListener("input", () => {
  const name = input.value.trim();

  if (name === "") {
    preview.textContent = "請輸入名字。";
    return;
  }

  preview.textContent = `你好,${name}。`;
});

trim() 會去掉字串前後空白;return 則可以讓函式在這裡提早結束。

submit event:表單應該監聽 form,不是只監聽按鈕

HTML:

<form id="profile-form">
  <label for="project-name">專案名稱</label>
  <input id="project-name" name="projectName" type="text" required />
  <button type="submit">送出</button>
</form>

<p id="result"></p>

JavaScript:

const form = document.querySelector("#profile-form");
const projectName = document.querySelector("#project-name");
const result = document.querySelector("#result");

form.addEventListener("submit", (event) => {
  event.preventDefault();

  result.textContent = `已收到:${projectName.value}`;
});

為什麼不是只監聽按鈕的 click?因為 form 可以透過不同方式送出,例如在某些情境按 Enter。監聽 submit 比監聽「某一個按鈕被點」更貼近真正的行為。

preventDefault() 在阻止瀏覽器原本要做的事

HTML form 預設送出時,瀏覽器通常會進行 navigation / request,頁面可能因此重新載入。

在純前端練習裡,我們不想讓頁面立刻跳走,所以:

event.preventDefault();

它的意思不是「停止所有 JavaScript」,而是:

事件照樣發生 → handler 照樣執行 → 阻止這個事件的瀏覽器預設行為

狀態:畫面不是資料本身

做一個計數器:

<button id="increase" type="button">+1</button>
<p id="count">0</p>

JavaScript:

const increase = document.querySelector("#increase");
const countText = document.querySelector("#count");

let count = 0;

increase.addEventListener("click", () => {
  count += 1;
  countText.textContent = count;
});

這裡真正的資料是變數 count;#count 只是把目前狀態顯示出來。

state 改變 → 更新 DOM → 使用者看到新畫面

這個思維以後進 React、Vue 或任何 UI framework 都會一直出現。

不要把 DOM 當成唯一資料來源

這種寫法可以動:

const current = Number(countText.textContent);
countText.textContent = current + 1;

但當應用開始變複雜,如果每次都回 DOM 把字讀出來當資料來源,會很難追。

更穩定的習慣通常是:

JavaScript state 是資料 → render / update DOM 反映資料

一個元素可以監聽很多事件

input.addEventListener("focus", () => {
  console.log("開始輸入");
});

input.addEventListener("blur", () => {
  console.log("離開輸入框");
});

你也可能看到 change、keydown、keyup、pointerdown 等等。

現在不要背完整事件表。先掌握:你要先問「我希望使用者做什麼時觸發邏輯?」再選事件。

事件處理函式不要塞成巨大一坨

例如:

function updatePreview(name) {
  preview.textContent = name
    ? `你好,${name}。`
    : "請輸入名字。";
}

input.addEventListener("input", () => {
  const name = input.value.trim();
  updatePreview(name);
});

事件 listener 可以很薄:取得事件資料,再交給有清楚名稱的函式處理。

這會比把所有判斷、DOM 操作、資料處理全部塞進一個匿名函式容易維護。

多個按鈕:event.target 會開始有用

HTML:

<button class="filter" data-filter="all">全部</button>
<button class="filter" data-filter="done">已完成</button>
<button class="filter" data-filter="todo">未完成</button>

JavaScript:

const filters = document.querySelectorAll(".filter");

for (const filter of filters) {
  filter.addEventListener("click", (event) => {
    console.log(event.target.dataset.filter);
  });
}

data-* attribute 可以透過 dataset 讀取。這讓 DOM element 可以帶一點和操作相關的描述資訊。

事件也會傳遞,但現在先知道有這件事

如果一個按鈕放在一個卡片裡,click 事件可能會沿著 DOM 結構繼續傳遞。你之後會看到 bubbling、capturing、event delegation。

Web Basics 這一課不深入,但先記住:

事件不一定只和「被點的那一個元素」有關,它也和 DOM 結構有關。

等你開始處理大量動態列表時,event delegation 才會真正變得重要。

小挑戰:做一個互動式課程清單

沿用上一課的課程資料與 DOM rendering,再加入互動。

需求 1:畫面要有一個輸入框,可以新增課程名稱。

需求 2:用 form 的 submit 處理新增,不只監聽按鈕 click。

需求 3:空白內容不能被新增;需要顯示提示。

需求 4:新增後要更新 JavaScript 中的 lessons array,再重新把結果顯示到 DOM。

需求 5:每個 lesson 至少有一個「完成 / 未完成」按鈕。

需求 6:點擊後要改變該 lesson 的 done 狀態,再更新畫面。

需求 7:畫面上要顯示目前完成數,例如 3 / 5 completed。

需求 8:至少在一個 handler 裡使用 event object。

這次重點不是 UI 漂亮,而是完整跑通:

User action → Event → Update state → Update DOM

除錯時先問事件有沒有發生

如果按鈕完全沒反應,可以依序檢查:

元素有沒有選到? → listener 有沒有註冊? → event 有沒有觸發? → handler 裡有沒有報錯? → DOM 更新是不是寫錯位置?

最簡單的第一步常常是:

button.addEventListener("click", () => {
  console.log("clicked");
});

先確認 event pipeline 有通,再處理後面的資料與 DOM。

最後收斂:互動網站的最小循環

使用者操作 → event → JavaScript handler → state / data 改變 → DOM 更新 → 使用者看到結果

到這裡,你已經不只是「會改 DOM」,而是開始理解前端應用真正的互動循環。

下一課會再多一層:資料不一定一開始就在你的 JavaScript 裡。網站可以用 Fetch 向外部 API 發 request,再根據 response 更新狀態和畫面。

完成條件:你能使用 addEventListener 處理 click、input 與 submit,讀取使用者輸入、使用 preventDefault() 控制表單流程,並用「event → state → DOM」的方式完成至少一個互動功能。