iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 27
0
Modern Web

Angular 全集中筆記系列 第 27

第 27 型 - 路由 (Router) - 參數傳遞

  • 分享至 

  • xImage
  •  

上一篇利用 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]);
  }
}

利用 ActivatedRoute 服務元件取得路由變數

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() 方法設定表單值後,頁面能夠正確的顯示。

Result

不過利用 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>

Result

從上圖結果可見,當使用 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));
  }
}

Result

需要注意一點,針對 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 屬性來取得井號 (#) 後面的錨點參數。


上一篇
第 26 型 - 路由 (Router)
下一篇
第 28 型 - 路由 (Router) - Resolve / 延遲載入 (Lazy Router)
系列文
Angular 全集中筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言