iT邦幫忙

2024 iThome 鐵人賽

0
JavaScript

Signal API in Angular系列 第 40

Day 40 - 使用 Angular 原理圖從裝飾器遷移到函數

  • 分享至 

  • xImage
  •  

input()、output()、viewChild()、viewChildren()、contentChild()、contentChildren()、outputFromObservable()和outputToObservable()在Angular 19中處於穩定狀態。 應用程式可以開始使用 Angular 原理圖從裝飾器轉換為訊號。

Angular 19 將訊號輸入 (signal input)、輸出 (output) 和查詢 (queries) 遷移合併為單一訊號遷移 (signal migration)。開發人員可以打開終端,執行 schematic,然後從文字選單中選擇一個或所有遷移。

讓我們在這篇文章中逐步進行遷移。

您可以在此 feature branch 中找到 decorators 版本:
https://github.com/railsstudent/ithome2024-demos/tree/refactor/day40-decorators/projects/day40-migration-schematic-demo

範例 1:遷移到訊號輸入 (Signal Input) 和輸出 (Output)

@Component({
 selector: 'app-some',
 standalone: true,
 imports: [FormsModule],
 template: `
   <div>
     <p>bgColor: {{ bgColor }}</p>
     <p>name: {{ name }}</p>
   </div>
 `,
})
export class SomeComponent {
 @Input({ required: true, alias: 'backgroundColor'}) bgColor!: string;
 @Input({ transform: (x: string) => x.toLocaleUpperCase() }) name: string = 'input decorator';
}

SomeComponent 元件使用 Input 裝飾器接收來自父元件的輸入。 bgColor 是必要的輸入,具有別名backgroundColorname 是將傳入值轉換為大寫的輸入。

export class SomeComponent {
 @Output() triple = new EventEmitter<number>();
 @Output('cube') powerXBy3 = new EventEmitter<number>();

 numSub = new BehaviorSubject<number>(2);
 @Output() double = this.numSub.pipe(map((n) => n * 2));
}

同一組件還具有三個輸出裝飾器 (Output decorator)。 triple 是一個發出數字的事件發射器。 powerXBy3 也是一個帶有別名 cube 的數字事件發射器。 double 使用 numSub BehaviorSubject 的值,乘以二,並將結果傳送給父元件。

在終端機中運行 Angular 訊號 schematic 以遷移程式碼。

將輸入裝飾器 (decorator) 移轉到訊號輸入 (signal inputs)

ng g @angular/core:signals

在選擇選單中,選擇所有遷移並繼續下一步。

訊號輸入遷移後,

readonly bgColor = input.required<string>({ alias: "backgroundColor" }); 
readonly name = input<string, string>('input decorator', 
{ transform: (x: string) => x.toLocaleUpperCase() });

對於 bgColor,使用 alias 選項呼叫 input.required 函數。 對於 name,使用初始值和 transform選項來呼叫輸入以將輸入文字大寫。 遷移對父元件沒有影響。

@Component({
 selector: 'app-some',
 standalone: true,
 imports: [FormsModule],
 template: `
   <div>
     <p>bgColor: {{ bgColor() }}</p>
     <p>name: {{ name() }}</p>
 `,
})
export class SomeComponent {}

bgColorname 是訊號輸入; HTML 模板呼叫訊號函數來顯示它們的值。

輸出遷移後,

readonly triple = output<number>();
readonly powerXBy3 = output<number>({ alias: 'cube' });

numSub = new BehaviorSubject<number>(2); 
double = outputFromObservable(this.numSub.pipe(map((n) => n * 2)));

triple 是一個輸出函數,產出數字。 powerX3 是一個輸出函數,其第一個參數為 alias 選項。別名的值為 cube,與之前相同。 double 使用 outputFromObservable 函數將 Observable 轉換為 output。 遷移對父元件也沒有影響。

將 OutputRef 轉換為輸出 (Output)

num = signal(2);
double = output<number>();

updateNum(value: number) {
   this.num.set(value);
   this.double.emit(value * 2);
} 

numSub BehaviorSubject 轉換為 num 訊號,double OutputRef 轉換為輸出函數。我定義了一個 updateNum 方法來覆寫 num 訊號並將該值傳送到 double 自訂事件。

<div>
   Num: <input type="number" [ngModel]="n" (ngModelChange)="updateNum($event)" />
