iT邦幫忙

2024 iThome 鐵人賽

0
JavaScript

Signal API in Angular系列 第 33

Day 33 - 將 manual injector 傳遞給 toSignal 函數

  • 分享至 

  • xImage
  •  

Required signal inputs 不能在 constructorfield initializers 使用,因為當時該值不可用。為了存取該值,我的解決方案是在 effect 中觀察 signal input 的實際變化,向伺服器發出 HTTP 請求並設定 signal 的值。關於不使用 effect 的討論有很多,我必須找到其他解決方案來刪除它。

Required signal inputs 可在 ngOnInitngOnChanges 生命週期方法中存取。但是,toSignal會在其中引發錯誤,因為它們位於context injection` 之外。它可以透過兩種方式修復:

  • manual injector 傳遞給 toSignal 函數
  • runInInjectionContext 的回呼函數中 (callback function) 執行 toSignal 函數。

在 effect 中使用 signal input(稍後變更)

import { Component, effect, inject, Injector, input, signal } from '@angular/core';
import { getPerson, Person } from './star-war.api';
import { StarWarPersonComponent } from './star-war-person.component';

@Component({
 selector: 'app-star-war',
 standalone: true,
 imports: [StarWarPersonComponent],
 template: `
     <p>Jedi Id: {{ jedi() }}</p> 
     <app-star-war-person [person]="fighter()" kind="Jedi Fighter" />`,
})
export class StarWarComponent {
 // required signal input
 jedi = input.required<number>();

 injector = inject(Injector);
 fighter = signal<Person | undefined>(undefined);
 
 constructor() {
  effect((OnCleanup) => {
     const sub = getPerson(this.jedi(), this.injector)
       .subscribe((result) => this.fighter.set(result));

     OnCleanup(() => sub.unsubscribe());
   });
 }
}

程式碼更改如下:

  • 建立一個 StarWarService 來呼叫 API 並回傳 Observable。
  • StarWarComponent 實作 OnInit 介面。
  • 使用 inject 函數注入元件的 Injector
  • ngOnInit 中,透過 required signal input 呼叫 StarWar API 並從 Observable 建立signal。將 manual injector 傳遞給 toSignal 函數以避免錯誤。
  • ngOnInit 中,runInInjectionContext 函數呼叫 toSignal 函數。

建立 StarWarService

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, tap } from "rxjs";
import { Person } from "./person.type";

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

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

 getData(id: number): Observable<Person | undefined> {
   return this.http.get<Person>(`${URL}/${id}`).pipe(
     tap((data) => console.log('data', data)),
     catchError((err) => {
       console.error(err);
       return of(undefined);
     }));
 }
}

建立一個帶有 getData 方法的 StarWarService 來呼叫 StarWar API 來檢索人員。結果是 Observableundefined

Required Signal Input

import { Component, input } from '@angular/core';

@Component({
 selector: 'app-star-war',
 standalone: true,
 template: `
  <p>Jedi Id: {{ jedi() }}</p>
  <p>Sith Id: {{ sith() }}</p>
 `,
})
export class StarWarComponent implements OnInit {
 // required signal input
 jedi = input.required<number>();

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

 ngOnInit(): void {}
}

jedisith 都是 required signal inputs;因此,我無法在 constructor 中使用它們或使用服務 (service) 呼叫 toSignal 來初始化欄位 (field initializers)。

我實作了 OnInit 介面並在 ngOnInit 方法中存取兩個 required signal inputs

準備 App 元件

import { Component, VERSION } from '@angular/core';
import { StarWarComponent } from './star-war.component';

@Component({
 selector: 'app-root',
 standalone: true,
 imports: [StarWarComponent],
 template: `
   <app-star-war [jedi]="1" [sith]="4" />
   <app-star-war [jedi]="10" [sith]="44" />`,
})
export class App {}

App 元件有兩個 StarWarComponent 實例。 第一個實例的 jedi id 為 1,第二個實例的 jedi id 為 10。 實例的 sith id 分別為 4 和 44。

將 manual injector 傳遞給 toSignal 來查詢絕地武士

export class StarWarComponent implements OnInit {
 // required signal input
 jedi = input.required<number>();

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

StarWarComponent 元件中,我注入 StarWarService 和元件的 Injector。此外,我聲明了一個 light Signal 來儲存 toSignal 函數傳回的結果。

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

ToSignalOptions 選項有一個 injector,當在 context injection 之外使用 toSignal 函數時,我可以將元件的 Injector 傳遞給該選項。

export class StarWarComponent implements OnInit {
 // required signal input
 jedi = input.required<number>();

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

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

ngOnInit 方法中,我呼叫服務 (service) 來取得 Observable,並使用 toSignal 函數建立 signal。第二個參數是元件的 Injector 的選項。

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

接下來,我將 light signal 傳遞給 StarWarPersonComponent 元件以顯示絕地戰士的詳細資訊。

runInInjectionContext 在元件的 Injector 中執行 toSignal

export class StarWarComponent implements OnInit {
 // required signal input
 sith = input.required<number>();

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

 ngOnInit(): void {
   // this also works
   runInInjectionContext(this.injector, () => {
     this.evil = toSignal(this.starWarService.getData(this.sith()));
   })
 }
}

我聲明了一個 evil 的 Signal 來儲存 toSignal 函數傳回的結果。 runInInjectionContext 的第一個參數是元件的 Injector。第二個參數是一個回呼函數 (callback function),它執行 toSignal 函數並將 person 指派給 evil 變數。

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

接下來,我將 evil signal 傳遞給 StarWarPersonComponent 元件以顯示西斯尊主的詳細資料。

如果元件有 required signal inputs,我可以存取 ngOnInitngOnChanges 中的值來發出 HTTP 請求或其他操作。 然後,我不需要創建 effect 來觀看所需的 required signal inputs。

結論:

  • Required signal input 無法在 constructor 中調用,因為此時該值不可用。
  • Required signal input 可以在 ngOnInitngOnChanges 方法中使用。
  • toSignalngOnInitngOnChanges 方法中拋出錯誤,因為它在 injection context 之外運行。
  • 將 manual injector 傳遞給 ToSignalOptionsinjector 選項。
  • runInInjectionContext 函數的 callback 中呼叫 toSignal 函數。

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

參考:


上一篇
Day 32 - 在 Angular 中從後端檢索資料的不同方法
下一篇
Day 34 - 在 toSignal 函數中使用 requireSync 選項 令 Observable 發出同步值
系列文
Signal API in Angular36
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言