iT邦幫忙

2025 iThome 鐵人賽

DAY 4
0
Modern Web

Angular、React、Vue 三框架實戰養成:從零打造高品質前端履歷網站系列 第 4

DOM 操作與事件綁定 – 用 TS 操作 HTML 元素

  • 分享至 

  • xImage
  •  

今日目標

  • 了解 DOM 與常用的節點操作:查找、內容、屬性、樣式、類別、資料屬性
  • 會用 事件綁定(click、input)及 事件委派(event delegation)
  • 在履歷網站加入三個互動功能:主題切換、技能分類篩選、照片切換

基礎概念

什麼是 DOM?

DOM(Document Object Model)把 HTML 文件轉成「節點(Node)樹」,你可以用 JavaScript/TypeScript 操作它:查找元素、改文字、改屬性、改樣式、監聽事件等等。

常用 API 速查(本篇會全部用到)

  • 查找元素:querySelectorquerySelectorAll
  • 內容:textContentinnerHTML(本篇盡量用 textContent
  • 屬性:getAttributesetAttributehasAttributeremoveAttribute
  • 樣式與類別:classList.add/remove/togglestyle.xxx
  • 自訂資料屬性:dataset(如 data-category
  • 事件:addEventListener('click', handler)preventDefault()stopPropagation()
  • 事件委派:監聽父節點,靠 event.target 判斷實際點擊者

實作一:主題切換(亮/暗)

HTML(在 <header> 裡放一顆切換按鈕)

<button id="theme-toggle" type="button" aria-pressed="false">切換主題</button>

建議 Day 2 的 CSS/SCSS 有用到顏色變數(或 root 變數),這裡只示範最小掛鉤:當 有 data-theme="dark" 就套用深色主題。

最小 CSS 掛鉤(節錄,供參考)

/* 你可以在 Day 2 的樣式檔中加入 */
:root {
  --bg: #ffffff;
  --fg: #2c3e50;
}
html[data-theme="dark"] {
  --bg: #1f2937;
  --fg: #f3f4f6;
}
body { background: var(--bg); color: var(--fg); }

TypeScript(在 main.ts

// 主題切換:把狀態存在 <html data-theme="..."> 與 localStorage
const htmlEl = document.documentElement;
const themeToggleBtn = document.querySelector<HTMLButtonElement>('#theme-toggle');

function applyTheme(theme: 'light' | 'dark') {
  htmlEl.setAttribute('data-theme', theme);
  if (themeToggleBtn) {
    themeToggleBtn.setAttribute('aria-pressed', String(theme === 'dark'));
    themeToggleBtn.textContent = theme === 'dark' ? '切換為亮色' : '切換為暗色';
  }
  localStorage.setItem('theme', theme);
}

// 初始化:讀取上次選擇
const saved = (localStorage.getItem('theme') as 'light' | 'dark') || 'light';
applyTheme(saved);

// 綁定按鈕
themeToggleBtn?.addEventListener('click', () => {
  const next = htmlEl.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
  applyTheme(next as 'light' | 'dark');
});


實作二:技能分類篩選(全部/前端/後端/工具)

HTML(在 Skills 區塊增加篩選器與標籤上的 data-category

<section id="skills">
  <h2>技能 Skillset</h2>

  <!-- 篩選器 -->
  <div id="skill-filters" role="tablist" aria-label="技能分類">
    <button role="tab" data-filter="all" aria-selected="true">全部</button>
    <button role="tab" data-filter="frontend">前端</button>
    <button role="tab" data-filter="backend">後端</button>
    <button role="tab" data-filter="tools">工具</button>
  </div>

  <ul id="skill-list">
    <li data-category="frontend">HTML / CSS / SCSS</li>
    <li data-category="frontend">TypeScript</li>
    <li data-category="frontend">Angular / React / Vue</li>
    <li data-category="backend">Node.js / Express</li>
    <li data-category="tools">Git / GitHub / Docker</li>
  </ul>
</section>

TypeScript(事件委派處理整個篩選器)

type SkillCategory = 'all' | 'frontend' | 'backend' | 'tools';

const filters = document.querySelector<HTMLDivElement>('#skill-filters');
const skillList = document.querySelector<HTMLUListElement>('#skill-list');

function applySkillFilter(cat: SkillCategory) {
  if (!skillList) return;
  const items = Array.from(skillList.querySelectorAll<HTMLLIElement>('li'));
  items.forEach(li => {
    const c = (li.dataset.category || 'frontend') as SkillCategory;
    li.style.display = (cat === 'all' || c === cat) ? '' : 'none';
  });
}

filters?.addEventListener('click', (e) => {
  const target = e.target as HTMLElement;
  if (target.matches('button[data-filter]')) {
    const cat = target.getAttribute('data-filter') as SkillCategory;
    // 視覺/a11y 狀態
    filters.querySelectorAll('[aria-selected="true"]').forEach(el => el.setAttribute('aria-selected', 'false'));
    target.setAttribute('aria-selected', 'true');
    // 套用篩選
    applySkillFilter(cat);
  }
});

// 預設顯示全部
applySkillFilter('all');


實作三:照片切換(正式照/生活照)

HTML(在 About 區塊加入兩張圖的來源路徑)

<section id="about">
  <h2>關於我</h2>
  <img id="avatar" src="me-formal.jpg" alt="Chiayu 的正式照片" width="200"
       data-alt-src="me-casual.jpg">
  <p>嗨,我是 Chiayu,一名前端工程師...</p>
  <button id="photo-toggle" type="button">切換照片</button>
</section>

這裡使用 data-alt-src 存放替代圖片路徑,日後在框架中也很好綁定。

TypeScript

const img = document.querySelector<HTMLImageElement>('#avatar');
const photoToggle = document.querySelector<HTMLButtonElement>('#photo-toggle');

photoToggle?.addEventListener('click', () => {
  if (!img) return;
  const current = img.getAttribute('src') || '';
  const altSrc = img.dataset.altSrc || '';
  if (!altSrc) return;
  // 交換 src 與 data-alt-src
  img.setAttribute('src', altSrc);
  img.dataset.altSrc = current;
  // 同步替代文字(若兩張照性質不同)
  const isFormal = /formal/.test(altSrc);
  img.alt = isFormal ? 'Chiayu 的正式照片' : 'Chiayu 的生活照片';
});


成果

完成以上三段後,你的履歷網站具備:

  1. 主題切換:可記住使用者偏好(localStorage),重載仍生效。
  2. 技能篩選:以 data-category 為依據,不用改 HTML 結構就能擴充。
  3. 照片切換:一鍵切換兩張照片,實作了自訂資料屬性與屬性交換。

小心踩雷(常見誤用 → 正確作法)

  1. 直接用 innerHTML 填入未消毒字串

    錯誤:

titleEl.innerHTML = userInput; // 可能造成 XSS

正確:

titleEl.textContent = userInput; // 文字內容請用 textContent

  1. 忘了考慮元素可能為 null

    錯誤:

document.querySelector('#x')!.addEventListener('click', fn);

正確:

const el = document.querySelector<HTMLButtonElement>('#x');
if (el) el.addEventListener('click', fn);

  1. 為每一個子元素都綁監聽,造成效能浪費

    錯誤:

document.querySelectorAll('#skill-filters button')
  .forEach(b => b.addEventListener('click', onClick));

正確:事件委派(監聽父元素)

filters?.addEventListener('click', (e) => {
  const target = e.target as HTMLElement;
  if (target.matches('button[data-filter]')) { /* ... */ }
});

  1. 用行內樣式硬改所有外觀,導致樣式與行為耦合

    錯誤:

el.style.display = 'none';
el.style.color = 'red';

正確:

el.classList.add('is-hidden'); // 把樣式定義在 CSS 中

  1. 濫用 dataset 當狀態倉庫

    錯誤:

el.dataset.state = JSON.stringify({ aLotOfState: true }); // 難以維護

正確: dataset 存放輕量、與 DOM 強相關的設定值(如類別、替代路徑);複雜狀態請放 JS/TS 內部結構。


進一步練習(可選)

  • 技能清單支援「關鍵字即時搜尋」(監聽 input 事件 → includes 過濾)
  • 主題切換加上動畫過渡(CSS transition)
  • 按下導覽列的錨點時,平滑滾動到目標區塊(scrollIntoView({ behavior: 'smooth' })

下一步(Day 5 預告)

明天我們會做一個 原生 HTML/CSS/TS 的一頁式自我介紹頁(小專案),把 Day1–Day4 的知識整合起來,形成「無框架也能交付」的最小可用作品。

接著 Day 6 起,我們會把同樣的資訊架構搬進 Angular,正式開始框架實戰。


上一篇
Day 3 TypeScript 開始 – 讓履歷網站動起來
下一篇
Day 5 RWD 響應式網頁設計 – 讓自我介紹頁在各裝置都好看
系列文
Angular、React、Vue 三框架實戰養成:從零打造高品質前端履歷網站28
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言