UiStateService
,集中管理狀態<body>
顏色)Angular 沒強迫你用特定解法,但常見三種:
ng g s services/ui-state
ui-state.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class UiStateService {
// 預設值:亮色模式
private themeSubject = new BehaviorSubject<'light' | 'dark'>('light');
theme$ = this.themeSubject.asObservable();
private skillCategorySubject = new BehaviorSubject<'all' | 'frontend' | 'backend' | 'tools'>('all');
skillCategory$ = this.skillCategorySubject.asObservable();
private keywordSubject = new BehaviorSubject<string>('');
keyword$ = this.keywordSubject.asObservable();
// Actions:提供更新方法
setTheme(theme: 'light' | 'dark') {
this.themeSubject.next(theme);
localStorage.setItem('theme', theme); // 永續化
}
setSkillCategory(cat: 'all' | 'frontend' | 'backend' | 'tools') {
this.skillCategorySubject.next(cat);
}
setKeyword(kw: string) {
this.keywordSubject.next(kw);
}
// 初始化時可從 localStorage 把 theme 載回來
init() {
const saved = localStorage.getItem('theme') as 'light' | 'dark' | null;
if (saved) this.themeSubject.next(saved);
}
}
app.component.ts
import { Component, OnInit, Renderer2 } from '@angular/core';
import { UiStateService } from './services/ui-state.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {
constructor(private ui: UiStateService, private renderer: Renderer2) {}
ngOnInit() {
this.ui.init();
this.ui.theme$.subscribe(theme => {
this.renderer.setAttribute(document.documentElement, 'data-theme', theme);
});
}
}
👉 現在主題狀態集中管理,Header / 按鈕 / AppComponent 都能共用同一份。
header.component.ts
import { Component } from '@angular/core';
import { UiStateService } from '../../services/ui-state.service';
@Component({
selector: 'app-header',
templateUrl: './header.component.html'
})
export class HeaderComponent {
theme$ = this.ui.theme$;
constructor(private ui: UiStateService) {}
toggleTheme() {
this.theme$.subscribe(current => {
const next = current === 'dark' ? 'light' : 'dark';
this.ui.setTheme(next);
}).unsubscribe(); // 單次取值
}
}
header.component.html
<header class="site-header">
<div class="container">
<a class="brand" routerLink="/">Chiayu</a>
<nav class="site-nav">
<a routerLink="/projects">作品集</a>
<a routerLink="/contact">聯絡</a>
</nav>
<button type="button" (click)="toggleTheme()">
切換主題
</button>
</div>
</header>
skills.component.ts
import { Component, OnInit } from '@angular/core';
import { UiStateService } from '../../services/ui-state.service';
import { combineLatest, map } from 'rxjs';
import { Skill, SkillCategory } from '../../models/skill.model';
import { SkillsDataService } from '../../services/skills-data.service';
@Component({
selector: 'app-skills',
templateUrl: './skills.component.html'
})
export class SkillsComponent implements OnInit {
skills: Skill[] = [];
filtered$ = combineLatest([
this.ui.skillCategory$,
this.ui.keyword$
]).pipe(
map(([cat, kw]) =>
this.skills.filter(s => {
const byCat = cat === 'all' || s.category === cat;
const byKw = !kw || s.name.toLowerCase().includes(kw.toLowerCase());
return byCat && byKw;
})
)
);
constructor(private ui: UiStateService, private svc: SkillsDataService) {}
ngOnInit() {
this.skills = this.svc.getAll();
}
setFilter(cat: SkillCategory | 'all') { this.ui.setSkillCategory(cat); }
setKeyword(kw: string) { this.ui.setKeyword(kw); }
}
skills.component.html
<section class="container section">
<h2>技能 Skillset</h2>
<div class="filters">
<button (click)="setFilter('all')">全部</button>
<button (click)="setFilter('frontend')">前端</button>
<button (click)="setFilter('backend')">後端</button>
<button (click)="setFilter('tools')">工具</button>
</div>
<input type="text" placeholder="搜尋…" (input)="setKeyword($event.target.value)" />
<ul>
<li *ngFor="let s of filtered$ | async">{{ s.name }}</li>
</ul>
</section>
BehaviorSubject
管理,初始值、最新值、訂閱者同步都搞定this.theme$.subscribe(...)
每次呼叫都留一個訂閱async
pipe 或 take(1)
,避免 memory leak@Input()
傳來傳去BehaviorSubject
會保留最新值(適合狀態)ReplaySubject
會保留多個值(適合事件歷史)我們要更深入 RxJS + 狀態管理:
switchMap
、debounceTime
、distinctUntilChanged
)