[(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.ts
import { 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()