iT邦幫忙

2024 iThome 鐵人賽

DAY 22
0
JavaScript

Signal API in Angular系列 第 22

Day 22 - contentChildren 函數介紹

  • 分享至 

  • xImage
  •  

今天,我將介紹 contentChildren,它是 @ContentChild 裝飾器 (decorator) 的訊號對應部分。

ContentChildren 裝飾器和 contentChildren 函數的區別

  • ContentChildren 裝飾器傳回 ReadT[],而 contentChildren 函數傳回 `Signal<readonly ReadT[]>。
  • ContentChildren 裝飾器 (decorator) 的 static 屬性從 contentChild 函數中刪除。

在下面的例子中,我展示了 contentChildren 如何透過範本變數 (template variables)、ngTemplates 和 Angular 組件查詢元素

contentChildren 的生命週期

<app-query-by-type>
     <ng-template let-now><p>Custom Header 3, now = {{ now }}</p></ng-template>
     <ng-template let-now>
       <p>Custom Body 3</p>
       <p>now = {{ now }}</p>
     </ng-template>
</app-query-by-type>
templates = contentChildren(TemplateRef);

constructor() {
   console.log('constructor', this.templates());
}

ngAfterContentInit(): void {
   console.log('ngAfterContentInit', this.templates());
}

contentChildren 函數在 OnInit hook執行後可用。 在 constructor 中,元素的數量為0。在ngOnInitngContentInit 中,TemplateRef 的數量為2。

在以下例子中,我展示了 viewChildren 如何透過範本變數 (template variables)、指令 (directives) 和 Angular 元素進行查詢。

例子 1:透過模板變數查詢元素

import { Component, contentChild, effect, ElementRef, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
 selector: 'app-query-by-variable',
 standalone: true,
 imports: [FormsModule],
 template: `
   <div class="container">
     <h3>Query ContentChildren by variables</h3>
     <label for="color">
       <span>Color: </span>
       <select id="color" name="color" [(ngModel)]="color">
         <option value="">----</option>
         <option value="red">red</option>
         <option value="yellow">yellow</option>
         <option value="cyan">cyan</option>
         <option value="goldenrod">goldenrod</option>
         <option value="pink">pink</option>
       </select>
     </label>
     <ng-content></ng-content>
   </div>
 `,
})
export class AppQueryByVariableComponent {
 color = signal('');
}

AppQueryByVaraibleComponent 組件具有用於頁首、頁尾和正文的 <ng-content /> 元素。 它還具有一個下拉式選單來選擇元素的背景顏色。

<app-query-by-variable>
     <div #divs class="projection">Custom Header 2</div>
     <div #divs class="projection">Custom Body</div>
     <div #divs class="projection">
       <ul>
         <li>Custom Footer</li>
         <li>Custom Footer 2</li>
       </ul>
     </div>
</app-query-by-variable>

App 組件中,div 元素 被投影到 <app-query-by-variable> 的預設 <ng-content>。 div 元素具有相同的範本變數 #div,我們可以使用 contentChildren 函數來查詢所有 <div> 元素。

divs = contentChildren('divs', { read: ElementRef });

contentChildren 函數透過範本變數 (template variables) 查詢元素。 第二個參數 { read: ElementRef } 期望每個項目都是 ElementRef

constructor() {
   effect(() => {
     this.divs().forEach((div) =>
       div.nativeElement.style.backgroundColor = this.color()
     );
   });
}

color signal 更新時,effect 會執行邏輯來變更背景顏色的 CSS 樣式。 contentChildren 函數傳回一個 ElementRef array,它可以是一個 empty array。迭代 (iterate) 該 array 以更新 div 元素的背景顏色。

例子 2:透過 contentChildren 查詢 ngTemplates

@Directive({
 selector: '[someDirective]',
 standalone: true,
})
export class AppSomeDirective {
 templates = contentChildren(TemplateRef);
}

我們可以寫一個 AppSomeDirective 指令 (directive),使用 contentChildren 來查詢投影的 ngTemplates。

@Component({
 selector: 'app-query-by-type',
 standalone: true,
 imports: [AppSomeDirective, NgTemplateOutlet, FormsModule],
 template: `
   <div class="container">
     <h3>Query ContentChildren by TemplateRef</h3>
     <div someDirective>
       <ng-template>
         <p>Projected item</p>
         <p>Projected item 2</p>
         <p>Projected Item 3</p>
       </ng-template>

       <ng-template>
         <p>Template 2</p>
       </ng-template>
     </div>
     @for (template of directive().templates(); track $index) {
       <ng-container *ngTemplateOutlet="template"></ng-container>
     }
    
     <label for="index">
       <span>Template: </span>
       <select id="index" name="index" [(ngModel)]="index" style="margin-right: 0.25rem;">
         <option value="">----</option>
         @for (x of templates(); track $index) {
           <option [value]="$index">{{ $index + 1 }}</option>
         }
       </select>
     </label>
     <button (click)="changeTemplate()">Change template</button>
     <ng-container *ngTemplateOutlet="dynamicTemplate() || defaultTemplate"></ng-container>
     <ng-template #defaultTemplate>
       <p>Choose a template from the dropdown.</p>
     </ng-template>
   </div>
 `,
})
export class AppQueryByDirectiveComponent {
 index = signal('');
 
 directive = viewChild.required(AppSomeDirective);
}

div 元素有一個 someDirective 屬性;因此,指令 (directive) 的 contentChildren 可以檢索兩個 ngTemplate。此組件也可以查詢投影在 <app-query-by-type> 標記內的 ngTemplates。

@for (template of directive().templates(); track $index) {
    <ng-container *ngTemplateOutlet="template">
</ng-container>
}

AppQueryByDirectiveComponent 組件使用 viewChild 函數來查詢指令 (directive) 並存取其 contentChildren。該指令的 ngTemplates 被指派給 ngTemplateOutlet 指令來渲染動態內容。

<label for="index">
       <span>Template: </span>
       <select id="index" name="index" [(ngModel)]="index" style="margin-right: 0.25rem;">
         <option value="">----</option>
         @for (x of templates(); track $index) {
           <option [value]="$index">{{ $index + 1 }}</option>
         }
       </select>
</label>
<button (click)="changeTemplate()">Change template</button>
<ng-container *ngTemplateOutlet="dynamicTemplate() || defaultTemplate"></ng-container>
<ng-template #defaultTemplate>
   <p>Choose a template from the dropdown.</p>
</ng-template>
export class AppQueryByDirectiveComponent {
 index = signal('');
 templates = contentChildren(TemplateRef);
 dynamicTemplate = signal<TemplateRef<any> | undefined>(undefined);

 changeTemplate() {
   const idx = this.index();
   const t = idx ? this.templates()[+idx] : undefined;

   this.dynamicTemplate.set(t);
 }
}

AppQueryByDirectiveComponent 組件使用 contentChildren 函數來查詢預設 <ng-content> 中存在的 TemplateRef。當使用者按一下該按鈕時,將執行 changeTemplate 以確定要顯示的範本。 當所選索引為空字串時,將顯示預設範本。否則,組件將顯示 <app-query-by-type> 標記中投影的範本。

<app-query-by-type>
     <ng-template><p>Custom Header 3</p></ng-template>
     <ng-template><p>Custom Body 3</p></ng-template>
</app-query-by-type>

App 組件中,ngTemplates 被投影到 <app-query-by-type> 中,以便 contentChildren 函數可以查詢它們並將它們指派給 templates signal。

例子 3:按類型查詢 Angular 組件

import { Component, ChangeDetectionStrategy, signal, computed } from '@angular/core';

const imgURL = 'https://picsum.photos/300/200';

@Component({
 selector: 'app-photo',
 standalone: true,
 template: `
   <div class="photo">
     <img [src]="img()" alt="Random picture" />
   </div>
 `,
})
export default class AppPhotoComponent {
 #random = signal(Date.now());

 img = computed(() => `${imgURL}?random=${this.#random()}`) 

 loadImage() {
   this.#random.set(Date.now());
 }
}

