iT邦幫忙

2024 iThome 鐵人賽

DAY 6
0
JavaScript

Signal API in Angular系列 第 6

Day 06 - RxJS 與 Signal 互通性 第 2 部分 - toObservable

  • 分享至 

  • xImage
  •  

昨天,我介紹了toSignal函數,它可以將可觀察序列(Observable)轉換成訊號 (Signal)。事實上,我們也可以使用 toObservable 函數將訊號轉換成可觀察序列。

以下是toObservable的一些使用案例:

  • 當範本驅動表單(template-driven form)中的訊號值發生變更時,可以將其轉換成可觀察序列,然後再使用 RxJS 運算子處理資料,最後呼叫 API 獲取數據。
  • 建立番茄鐘計時器,通過發射 X 個整數來計時。

在呼叫 HTTP 請求之前,將範本驅動表單的值轉換成可觀察序列。

以下示例演示了如何基於 HTML 輸入欄位中的 ID 獲取星球大戰角色的資訊。首先,我們使用toObservableid signal轉換成可Observable,然後再使用各種RxJS運算子來獲取角色數據。接著,我們使用 forkJoin函數來獲取角色所出演的電影列表。

export type Person = {
 name: string;
 height: string;
 mass: string;
 hair_color: string;
 skin_color: string;
 eye_color: string;
 gender: string;
 films: string[];
}

// 建立人物類型 (Create a Person type)

export function getPerson(id: number, injector: Injector) {
 return runInInjectionContext(injector, () => {
   const http = inject(HttpClient);
   const URL = 'https://swapi.dev/api/people';
   return http.get<Person>(`${URL}/${id}`).pipe(
     catchError((err) => {
       console.error(err);
       return of(undefined);
     }));
 });
}

定義getPerson函數以根據id檢索 Star War 角色。

export function getFilmTitle(url: string, injector: Injector): Observable<string> {
 return runInInjectionContext(injector, () => {
   const http = inject(HttpClient);
   return http.get<{ title: string }>(url)
     .pipe(
       map(({ title }) => title),
       catchError((err) => {
         console.error(err);
         return of('');
       })
     );
 });
}

定義一個getFilmTitle函數,該函數接受電影URL並呼叫Star War API來檢索電影標題。 然後,我將在元件中匯入這兩個函數來檢索要顯示的角色和電影標題。

export class App {
 id = signal(1);
 injector = inject(Injector);
 person$ = toObservable(this.id)
   .pipe(
     debounceTime(300),
     distinctUntilChanged(),
     filter((v) => v >= 1 && v <= 83),
     switchMap((v) => getPerson(v, this.injector)),
     shareReplay(1),
   );

 films$ = this.person$.pipe(
   map((p) => {
     const films = p ? p.films : [];
     return films.map((url) => getFilmTitle(url, this.injector));
   }),
   concatMap((x) => forkJoin(x)),
 );
}

toObservableid signal轉換為 Observable 並將值傳送到

  • debounceTime 確保300毫秒後沒有變化
  • distinctUntilChanged 並在新id與當前id不同時繼續
  • filter 檢查id是否在 1 到 83 之間
  • switchMap 擷取資料並取消先前未完成的Observable
  • shareReplay 快取結果,最後
  • 將結果分配給person$Observable

film$ Observable 從角色中擷取電影 URL,將取得電影標題的Observables傳遞給forkJoin來取得數值,並使用concatMap展平內部Observables

<div>
     <label for="id">
       <span>Id: </span>
       <input id="id" name="id" type="number" min="1” [(ngModel)]="id"> 
     </label>
   </div>

   <div>
     @if(person$ | async; as person) {
       <p>Name: {{ person.name }}</p>
       <p>Height: {{ person.height }}</p>
       <p>Mass: {{ person.mass }}</p>
       <p>Hair Color: {{ person.hair_color }}</p>
       <p>Skin Color: {{ person.skin_color }}</p>
       <p>Eye Color: {{ person.eye_color }}</p>
       <p>Gender: {{ person.gender }}</p>
     } @else {
       <p>No info</p>
     }

     @if (films$ | async; as films) {
       <p>Movies</p>
       @for(film of films; track film) {
         <ul style="padding-left: 1rem;">
           <li>{{ film }}</li>
         </ul>
       }
     } @else {
       <p>No movie</p>
     }
   </div>

