iT邦幫忙

2025 iThome 鐵人賽

DAY 10
0
Modern Web

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

Day 10 Angular 事件綁定 – 讓按鈕驅動 UI

  • 分享至 

  • xImage
  •  

今日目標

  • 了解 (click) 事件綁定與 屬性 / 類別綁定[attr.*][class.*]
  • 用 Angular 實作「更多介紹」展開 / 收起(不再用原生 JS)
  • 用 Angular 實作「技能分類篩選」按鈕(搭配 Day 9 的資料綁定)

基礎概念:事件綁定與屬性綁定

  • 事件綁定:在模板上寫 (事件名)="TS方法()"

    <button (click)="doSomething()">點我</button>
    
    

    當按鈕被點擊,Angular 會呼叫元件類別(.ts)的 doSomething()

  • 屬性 / 類別綁定:用中括號把 HTML 屬性綁到變數

    <a [href]="linkUrl">連結</a>
    <button [class.active]="isActive">按鈕</button>
    <div [attr.aria-expanded]="isOpen">...</div>
    
    
  • 結構指令(回顧):ngIf / ngFor 控制 DOM 是否存在與如何重複渲染。


實作一:About 的「更多介紹」用 Angular 寫

1) about.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-about',
  templateUrl: './about.component.html',
  styleUrls: ['./about.component.scss']
})
export class AboutComponent {
  isMoreOpen = false; // 控制展開/收起

  toggleMore() {
    this.isMoreOpen = !this.isMoreOpen;
  }
}

2) about.component.html

把原先 hidden 與原生 JS 移除,改用 *ngIf 與屬性綁定:

<section id="about" class="container section" aria-labelledby="about-title">
  <h2 id="about-title">關於我</h2>
  <p>
    我是一名前端工程師,喜歡理解使用者需求並把它落地成產品。近期專注於
    Angular、TypeScript、前端架構與效能最佳化。
  </p>
  <blockquote class="quote">「持續學習,讓自己比昨天更強。」</blockquote>

  <!-- 只有在 isMoreOpen 為 true 時才渲染 -->
  <p *ngIf="isMoreOpen" id="more-info">
    曾參與金融科技與電商專案,也投入設計系統與可存取性。閒暇時間喜歡健身、魔術與寫作分享。
  </p>

  <!-- 事件綁定:點擊時呼叫 toggleMore() -->
  <buttonclass="btn small"
    type="button"
    (click)="toggleMore()"
    [attr.aria-expanded]="isMoreOpen"
    [attr.aria-controls]="'more-info'">
    {{ isMoreOpen ? '收起介紹' : '更多介紹' }}
  </button>
</section>

說明:

  • ngIf="isMoreOpen" 讓段落在狀態為 true 時才存在於 DOM。
  • aria-expanded / aria-controls 由狀態同步,對可存取性更友善。
  • 按鈕文字用插值切換({{ ... ? '收起' : '更多' }})。

實作二:Skills 的「分類篩選」用 Angular 寫

我們想要的互動

  • 按鈕列:全部 / 前端 / 後端 / 工具
  • 點某個分類,清單只顯示該類別
  • 被選取的按鈕高亮(aria-selected="true"、CSS 的 class 切換)

1) skills.component.ts

沿用 Day 9 的資料,加入「目前分類」與「過濾後清單」的 getter。

import { Component } from '@angular/core';

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

interface Skill {
  name: string;
  category: Exclude<Category, 'all'>; // skill 本身不會是 all
}

@Component({
  selector: 'app-skills',
  templateUrl: './skills.component.html',
  styleUrls: ['./skills.component.scss']
})
export class SkillsComponent {
  current: Category = 'all';

  skills: Skill[] = [
    { name: 'HTML / CSS / SCSS', category: 'frontend' },
    { name: 'TypeScript', category: 'frontend' },
    { name: 'Angular / React / Vue', category: 'frontend' },
    { name: 'Node.js / Express', category: 'backend' },
    { name: 'Git / GitHub / Docker', category: 'tools' },
    { name: 'Vite / Webpack', category: 'tools' }
  ];