</div>

新值不會傳遞給 numSub Subject,而是傳遞給 updateNum 方法。

範例 2:遷移查詢裝飾器 (Queries Decorators)

export class QueriesComponent implements AfterContentInit {
 @ContentChild('header') header!: ElementRef<HTMLDivElement>;
 @ContentChildren('p') body!: QueryList<ElementRef<HTMLParagraphElement>>;
 
 appendHeader = '';
 list = '';

 ngAfterContentInit(): void {
     this.appendHeader = `${this.header.nativeElement.textContent} Appended`;
     this.list = this.body.map((p) => p.nativeElement.textContent).join('---');
 }
}

QueriesComponent 元件會套用 ContentChildContentChildren 裝飾器 (decorators) 來查詢 HTML 元素,進而投影到 <ng-content> 元素的。

export class AppComponent implements AfterViewInit {
 @ViewChild(QueriesComponent) queries!: QueriesComponent;
 @ViewChildren('a') aComponents!: QueriesComponent[];

 viewChildName = '';
 numAComponents = 0;

 ngAfterViewInit(): void {
    this.viewChildName = this.queries.name; 
    this.numAComponents = this.aComponents.length;
 }
}

AppComponent 元件使用 ViewChild 裝飾器來查詢第一次出現的 QueriesComponent 。 它使用 ViewChildren 裝飾器來查詢與模板變數 a 相符的所有 QueriesComponent 元件。

將查詢裝飾器 (Query Decorators) 移轉到查詢函數 (Query functions)

查詢遷移後,

export class QueriesComponent implements AfterContentInit {
 readonly header = contentChild.required<ElementRef<HTMLDivElement>>('header');
 readonly body = contentChildren<ElementRef<HTMLParagraphElement>>('p');

 appendHeader = '';
 list = '';

 ngAfterContentInit(): void {
   this.appendHeader = `${this.header().nativeElement.textContent} Appended`;
   this.list = this.body().map((p) => p.nativeElement.textContent).join('---');
 }
}
<div>Appendheader: {{ appendHeader() }}</div>
<div>List: {{ list() }}</div>

此原理圖將 ContentChild 裝飾器遷移到具有正確類型的 contentChild 函數。 同樣,原理圖將 ContentChildren 裝飾器遷移到 contentChildren 函數。
contentChildcontentChildren 函數傳回一個訊號;因此,ngAfterContentInit 生命週期方法中的程式碼也被修改。

將變數轉換為計算訊號

appendHeader = computed(() => `${this.header().nativeElement.textContent} Appended`);
list = computed(() => this.body().map((p) => p.nativeElement.textContent).join('---'));

此元件刪除了 AfterContentInit 介面和 ngAfterContentInit 方法。 appendHeaderlist 被轉換為計算訊號 (computed signals)。 HTML 模板呼叫函數來顯示 appenderHeaderlist 的值。

export class AppComponent implements AfterViewInit {
  readonly queries = viewChild.required(QueriesComponent);
  readonly aComponents = viewChildren('a');

  viewChildName = '';
  numAComponents = 0;
  
  ngAfterViewInit(): void {
    this.viewChildName = this.queries().name; 
    this.numAComponents = this.aComponents().length;
  }
}

此原理圖將 ViewChild 裝飾器遷移到具有正確類型的 viewChild 函數。同樣,原理圖將 ViewChildren 裝飾器遷移到 viewChildren 函數。viewChildviewChildren 函數傳回一個訊號;因此,ngAfterViewInit生命週期方法中的程式碼也被修改。

將變數轉換為計算訊號

viewChildName = computed(() => this.queries().name);
numAComponents = computed(() => this.aComponents().length);
<p>ViewChildName: {{ viewChildName() }}</p>
<p>numAComponents: {{ numAComponents() }}</p>

該元件刪除了 AfterViewInit 介面和 ngAfterViewInit 方法。 viewChildNamenumAComponents 被轉換為計算訊號 (computed signals)。 最後,HTML 模板呼叫函數來顯示 viewChildNamenumAComponents 的值。。

參考:


上一篇
Day 39 - 使用 Angular 19 中的 Resource API 進行資料檢索
下一篇
Day 41 - 使用 AfterRenderEffect 生命週期鉤子進行反應式 DOM 讀寫
系列文
Signal API in Angular41
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言