[(ngModel)]:畫面改 → 變數改;變數改 → 畫面跟著改[(ngModel)][(ngModel)]="keyword" 等同於同時綁定 [ngModel]="keyword" + (ngModelChange)="keyword=$event"。FormsModule 才能用。ngModel):小型表單、快速、直覺;驗證邏輯分散在模板。FormGroup/FormControl):中大型表單、驗證邏輯集中在 TS、可測試、可組合。今天兩者都用:Skills 搜尋用 ngModel,Contact 表單用 Reactive Forms。
開啟 src/app/app.module.ts,加入 FormsModule 和 ReactiveFormsModule:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
// 其他元件 imports 省略
@NgModule({
  declarations: [
    AppComponent,
    // 你的 Header/Hero/About/Skills/Projects/Contact/Footer…
  ],
  imports: [
    BrowserModule,
    FormsModule,          // for [(ngModel)]
    ReactiveFormsModule,  // for Reactive Forms
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}
[(ngModel)])skills.component.ts(在 Day 10 的基礎上加 keyword 過濾)import { Component } from '@angular/core';
type Category = 'all' | 'frontend' | 'backend' | 'tools';
interface Skill {
  name: string;
  category: Exclude<Category, 'all'>;
}
@Component({
  selector: 'app-skills',
  templateUrl: './skills.component.html',
  styleUrls: ['./skills.component.scss']
})
export class SkillsComponent {
  current: Category = 'all';
  keyword = ''; // 追蹤關鍵字
  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[] {
    const kw = this.keyword.trim().toLowerCase();
    return this.skills.filter(s => {
      const byCat = this.current === 'all' || s.category === this.current;
      const byKw  = !kw || s.name.toLowerCase().includes(kw);
      return byCat && byKw;
    });
  }
  trackByName(_i: number, s: Skill) { return s.name; }
}
skills.component.html(新增搜尋輸入框 + 計數)<section id="skills" class="container section" aria-labelledby="skills-title">
  <div class="section-header">
    <h2 id="skills-title">技能 Skillset</h2>
    <!-- 分類按鈕列(沿用 Day 10) -->
    <div role="tablist" aria-label="技能分類" class="filters">
      <button role="tab" class="chip" (click)="setFilter('all')"      [attr.aria-selected]="current==='all'">全部</button>
      <button role="tab" class="chip" (click)="setFilter('frontend')" [attr.aria-selected]="current==='frontend'">前端</button>
      <button role="tab" class="chip" (click)="setFilter('backend')"  [attr.aria-selected]="current==='backend'">後端</button>
      <button role="tab" class="chip" (click)="setFilter('tools')"    [attr.aria-selected]="current==='tools'">工具</button>
    </div>
  </div>
  <!-- 🔎 關鍵字搜尋:雙向綁定 -->
  <div class="field" style="margin:12px 0;">
    <label for="skill-search">關鍵字搜尋</label>
    <input id="skill-search" type="text" [(ngModel)]="keyword" placeholder="例如:Angular、Docker…" />
    <small class="muted">結果:{{ filtered.length }} 項</small>
  </div>
  <!-- 清單 -->
  <ul class="skill-grid">
    <li *ngFor="let s of filtered; trackBy: trackByName">
      {{ s.name }}
    </li>
  </ul>
</section>
✅ 現在輸入框任何變動都會即時反映到 keyword,清單自動過濾,不用自己手動更新 DOM。
contact.component.tsimport { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
@Component({
  selector: 'app-contact',
  templateUrl: './contact.component.html',
  styleUrls: ['./contact.component.scss']
})
export class ContactComponent {
  constructor(private fb: FormBuilder) {}
  form = this.fb.group({
    name:    ['', [Validators.required, Validators.minLength(2), Validators.maxLength(20)]],
    email:   ['', [Validators.required, Validators.email]],
    message: ['', [Validators.required, Validators.minLength(10)]],
  });
  // 便捷 getter(模板好讀)
  get name()    { return this.form.get('name'); }
  get email()   { return this.form.get('email'); }
  get message() { return this.form.get('message'); }
  submit() {
    if (this.form.invalid) {
      this.form.markAllAsTouched(); // 讓錯誤一次顯示
      return;
    }
    // TODO: 在這裡串接 API 或寄信服務
    alert('已送出!感謝你的來信。');
    this.form.reset();
  }
}
contact.component.html把原本的 <form> 改成 Reactive Forms 寫法:[formGroup]、formControlName,加上即時錯誤提示。
<section id="contact" class="container section" aria-labelledby="contact-title" [formGroup]="form">
  <h2 id="contact-title">聯絡我</h2>
  <form (ngSubmit)="submit()" novalidate>
    <div class="field">
      <label for="name">姓名</label>
      <input id="name" type="text" formControlName="name" placeholder="王小明" />
      <small class="error" *ngIf="name?.touched && name?.invalid">
        <ng-container *ngIf="name?.errors?.['required']">請輸入姓名。</ng-container>
        <ng-container *ngIf="name?.errors?.['minlength']">至少 2 個字。</ng-container>
        <ng-container *ngIf="name?.errors?.['maxlength']">最多 20 個字。</ng-container>
      </small>
    </div>
    <div class="field">
      <label for="email">Email</label>
      <input id="email" type="email" formControlName="email" placeholder="name@example.com" />
      <small class="error" *ngIf="email?.touched && email?.invalid">
        <ng-container *ngIf="email?.errors?.['required']">請輸入 Email。</ng-container>
        <ng-container *ngIf="email?.errors?.['email']">Email 格式不正確。</ng-container>
      </small>
    </div>
    <div class="field">
      <label for="message">訊息</label>
      <textarea id="message" rows="4" formControlName="message" placeholder="想合作的內容…"></textarea>
      <small class="error" *ngIf="message?.touched && message?.invalid">
        <ng-container *ngIf="message?.errors?.['required']">請輸入訊息。</ng-container>
        <ng-container *ngIf="message?.errors?.['minlength']">至少 10 個字。</ng-container>
      </small>
    </div>
    <div class="actions">
      <button class="btn" type="submit">送出</button>
      <button class="btn btn-outline" type="button" (click)="form.reset()">清除</button>
    </div>
  </form>
  <aside class="contact-aside">
    <h3>其他聯絡方式</h3>
    <address>
      Email:<a href="mailto:hotdanton08@hotmail.com">hotdanton08@hotmail.com</a><br />
      GitHub:<a href="https://github.com/你的帳號">github.com/你的帳號</a><br />
      LinkedIn:<a href="https://linkedin.com/in/你的帳號">linkedin.com/in/你的帳號</a>
    </address>
  </aside>
</section>
✅ 只要輸入框被碰過(touched)且不符合驗證規則,就會即時顯示對應錯誤。
✅ 送出時若無效,會
markAllAsTouched(),一次提示所有欄位。
FormsModule 就用 ngModel
[(ngModel)] 會報錯AppModule 加上 FormsModule
ReactiveFormsModule 就用 formGroup
[formGroup] 或 formControlName 無效AppModule 加上 ReactiveFormsModule
touched 或 dirty
ngIf="control?.touched && control?.invalid"
ngFor="let s of skills | filter: keyword : current" 自定義 pipe 若不純會拖慢變更檢測form.reset(),否則錯誤訊息或 touched 狀態會殘留明天要把「外觀」與「資料」更解耦:
@Injectable()