iT邦幫忙

2024 iThome 鐵人賽

DAY 19
0
JavaScript

Signal API in Angular系列 第 19

Day 19 - viewChild函數的高階使用案例(二)- 將NgTemplate嵌入到ViewContainerRef中

  • 分享至 

  • xImage
  •  

viewChild 函數的另一個高階用例是將 NgTemplate 嵌入到 ViewConatinerRef 中。當範本非常簡單到擁有一個組件就顯得有些過分時,我們可以透過程式設計方式附加一個 NgTemplate 而不是組件。嵌入 NgTemplate 的工作類似於建立動態組件。

我將重複第 18 天完成的示範,但這一次,App 組件附加 NgTemplate 而不是 StarWarCharacterComponent。此示範使用 viewChild 函數來查詢 ViewContainerRef 並呼叫 createEmbeddedView 方法。 createEmbeddedView 方法接受 TemplateRef 和選擇性的 template context。

引導應用程式

import { provideExperimentalZonelessChangeDetection } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';

export const appConfig = {
 providers: [
   provideHttpClient(),
   provideExperimentalZonelessChangeDetection()
 ]
}
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app.config';

bootstrapApplication(App, appConfig);

提供 Http client 和 experiental zoneless feature,並引導應用程式設定。

新增獲取 StarWar 資料的函數

import { catchError, map, of, mergeMap, forkJoin } from 'rxjs';
import { inject, runInInjectionContext, Injector } from '@angular/core';
import { HttpClient } from '@angular/common/http';

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

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

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

getPerson 函數透過 id 檢索星際大戰角色。`

在 App 組件中建立 ngTemplate

<ng-template #starWar let-id let-person="person" let-isSith="isSith">
     <div class="border">
       @if(person) {
         <p>Id: {{ id }} </p>
         @if (isSith) {
           <p>Is a Sith. He is evil.</p>
         }
         <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>
       }
     </div>
</ng-template>

ngTemplate 有一個範本變數 (template variable) starWar、一個隱式變數 (let-id) 和兩個命名變數(let-person 和 let-isSith)。 let-person 綁定到 person 屬性,而 let-isSith 則綁定到 isSith 屬性。 此範本顯示星際大戰角色的 id 和詳細資訊。 如果 isSithtrue,範本將顯示文字 "Is a Sith, He is evil."。

新增 NgContainer 和下拉式清單以程式設計方式渲染組件

Component({
 selector: 'app-root',
 standalone: true,
 imports: [FormsModule],
 template: `
   <div class="container">
     <ng-container #vcr />
   </div>
   <select [(ngModel)]="jediId">
     <option value="1">Luke</option>
     <option value="10">Obi Wan Kenobe</option>
     <option value="20">Yoda</option>
   </select>
   <button (click)="addAJedi(jediId())">Add a Jedi</button>

   <select [(ngModel)]="sithId">
     <option value="4">Darth Vader</option>
     <option value="44">Darth Maul</option>
   </select>
   <button (click)="addAJedi(sithId(), true)">Add a Sith</button>`,
   
   <ng-template #starWar>...</ng-template>`,
})
export class App implements OnDestroy {
 jediId = signal(1);
 sithId = signal(4);

 ngOnDestroy(): void {}
}

應用程式組件由 JediSith 下拉列表組成。 Jedi 列表的 NgModel 綁定到 jediId signal,Sith 列表的 NgModel 綁定到 sithId signal。當使用者按一下 "Add a Jedi" 按鈕時,該元件會呼叫 addAJedi 方法將範本嵌入到 ViewContainerRef 中。同樣,使用者點擊 "Add a Sith" 按鈕來呼叫相同的方法將範本嵌入到 ViewContainerRef 中。

以程式設計方式嵌入 NgTemplate

<ng-container #vcr />

NgContainer 有一個範本變數 vcrviewChild 函數使用它來查詢 ViewContainerRef

vcr = viewChild.required('vcr', { read: ViewContainerRef });

vcr 的類型是 Signal<ViewContainerRef>,因為 read 屬性會擷取 ViewContainerRef

templateRef = viewChild.required('starWar', { read: TemplateRef });

viewChild 函數查詢 starWar 並檢索 TemplateRef

embeddedViewRefs = [] as EmbeddedViewRef<any>[];

async addAJedi(id: number, isSith = false) {
   const person = await lastValueFrom(getPerson(id, this.injector));
   const context = {
     $implicit: id,
     isSith,
     person,
   };
   const embeddedViewRef = this.vcr().createEmbeddedView(this.templateRef(), context);
   this.embeddedViewRefs.push(embeddedViewRef);
 }

addJedi 方法呼叫 getPerson 函數並使用 RxJS 運算子 lastValueFrom 來檢索 Star War 角色。 然後,context 儲存隱式屬性和命名屬性。 createEmbeddedView 方法將 TemplateRef 附加到 ViewContainerRer 並傳回 embeddedViewRef。 將 embeddedViewRef 附加到要在ngDestroy lifecycle hook 中銷毀 embeddedViewRefs 陣列。

ngOnDestroy(): void {
   if (this.embeddedViewRefs) {
     for (const ref of this.embeddedViewRefs) {
       ref.destroy();
     }
   }
}

當應用程式銷毀 App 組件時,ngOnDestroy 會釋放 embeddedViewRefs 的 memory 以避免 memory leak。

結論:

  • viewChild 可以查詢 ViewContainerRef,並且 ViewContainerRef 可以呼叫 createEmbeddedView 方法以程式設計方式附加 ngTemplate
  • createEmbddedView 接受 TemplateRef 和 template context, ngTemplate 可以存取屬性以顯示其值。
  • 如果使用者介面簡單,請使用 NgTemplate。否則,請使用 Angular 組件。

鐵人賽的第 19 天就這樣結束了。

參考:


上一篇
Day 18 - viewChild 函數的高階使用者案例 1 - 以程式設計方式建立 Angular 組件
下一篇
Day 20 - viewChildren函數介紹
系列文
Signal API in Angular36
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言