iT邦幫忙

2024 iThome 鐵人賽

0
JavaScript

Signal API in Angular系列 第 34

Day 34 - 在 toSignal 函數中使用 requireSync 選項 令 Observable 發出同步值

  • 分享至 

  • xImage
  •  

toSignal 函數的回傳類型為 Signal<T | undefined>Observable 是惰性的,當事件發生時發出第一個值。因此,在 Observable 發出第一個值之前,signalundefined。 如果 toSignal 函數希望 Observable 同步發出,例如 BehaviourSubjectstartWith,它可以為第二個參數提供 requireSync: true 選項。

在這篇文章中,我將展示 requireSync 選項的兩個用例。

用例

  • HttpClient 透過 id 查詢一個人,startWith 運算子提供一個初始值。
  • Angular 元件具有單擊時更新 BehaviorSubject 值的按鈕。

使用 RxJS startWith 發出初始值

export type Person = {
 name: string;
 height: string;
 mass: string;
 hair_color: string;
 skin_color: string;
 eye_color: string;
 gender: string;
 films: string[];
}
import { HttpClient } from "@angular/common/http";
import { inject, Injectable } from "@angular/core";
import { catchError, Observable, of, startWith } from "rxjs";
import { Person } from "./person.type";

const URL = 'https://swapi.dev/api/people';

const DEFAULT: Person = {
 name: '',
 height: '',
 mass: '',
 hair_color: '',
 skin_color: '',
 eye_color:  '',
 gender: '',
 films: [],
};

@Injectable({
 providedIn: 'root'
})
export class StarWarService {
 private readonly http = inject(HttpClient);

 getData(id: number): Observable<Person> {
   return this.http.get<Person>(`${URL}/${id}`).pipe(
     startWith(DEFAULT)
     catchError((err) => {
       console.error(err);
       return of(DEFAULT);
     }));
 }
}

StarWarService 建立一個帶有 getData 方法的來呼叫 StarWar API 來檢索人員。 HttpClient 將結果傳送給傳回初始值的 startWith 運算子。 因此,此方法的傳回類型為 Observable<Person>

將 requireSync 選項傳遞給 toSignal

import { ChangeDetectionStrategy, Component, inject, Injector, input, OnChanges, Signal } from '@angular/core';
import { StarWarService } from './star-war.service';
import { toSignal } from '@angular/core/rxjs-interop';
import { StarWarPersonComponent } from './star-war-person.component';
import { Person } from './person.type';

@Component({
 selector: 'app-star-war',
 standalone: true,
 imports: [NgStyle, StarWarPersonComponent],
 template: `
   <h3>Star War Jedi vs Sith</h3>
    <app-star-war-person [person]="light()" />
    <app-star-war-person [person]="evil()" />
   </div>
 `,
})
export class StarWarComponent implements OnChanges {
 // required signal input
 jedi = input.required<number>();

 // required signal input
 sith = input.required<number>();

 starWarService = inject(StarWarService);
 injector = inject(Injector);
 light!: Signal<Person>;
 evil!: Signal<Person>;

 ngOnChanges(): void {}
}

StarWarComponent 元件中,我注入 StarWarService 和元件的 injector。此外,我聲明了 lightevil 的 signals 來儲存從 toSignal 函數傳回的結果。觀察到 signal 刪除了類型中的 undefined

interface ToSignalOptions<T> {
 initialValue?: unknown;
 requireSync?: boolean;
 injector?: Injector;
 manualCleanup?: boolean;
 rejectErrors?: boolean;
 equal?: ValueEqualityFn<T>;
}

ToSignalOptions 選項有一個 requireSync 屬性,我用它來確保 Observables 在訂閱時發出值。

export class StarWarComponent implements OnChanges {
 … same as before …

 ngOnChanges(): void {
   this.light = toSignal(this.starWarService.getData(this.jedi()), {
     injector: this.injector,
     requireSync: true,
   });

   this.evil = toSignal(this.starWarService.getData(this.sith()), {
     injector: this.injector,
     requireSync: true
   });
 }
}

ngOnChanges 方法中,我呼叫 service 來取得 Observables,並使用 toSignal 函數建立 signal。第二個參數是元件的 injector 和 requireSync 的選項。

<app-star-war-person [person]="light()" kind="Jedi Fighter" />
<app-star-war-person [person]="evil()" kind="Sith Lord" />

接下來,我將 lightevil signals 傳遞給 StarWarPersonComponent 元件,以顯示絕地武士和西斯領主的詳細資訊。

在 toSignal 中使用 BehaviorSubject

import { Route } from '@angular/router';

export const routes: Route[] = [
 {
   path: 'requireSync-example',
   loadComponent: () => import('./require-sync/example.component'),
   data: {
     btnValues: [-5, -3, 1, 2, 4]
   }
 },
];

routes array中, requireSync-example 路由的路由資料是一個數字數組。

export const appConfig = {
 providers: [
   provideRouter(routes, withComponentInputBinding()),
 ]
}

appConfig 中,provideRouter 函數的 withComponentInputBinding 功能將路由資料綁定到ExampleComponent元件的 required signal input。

@Component({
 selector: 'app-requireSync-example',
 standalone: true,
 template: `
   <div>
     @for (v of btnValues(); track v) {
       <button (click)="update(v)">{{ v }}</button>
     }
   </div>
   <div>
     <p>total: {{ total() }}</p>
     <p>source: {{ source.getValue() }}</p>
     <p>sum: {{ sum() }}</p>
   </div>
   <button (click)="changeArray()">Update the BehaviorSubject</button>
 `,
})
export default class ExampleComponent {
 btnValues = input.required<number[]>();
 something = new BehaviorSubject(0);
 total = toSignal(this.something, { requireSync: true });

 source = new BehaviorSubject([1,2,3,4,5]);
 sum = toSignal(
   this.source.pipe(map((values) => values.reduce((acc, v) => acc + v, 0))), { requireSync: true });

 update(v: number) {
   this.something.next(this.something.getValue() + v);
 }

 changeArray() {
   const values = this.source.getValue().length <= 5 ? [11,12,13,14,15,16,17,18] : [1,2,3,4,5];
   this.source.next(values);
 }
}

Something 是初始值為 0 的 BehaviorSubjecttoSignal 函數會從中建立一個 signal。 requireSync 選項是可能的,因為 BehaviorSubject 在被訂閱時可以立即發出一個值。點選時,按鈕會呼叫 update 方法來更新 BehaviorSubject。 HTML 範本在收到新值時顯示 total 的值。

Source 是另一個儲存數字 array 的 BehaviorSubject。 然後,BehaviorSubject 傳送給 map運算子來計算總和。 toSignal 函數和 requireSync: true 斷言事件流 (event stream) 在訂閱時會發出總和。 點選按鈕會執行 changeArray 方法來變更 source BehaviorSubject。由於 total signal 訂閱事件流 ,因此範本呈現來 sourcetotal 的新值。

結論:

  • requireSync 斷言 Observable 在訂閱後立即發出一個值。
  • ObservableBehaviorSubject 或由諸如 startWithof 之類的的 RxJS 運算子產生值,我們可以將 requireSync 選項傳遞給 toSignal 函數。
  • 如果 toSignalrequireSync: true 但 Observable 沒有立即發出值,則會拋出錯誤。

鐵人賽的第 34 天到此結束。

參考:


上一篇
Day 33 - 將 manual injector 傳遞給 toSignal 函數
下一篇
Day 35 - 使用rejectErrors選項更改toSignal的錯誤處理行為
系列文
Signal API in Angular36
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言