ContentChild & ContentChildren trong Angular
- 25-12-2023
- Toanngo92
- 0 Comments
Mục lục
Query single directive/component trong Angular
Chúng ta đã xem xét việc sử dụng Tab Component cùng với một component Counter để đếm số lượng component được khởi tạo. Bất ngờ thay, trong ví dụ dưới đây:
<app-bs-tab-group>
<app-tab-panel title="Tab 1">
content tab 1
<app-counter></app-counter>
</app-tab-panel>
<app-tab-panel title="Tab 2">
content tab 2
<app-counter></app-counter>
</app-tab-panel>
<app-tab-panel title="Tab 3">
content tab 3
<app-counter></app-counter>
</app-tab-panel>
</app-bs-tab-group>
<app-counter></app-counter>
Chúng ta hy vọng chỉ có một instance của counter, nhưng thực tế lại có đến 4 instances, chỉ có một instance được hiển thị. Khi có nhiều tab với các component phức tạp, và muốn chúng được khởi tạo chỉ khi cần thiết, chúng ta có cách nào không?
Có một giải pháp để TabPanelComponent nhận content và render nó khi cần. Chúng ta có thể sử dụng ContentChild để truy xuất một TemplateRef. Đầu tiên, chúng ta tạo một directive:
import { Directive } from "@angular/core";
@Directive({
selector: "ng-template[tabPanelContent]",
})
export class TabPanelContentDirective {
constructor() {}
}
Directive này sẽ thêm các tính năng lên một phần tử, trong trường hợp này là một thẻ ng-template có kèm theo attribute [tabPanelContent].
Tiếp theo, chúng ta sử dụng ContentChild để lấy directive đó:
export class TabPanelComponent implements OnInit, OnDestroy {
@ContentChild(TabPanelContentDirective, { static: true, read: TemplateRef })
explicitBody: TemplateRef<unknown>;
constructor(private tabGroup: TabGroupComponent) {}
ngOnInit() {
this.tabGroup.addTabPanel(this);
}
ngOnDestroy() {
this.tabGroup.removeTabPanel(this);
}
}
Nhưng nếu chúng ta đặt debugger hoặc console.log, sẽ thấy nó trả về một instance của TabPanelContentDirective. Làm thế nào để chúng ta có thể lấy được TemplateRef instance?
Cách đơn giản là thay đổi cách đọc một element:
@ContentChild(TabPanelContentDirective, {static: true, read: TemplateRef}) explicitBody: TemplateRef<unknown>;
Bây giờ chúng ta chỉ cần thêm chút logic cho Tab component:
export class TabPanelComponent implements OnInit, OnDestroy {
@ViewChild(TemplateRef, { static: true }) implicitBody: TemplateRef<unknown>;
@ContentChild(TabPanelContentDirective, { static: true, read: TemplateRef })
explicitBody: TemplateRef<unknown>;
get panelBody(): TemplateRef<unknown> {
return this.explicitBody || this.implicitBody;
}
}
Với cách này, chúng ta chỉ có 2 instances được khởi tạo. Lưu ý rằng với các lazy initialize như vậy, mỗi khi active một tab, TemplateRef sẽ được tạo lại một lần.
Query multiple content with ContentChildren
ContentChildren
là một cách để lấy các thành phần con (components, directives hoặc elements) mà chúng ta đã chèn vào trong một component cha thông qua cách sử dụng các thẻ template hoặc structural directives như ng-content.
Ví dụ, trong TabGroupComponent
, nếu chúng ta có nhiều TabPanelComponent
được chèn vào như sau:
<app-tab-group>
<app-tab-panel></app-tab-panel>
<app-tab-panel></app-tab-panel>
<app-tab-panel></app-tab-panel>
</app-tab-group>
Chúng ta có thể sử dụng @ContentChildren
để lấy danh sách các TabPanelComponent
này trong TabGroupComponent
:
import { Component, ContentChildren, QueryList, AfterContentInit } from '@angular/core';
import { TabPanelComponent } from 'đường dẫn tới TabPanelComponent';
@Component({
selector: 'app-tab-group',
template: `
<ng-content></ng-content>
`
})
export class TabGroupComponent implements AfterContentInit {
@ContentChildren(TabPanelComponent) tabPanels: QueryList<TabPanelComponent>;
ngAfterContentInit() {
// Ở đây, tabPanels chứa danh sách các TabPanelComponent được chèn vào từ template của TabGroupComponent
console.log(this.tabPanels);
}
}
Với đoạn mã trên, khi TabGroupComponent
được khởi tạo và các TabPanelComponent
được chèn vào bên trong nó, biến tabPanels sẽ chứa danh sách các TabPanelComponent
này. Điều này giúp chúng ta quản lý và tương tác với các thành phần con một cách thuận tiện hơn.
Listen to changes event trong Angular
ContentChildren
là một cách để chúng ta lắng nghe và phản ứng khi có sự thay đổi về các thành phần con được chèn vào trong một component cha. Trong Angular, khi chúng ta sử dụng ContentChildren
để lấy các thành phần con, chúng sẽ được khởi tạo trước khi ngAfterContentInit
được gọi. Điều này tạo điều kiện tốt để chúng ta thực hiện các thao tác cần thiết khi các thành phần con thay đổi.
Ví dụ, trong TabGroupComponent
, chúng ta muốn cập nhật tab được chọn khi có sự thay đổi về danh sách các tab:
import { Component, ContentChildren, QueryList, AfterContentInit, Output, EventEmitter } from '@angular/core';
import { TabPanelComponent } from 'đường dẫn tới TabPanelComponent';
@Component({
selector: 'app-tab-group',
template: `
<ng-content></ng-content>
`
})
export class TabGroupComponent implements AfterContentInit {
@Input() tabActiveIndex = 0;
@Output() tabActiveChange = new EventEmitter<number>();
@ContentChildren(TabPanelComponent) tabPanelList: QueryList<TabPanelComponent>;
ngAfterContentInit() {
this.tabPanelList.changes.subscribe(() => {
if (this.tabPanelList.length <= this.tabActiveIndex) {
this.selectItem(0);
}
});
}
selectItem(idx: number) {
this.tabActiveIndex = idx;
this.tabActiveChange.emit(idx);
}
}
Ở đoạn mã trên, khi danh sách các tabPanelList
thay đổi (ví dụ: khi thêm hoặc xoá một TabPanelComponent
), phương thức ngAfterContentInit
sẽ lắng nghe sự kiện changes của tabPanelList và kiểm tra xem tabActiveIndex
có hợp lệ không. Nếu không, nó sẽ chuyển đến tab đầu tiên. Điều này giúp chúng ta duy trì sự đồng bộ giữa các tabs và tab được chọn.
Content và View trong Angular
View và Content trong Angular có thể gây hiểu lầm về phần nào là phần hiển thị trực tiếp của component và phần nào là những gì được chèn vào từ bên ngoài. Để làm rõ điều này:
- View: Đây là phần mẫu (template) mà component quản lý trực tiếp và hiển thị ra. Nó bao gồm tất cả những gì bạn định nghĩa cho component đó trong templateUrl hoặc template properties của @Component, trừ những phần được chèn vào thông qua ng-content. View có thể coi là “hộp đen” (black box) đối với các component khác và không thể trực tiếp truy cập vào nội dung bên trong.
- Content: Là phần mẫu được chèn vào từ bên ngoài thông qua cặp thẻ mở và đóng của một component hoặc directive. Nó không được component quản lý trực tiếp. Nó thường được gọi là “light DOM”. Điều này đề cập đến nội dung được đưa vào component thông qua ng-content, là những phần tử hoặc dữ liệu được chèn vào component từ bên ngoài mà component không kiểm soát trực tiếp.