上一篇利用 Angular 路由機制實作待辦事項清單與表單頁面的切換,這一篇將路由的參數或資料的定義,來實作待辦事項的編輯功能。
在實作之前,需要在 task.component.ts 中加入 @Output
屬性,並在點選編輯按鈕時觸發此屬性 emit()
方法。
export class TaskComponent implements OnInit, OnChanges {
@Output() edit = new EventEmitter<void>();
}
<div class="card">
<div class="content">
<span>
{{ subject | slice: 0:10 }}<span *ngIf="subject.length > 10">... </span>
<button type="button" [disabled]="state === TaskState.Finish" (click)="edit.emit()">編輯</button>
</span>
</div>
</div>
另外,在 task.service.ts 加入依編號取得待辦事項方法。
export class TaskRemoteService {
get(id: number): Observable<Task> {
return this.httpClient.get<Task>(`${this._url}/${id}`);
}
}
在路由定義中,除了指定單純的字串之外,可以透過冒號 (:) 指定一路由變數,來利用網址路徑的內容傳遞資訊。因此,可以在 app-routing.module.ts 加入待辦事項表單的路由設定,並在此設定加入 :id
來接收待辦事項編號,
const routes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: 'main' },
{ path: 'main', component: MainPageComponent },
{ path: 'task-list', component: TaskPageComponent },
{ path: 'task-form', component: TaskFormComponent },
{ path: 'task-form/:id', component: TaskFormComponent },
];
接著就可以在 task-list.component.html 綁定待辦事項的編輯事項,並在 task-list.component.ts 中利用 Router 服務切換至表單頁面。
<ng-container *ngIf="tasks$ | async as tasks; else dataEmpty">
<app-task
*ngFor="let task of tasks; let odd = odd"
[class.odd]="odd"
[subject]="task.subject"
[(state)]="task.state"
[level]="task.level"
[tags]="task.tags"
[expectDate]="task.expectDate"
[finishedDate]="task.finishedDate"
(edit)="onEdit(task.id)"
></app-task>
</ng-container>
export class TaskListComponent implements OnInit {
constructor(private router: Router, private taskService: TaskRemoteService) {}
onEdit(id: number): void {
this.router.navigate(['task-form', id]);
}
}
Angular 內建的 ActivatedRoute 服務元件可以用來取得從路由中取得變數內容,因此可以在 task-form.component.ts 中注入 ActivatedRoute 服務,並使用此服務的 snapshot
屬性來取得 id
路由變數。
export class TaskFormComponent implements OnInit {
constructor(private fb: FormBuilder, private route: ActivatedRoute, private taskService: TaskRemoteService) {}
ngOnInit(): void {
const id = +this.route.snapshot.paramMap.get('id');
if (!!id) {
this.taskService
.get(id)
.pipe(
tap(() => this.tags.clear),
tap((task) => this.onAddTag(task.tags.length))
)
.subscribe((task) => this.form.patchValue(task));
}
}
onAddTag(count: number): void {
for (let i = 0; i <= count - 1; i++) {
const tag = this.fb.control(undefined);
this.tags.push(tag);
}
}
}
在上面程式中,因為取得路由參數會是字串型別,故需要利用 +
來轉換成數值型別;另外,在響應式表單 (Reactive Form) 中,需要先存在表單陣列 (FormArray) 的項目結構,才能在利用 patchValue()
方法設定表單值後,頁面能夠正確的顯示。
不過利用 snapshot
所取得路由變數會有所限制;首先在 task-form.component.ts 中的表單模型加入 id
欄位,並注入 Router 服務來實作下筆待辦事項頁面切換的需求。
export class TaskFormComponent implements OnInit {
get id(): FormControl {
return this.form.get('id') as FormControl;
}
constructor(private fb: FormBuilder, private router: Router, private route: ActivatedRoute, private taskService: TaskRemoteService) {}
ngOnInit(): void {
this.form = this.fb.group({
id: this.fb.control(undefined),
subject: this.fb.control(undefined, [Validators.required], [this.shouldBeUnique.bind(this)]),
state: this.fb.control(0),
level: this.fb.control(undefined, [Validators.required]),
tags: this.fb.array([], [this.arrayCannotEmpty()]),
});
}
onNext(): void {
this.router.navigate(['task-form', this.id.value + 1]);
}
}
<form [formGroup]="form">
<div class="button">
<button type="button" (click)="onSave()">儲存</button>
<button type="button" (click)="onNext()">下一筆</button>
</div>
</form>
從上圖結果可見,當使用 snapshot
取得路由變數時,因為 OnInit()
生命週期方法只會在元件載入被觸發一次,而導致在路由切換後無法正確更換表單資料。此時,則會使用 ActivatedRoute 服務中的 paramMap
屬性來監控訂閱路由變數的變化。
export class TaskFormComponent implements OnInit {
constructor(private fb: FormBuilder, private router: Router, private route: ActivatedRoute, private taskService: TaskRemoteService) {}
ngOnInit(): void {
this.route.paramMap
.pipe(
map((param) => +param.get('id')),
filter((id) => !!id),
switchMap((id) => this.taskService.get(id)),
tap(() => this.tags.clear()),
tap((task) => this.onAddTag(task.tags.length))
)
.subscribe((task) => this.form.patchValue(task));
}
}
需要注意一點,針對 Observable 物件所建立的訂閱監控,在其狀態未為完成 (complete) 前是會一直存在的,而在每次元件頁面載入時都會建立一個路由的訂閱,因此需要在元件被銷毀時取消此路由訂閱。故在 task-form.component.ts 中實作 OnDestroy
方法,在此取消路由的訂閱。
export class TaskFormComponent implements OnInit, OnDestroy {
routerSubscription: Subscription;
ngOnInit(): void {
this.routerSubscription = this.route.paramMap
.pipe(
map((param) => +param.get('id')),
filter((id) => !!id),
switchMap((id) => this.taskService.get(id)),
tap(() => this.tags.clear()),
tap((task) => this.onAddTag(task.tags.length))
)
.subscribe((task) => this.form.patchValue(task));
}
ngOnDestroy(): void {
this.routerSubscription.unsubscribe();
}
}
若需求需要針對多個訂閱監控的取消,上面程式會多出不少的 Subscription
屬性,此時可以利用 RxJs 的 takeUntil()
運算方法來減化程式。
export class TaskFormComponent implements OnInit, OnDestroy {
stop$ = new Subject<void>();
ngOnInit(): void {
this.route.paramMap
.pipe(
map((param) => +param.get('id')),
filter((id) => !!id),
switchMap((id) => this.taskService.get(id)),
tap(() => this.tags.clear()),
tap((task) => this.onAddTag(task.tags.length)),
takeUntil(this.stop$)
)
.subscribe((task) => this.form.patchValue(task));
}
ngOnDestroy(): void {
this.stop$.next();
this.stop$.complete();
}
}
這一篇透過路由傳遞待辦事項編號,來實作待辦事項的編輯功能;除此之外,ActivatedRoute 服務元件也提供 queryParamsMap
參數取得問號 (?) 後面的查詢參數,以及 fragment
屬性來取得井號 (#) 後面的錨點參數。