此範本使用async pipe解析Observables並顯示結果。

將範本驅動的表單值組合到Observable中以建立番茄計時器

function buildTimerString(currentSeconds: number) {
 const secondsInHour = 3600;
 const secondsInMinute = 60;
 const hours = Math.floor(currentSeconds / secondsInHour);     
 const minutes = Math.floor((currentSeconds - hours * secondsInHour) / secondsInMinute);     
 const seconds = currentSeconds - hours * secondsInHour - minutes * secondsInMinute;
 const padHours = hours < 10 ? `0${hours}` : `${hours}`;
 const padMinutes = minutes < 10 ? `0${minutes}` : `${minutes}`;
 const padSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`;
 return `${padHours}:${padMinutes}:${padSeconds}`;
}

buildTimerString是一個實用程式函數,它接受整數並建立<hours>:<months>:<seconds>格式的字串。 例如,10秒錶示為"00:00:10",4000 秒錶示為"01:06:40"。

export class App {
 amount = signal(60);
 unit = signal('1')
 totalSeconds = computed(() => this.amount() * parseInt(this.unit(), 10));
 timer$ = toObservable(this.totalSeconds).pipe(
   debounceTime(300),
   switchMap((x) => timer(0, 1000).pipe(
     map((y) => x - y),
     take(x + 1),
   )),
   shareReplay(1),
 );
 timerString$ = this.timer$.pipe(map((x) => buildTimerString(x)));
}

類似地,toObservabletotalSeconds signal轉換為Observable並將值傳送到switchMap取消先前的計時器並建構一個從x + 1開始一直到0的新計時器。
shareReplay快取最後的結果。

timerString$將總秒數對應到計時器字串,並將其顯示在範本中。

<div>
     <div>
       <label for="id">
         <span>Id: </span>
         <input id="id" name="id" type="number" min="1" [(ngModel)]="amount"> 
       </label> 
       <label for="unit">
         <span>Unit: </span>
         <select [(ngModel)]="unit">
             <option value="1">seconds</option>
             <option value="60">minutes</option>
             <option value="3600">hours</option>
         </select>
       </label>
     </div>
</div>
<p>{{ timerString$ | async }}</p> 

amountunit signal double binded到ngModel,以便對HTML控制項的任何變更都會寫回它們。
當任何signal更新時,Angular都會重新計算等於總秒數的totalSeconds signal。
timer$timerString$ Observables追蹤totalSeconds signal,並在範本中顯示新的計時器字串。

結論:

  • toObservable可以將signal轉換為Observable
  • toSignal類似,toObservable必須出現在injection context中,例如constructor, field initializationsfactory function。當toObservableinjection context之外調用時,必須提供injector
    *toObservable可以使用pipe向其他RxJS運算子(operators)發出值,以產生結果。

鐵人挑戰賽的第6天就這樣結束了。

參考:

Stackblitz Demos:


上一篇
Day 05 - Observable 與 Signal 的互通性 第 1 部分 - toSignal
下一篇
Day 07 - ngXtension中的 toObservableSignal - 天使與魔鬼的結合體
系列文
Signal API in Angular39
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
mattchen0512
iT邦新手 5 級 ‧ 2024-08-20 11:22:43

可改寫成

如此就可替換成
unit = signal('1') => unit=signal(1) ,後續計算也不用特地 parseInt

option [ngValue]="1">seconds
option [ngValue]="60">minutes
option [ngValue]="3600">hours
(被吃字,補上)

Thank you. I don't know ngValue.

謝謝。 我不知道 ngValue。

我要留言

立即登入留言