  setFilter(cat: Category) {
    this.current = cat;
  }

  // 由目前分類產生要顯示的清單(模板可直接用)
  get filtered(): Skill[] {
    if (this.current === 'all') return this.skills;
    return this.skills.filter(s => s.category === this.current);
  }

  // ngFor trackBy:提升效能、避免重繪
  trackByName(_i: number, s: Skill) { return s.name; }
}

2) skills.component.html

(click) + [attr.aria-selected] 切換狀態,列表用 *ngFor 渲染 filtered

<section id="skills" class="container section" aria-labelledby="skills-title">
  <div class="section-header">
    <h2 id="skills-title">技能 Skillset</h2>

    <!-- 分類按鈕列 -->
    <div role="tablist" aria-label="技能分類" class="filters">
      <buttonrole="tab"
        class="chip"
        (click)="setFilter('all')"
        [attr.aria-selected]="current === 'all'">
        全部
      </button>
      <buttonrole="tab"
        class="chip"
        (click)="setFilter('frontend')"
        [attr.aria-selected]="current === 'frontend'">
        前端
      </button>
      <buttonrole="tab"
        class="chip"
        (click)="setFilter('backend')"
        [attr.aria-selected]="current === 'backend'">
        後端
      </button>
      <buttonrole="tab"
        class="chip"
        (click)="setFilter('tools')"
        [attr.aria-selected]="current === 'tools'">
        工具
      </button>
    </div>
  </div>

  <!-- 清單:用 filtered 取代全部 skills -->
  <ul class="skill-grid">
    <li *ngFor="let s of filtered; trackBy: trackByName">
      {{ s.name }}
    </li>
  </ul>
</section>

說明:

  • (click)="setFilter('frontend')":事件綁定,點即切換分類。
  • [attr.aria-selected]="current === 'frontend'":用屬性綁定同步高亮狀態。
  • filtered getter 讓模板很乾淨;trackBy 減少重繪。

成果

  • About 的「更多介紹」改成 純 Angular 寫法(click) + ngIf + 屬性綁定),移除原生 JS 依賴。
  • Skills 的分類列可以切換「全部 / 前端 / 後端 / 工具」,並且按鈕有高亮狀態。
  • 模板只負責顯示,資料與狀態都在 .ts 管理,維護成本更低。

小心踩雷(常見錯誤 → 正確做法)

  1. hidden 當邏輯
  • <p hidden>...</p> 然後用 JS 改 hidden
  • ✅ 用 ngIf / [hidden]:建議 ngIf,元件銷毀/建立更乾淨
  1. 按鈕沒有方法 / 方法名打錯
  • (click)="toggleMoree()" → console 報 undefined
  • ✅ 注意方法名稱與拼字;若需傳參 (click)="setFilter('tools')"
  1. 在模板寫複雜運算
  • ngFor="let s of skills.filter(...)" 每次檢測都跑運算
  • ✅ 把運算放 .ts 變成 getter 或預先產生的陣列(如 filtered
  1. 想高亮卻硬寫 class
  • <button class="chip active"> 然後用 JS 切 class
  • [class.active]="condition"[attr.aria-selected]="condition",資料驅動視覺
  1. 忘了 trackBy(清單大型時很卡)
  • ngFor="let item of list"
  • ngFor="let item of list; trackBy: trackById" 回傳穩定識別值

下一步(Day 11 預告)

明天我們把 Angular 的 雙向綁定 (Two-way binding) 帶進來,做兩個進階改善:

  • 在 Skills 上方加「關鍵字搜尋」即時過濾([(ngModel)] + 管線/過濾邏輯)
  • 在 Contact 表單用 Reactive Forms 或 Template-driven 加上「即時驗證」與錯誤訊息顯示(取代原本的原生 JS 驗證)

上一篇
Day 9 Angular 資料綁定 – 用程式產生 Skills 與 Projects 清單
下一篇
Day 11 Angular 表單與雙向綁定 – 關鍵字搜尋 + Reactive Forms 驗證 今日目標
系列文
Angular、React、Vue 三框架實戰養成:從零打造高品質前端履歷網站12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言