哈囉,各位邦友們!
昨天完成了 provideRouter 與兩個頁面(Dashboard / Heroes)。
今天要把「路由參數」接進來,實作 /detail/:id
英雄詳情頁。
重點是使用 withComponentInputBinding()
讓路由參數自動綁到元件的 input()
signal,不需要手動從 ActivatedRoute
取值。
HeroDetail
,並在元件內以 withComponentInputBinding()
自動接收 id
,查詢服務並顯示資料。app.routes.ts
加入 detail/:id
路由。Heroes
清單加上連結導向詳細頁。/detail/12
也能正確顯示。app.config.ts
註冊:provideRouter(routes, withComponentInputBinding())
。HeroService
與 getAll$()
或同步資料來源可供查詢。一、服務:新增單筆查詢
// src/app/hero.service.ts
import { delay, map, of, throwError } from 'rxjs';'rxjs';
// ...existing code...
@Injectable({ providedIn: 'root' })
export class HeroService {
// ...existing code...
getById$(id: number) {
return of(null).pipe(
delay(200), // 模擬非同步
map(() => this.getAll().find(h => h.id === id))
);
}
// ...existing code...
}
二、建立 HeroDetail 元件
ng g c hero-detail
// src/app/hero-detail/hero-detail.component.ts
import { Component, DestroyRef, effect, inject, input, signal } from '@angular/core';
import { Hero, HeroService } from '../hero.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { RouterModule } from '@angular/router';
@Component({
selector: 'app-hero-detail',
imports: [RouterModule],
templateUrl: './hero-detail.html',
styleUrl: './hero-detail.scss',
})
export class HeroDetail {
// 由 withComponentInputBinding() 自動把 route param `id` 綁進來
readonly id = input.required<number>();
private readonly heroService = inject(HeroService);
private readonly destroyRef = inject(DestroyRef);
// 狀態
readonly hero = signal<Hero | null>(null);
readonly loading = signal(true);
readonly error = signal<string | null>(null);
constructor() {
// 當 id 改變時重新載入
effect(() => {
const curId = Number(this.id());
this.loading.set(true);
this.error.set(null);
// 以 Observable 取得單筆
const sub = this.heroService
.getById$(curId)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: (h) => {
console.log('hero', h);
this.hero.set(h ?? null);
this.loading.set(false);
},
error: (e) => {
this.error.set(String(e ?? 'Unknown error'));
this.loading.set(false);
},
});
return () => sub.unsubscribe();
});
}
}
<!-- src/app/hero-detail/hero-detail.html -->
@if (loading()) {
<p class="muted">Loading detail...</p>
} @else if (error(); as e) {
<p class="error">Load failed: {{ e }}</p>
} @else if (hero(); as h) {
<section class="detail">
<h2>Hero #{{ h.id }}</h2>
<p>
<strong>{{ h.name }}</strong>
@if (h.rank) { <span class="rank">[{{ h.rank }}]</span> }
</p>
<a routerLink="/heroes">← Back to list</a>
</section>
} @else {
<p class="muted">No hero found.</p>
}
/* src/app/hero-detail/hero-detail.scss */
.detail {
padding: 8px 0;
}
.muted {
color: #888;
}
.error {
color: #c33;
}
.rank {
color: #445;
background: #eef;
padding: 2px 6px;
border-radius: 4px;
}
三、在路由加入 detail/:id
// src/app/app.routes.ts
import { Route } from "@angular/router";
import { DashboardComponent } from "./dashboard/dashboard.component";
import { HeroesComponent } from "./heroes/heroes.component";
import { HeroDetail } from "./hero-detail/hero-detail";
export const routes: Route[] = [
{ path: '', pathMatch: 'full', redirectTo: 'dashboard' },
{ path: 'dashboard', component: DashboardComponent, title: 'Dashboard' },
{ path: 'heroes', component: HeroesComponent, title: 'Heroes' },
{ path: 'detail/:id', component: HeroDetail, title: 'Hero Detail' },
];
四、Heroes 清單加入導覽連結
維持原本點擊 li 的行為,再額外提供「檢視」連結前往詳細頁。
// src/app/heros.component.ts
// ...existing code...
import { RouterModule } from '@angular/router';
@Component({
selector: 'app-heroes',
imports: [FormsModule, RouterModule],
templateUrl: './heroes.component.html',
styleUrl: './heroes.component.scss',
})
export class HeroesComponent {
// ...existing code...
}
<!-- src/app/heroes/heroes.component.html -->
@for (h of heroes(); track h.id; let i = $index; let c = $count) {
<li
(click)="onSelect(h)"
[class.is-a]="h.rank === 'A' || h.rank === 'S'"
[class.selected]="selectedHero()?.id === h.id"
[attr.data-id]="h.id"
[attr.aria-current]="selectedHero()?.id === h.id ? 'true' : null">
<span class="no">{{ i + 1 }}/{{ c }}</span>
<span class="name">{{ h.name }}</span>
@if (h.rank) { <span class="rank">[{{ h.rank }}]</span> }
<span class="actions">
<a [routerLink]="['/detail', h.id]" (click)="$event.stopPropagation()">View</a>
</span>
</li>
} @empty {
<li class="muted">No heroes.</li>
}
驗收清單:
/heroes
點擊某個項目的「View」能前往 /detail/:id
。常見錯誤與排查:
withComponentInputBinding()
:請檢查 app.config.ts
的 provideRouter(routes, withComponentInputBinding())
是否存在。detail/:id
,而元件的 input 也必須命名為 id
才能自動綁定。HeroDetail
路徑、app.routes.ts
匯入與宣告是否一致。今日小結:
我們完成了 /detail/:id
的詳細頁,並學會用 withComponentInputBinding()
讓路由參數無痛對應到元件輸入。
明天會開始整合 HttpClient
,把資料來源換成真正的 HTTP 呼叫。
參考資料: