Today, I will introduce contentChild that is the signal counterpart of @ContentChild decorator.
ContentChild 裝飾器傳回一個實例,而 contentChild 函數傳回一個訊號。ContentChild 裝飾器的 static 屬性從 contentChild 函數中刪除。contentChild 有 2 個變體: conentChild.required() 和 contentChild() 。
contentChild.required() 表示該元素在範本中至少出現一次,並且檢索第一個元素。 此函數的傳回類型是 Signal<T>。contentChild() 表示該元素未出現在範本中且可能是未定義的。函數的傳回類型為 Signal<T|undefined>。在下面的例子中,我展示了 contentChild 如何透過範本變數 (template variables)、ngTemplates 和 Angular 組件查詢元素。
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 ContentChild 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>
</select>
</label>
<ng-content select="[header]">Default Header</ng-content>
<ng-content></ng-content>
<ng-content select="[footer]">Default Footer</ng-content>
</div>
`,
})
export class AppQueryByVariableComponent {
color = signal('');
}
AppQueryByVaraibleComponent 元件有三個 <ng-content /> 組件,分別用於頁首、頁尾和正文。 它還具有一個下拉式選單來選擇元素的背景顏色。
<app-query-by-variable>
<div #header header class="projection">Custom Header 2</div>
<div #body class="projection">Custom Body</div>
<div #footer footer 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 元素的範本變數是 #header,並投影到命名 header 的 ng-content。第二個 div 元素的範本變數是#body,並投影到預設的 ng-content。最後一個 div 元素的範本變數是 #header,並投影到命名 footer 的 ng-content。
header = contentChild('header', { read: ElementRef });
footer = contentChild('footer', { read: ElementRef });
body = contentChild('body', { read: ElementRef });
contentChild 函數透過範本變數查詢元素。 第二個參數 { read: ElementRef } 期望函數會擷取 ElementRef。
constructor() {
effect(() => {
const color = this.color();
const header = this.header()?.nativeElement;
const footer = this.footer()?.nativeElement;
const body = this.body()?.nativeElement;
if (header) {
header.style.backgroundColor = color;
}
if (footer) {
footer.style.backgroundColor = color;
}
if (body) {
body.style.backgroundColor = color;
}
});
}
當 color signal 更新時,effect 會執行邏輯來變更背景顏色的 CSS 樣式。我使用了 contentChild 而不是 contentChild.required;因此, signal 值可能是 undefined 的。每個 if-block 在將顏色指派給 backgroundColor 屬性之前都會比較 signal 是否 defined。
@Directive({
selector: '[someDirective]',
standalone: true,
})
export class AppSomeDirective {
template = contentChild.required(TemplateRef);
}
我們可以寫一個 AppSomeDirective 指令 (directive),使用 contentChild 來查詢投影的 ngTemplate。
@Component({
selector: 'app-query-by-type',
standalone: true,
imports: [AppSomeDirective, NgTemplateOutlet],
template: `
<div class="container">
<h3>Query ContentChild by TemplateRef</h3>
<div someDirective>
<ng-template>
<p>Projected item</p>
<p>Projected item 2</p>
<p>Projected Item 3</p>
</ng-template>
</div>
<ng-content>Default Header</ng-content>
<ng-container *ngTemplateOutlet="template()"></ng-container>
<ng-container *ngTemplateOutlet="directive().template()"></ng-container>
</div>
`,
})
export class AppQueryByDirectiveComponent {}
div 元素有一個 someDirective 屬性;因此,directive 的 contentChild 可以檢索由三個段落元素組成的 ngTemplate。該組件還有一個預設的 <ng-content>,它可以使用 contentChild 查詢另一個 ngTemplate。
<ng-container *ngTemplateOutlet="template()"></ng-container>
<ng-container *ngTemplateOutlet="directive().template()"></ng-container>
ngTemplates 被指派給 ngTemplateOutlet 指令來呈現動態內容。
export class AppQueryByDirectiveComponent {
directive = viewChild.required(AppSomeDirective);
template = contentChild.required(TemplateRef);
}
AppQueryByDirectiveComponent 元件使用 viewChild 函數來查詢指令 (directive) 並存取其 contentChild。 contentChild.required 查詢預設 <ng-content> 中存在的 TemplateRef。
<app-query-by-type>
<ng-template>Custom Header 3</ng-template>
</app-query-by-type>
在 App 元件中,ngTemplate 被投影到 <app-query-by-type> 中,這樣 contentChild.required 函數就不會拋出錯誤。
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 值時,img computed signal 會產生一個新的圖片 URL。
import { Component, ChangeDetectionStrategy, contentChild } from '@angular/core';
import AppPhotoComponent from './photo.component';
@Component({
selector: 'app-photo-wrapper',
standalone: true,
template: `
<div class="photo-wrapper">
<ng-content />
<button (click)="changeImage()">Change photo</button>
</div>
`,
})
export default class AppPhotoWrapperComponent {
photo = contentChild.required(AppPhotoComponent);
changeImage() {
this.photo().loadImage();
}
}
AppPhotoWrapperComponent 組件包含一個預設的 <ng-content>,我們可以投影 AppPhotoComponent 組件。此組件使用 contentChid.required 函數來查詢 AppPhotoComponent 組件。 changeImage 方法呼叫 loadImage 方法來顯示新圖片。
<app-photo-wrapper>
<!-- <app-photo-size size /> -->
<app-photo style="margin-bottom: 0.25rem;" />
</app-photo-wrapper>
在 <app-photo-wrapper> 標籤的內部,有一個 <app-photo> 標籤,並將對應的照片組件投影到<ng-content> 。 contentChild.required 函式成功查詢 AppPhotoWrapperComponent 中的 AppPhotoComponent。
contentChild 可以查詢元素、ngTemplates、指令 (directive) 和組件。第一個參數是一個 selector,它是範本變數 (template variables) 或類型。read 屬性指定 contentChild 要傳回的元素類型contentChild.required 函數。否則,我們應該使用 contentChild 函數,它可以傳回 signal 或 undefined。contentChild 函數的 selector 與範本中的多個元素相符時,傳回第一個。content.required 無法查詢某個元素,則函數會拋出錯誤。鐵人賽的第 21 天就這樣結束了。