AppPhotoComponent 組件具有附加到圖像 URL 的random seed signal。 當 loadImage 函數更新signal value 時,img computed signal會產生一個新的圖片 URL。

import { Component, ChangeDetectionStrategy, contentChildren, signal } from '@angular/core';
import AppPhotoComponent from './photo.component';
import { FormsModule } from '@angular/forms';

@Component({
 selector: 'app-photo-wrapper',
 standalone: true,
 imports: [FormsModule],
 template: `
   <div class="photo-wrapper">
     <div class="photos">
       <ng-content />
     </div>
     <div>
       <label for="index">
         <span>Photo: </span>
         <select id="index" name="index" [(ngModel)]="index">
           <option value="">----</option>
           @for (x of photos(); track $index) {
             <option [value]="$index">{{ $index + 1 }}</option>
           }
         </select>
       </label>
       <button (click)="changeImage()">Change photo</button>
     </div>
   </div>
 `,
})
export default class AppPhotoWrapperComponent {
 index = signal('');
 photos = contentChildren(AppPhotoComponent);

 changeImage() {
   const strIdx = this.index();
   if (strIdx) {
     const index = +this.index();
     this.photos()[index].loadImage();
   }
 }
}

AppPhotoWrapperComponent 組件包含一個預設的 <ng-content>,我們可以用它來投影 AppPhotoComponent 組件。此組件使用 contentChidren 函數來查詢所有 AppPhotoComponent 組件。下拉清單使用 ngModel 將值綁定到 index signal。當使用者點擊該按鈕時,changeImage 方法將呼叫 loadImage 方法來顯示新圖片。

<app-photo-wrapper>
     <app-photo class="photo" />
     <app-photo class="photo" />
     <app-photo class="photo" />
 </app-photo-wrapper>

<app-photo-wrapper> tag 有三個 <app-photo> tags,AppPhotoComponent 組件被投影到<ng-content>contentChildren 函式成功查詢 AppPhotoWrapperComponent 中的 AppPhotoComponent

結論:

  • contentChildren 可以查詢元素、TemplateRef 和組件。第一個參數是一個 selector,它是範本變數 (template variables) 或類型。
  • read 屬性指定 contentChildren 傳回的元素類型。
  • contentChildren 函數傳回 signal 中的元素 array。 當函數找不到匹配項時,它會傳回一個 empty array,而不是錯誤。
  • contentChildren 在 OnInit hook 後可用。

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

參考:


上一篇
Day 21 - contentChild 函數介紹
下一篇
Day 23 - output 函數介紹
系列文
Signal API in Angular36
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言