第 27 型 - 路由 (Router) - 参数传递

上一篇利用 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 属性来取得井号 (#) 後面的锚点参数。


<<:  [Day27] 实战 - 撰写均线金三角交叉的策略

>>:  [Kata] Clojure - Day 28

lejos ev3 (ev3 使用 java 完整攻略) - jerry Tsai

Ev3 是 LEGO® MINDSTORMS® 乐高公司发展的可程序机器人,有原生的专用语法,且可使...

[Day7] Flutter - 堆叠布局 ( Stack、Positioned )

前言 Hi, 我是鱼板伯爵今天要教大家 Stack(堆叠) 和 Positioned(位子),Sta...

Day-8 Divide-and-Conquer-3 : 二分搜寻法, 费波那契数列, Strassen’s演算法

二分搜寻法(Binary Search) 前提,在一个已经排序完成的A阵列中 Divide : 元素...

[Day11] Tableau 轻松学 - Workbook/Worksheet/Dashboard/Story

前言 档案架构是在开发前应该要先了解的事,可以让我们在对的地方做对的事情,以节省宝贵的时间。主要有四...

Day.20 Course Schedule

Leetcode #207. Course Schedule 题目给你一系列的课程,每一个门课都有它...