前言
熟悉 Angular 的朋友都知道,Angular 有非常多的侷限,許多事情它都做不好,打開 Github 一堆 2016 - 2017 的 Issues,時至今日都沒有解決。
原因也很簡單 -- Angular 團隊的不作爲😔。
通常我會把常見的 Angular 的侷限記入在這篇 <<Angular 的侷限和 Github Issues>>,但由於本篇要講的問題篇幅比較大,所以特別把它分一篇出來。
本篇要講的是 Angular Query Element 的侷限。雖然我們已經在 <<Component 組件 の Query Elements>> 文章中,深入理解了 Angular 的 Query 機制。
但是!理解沒有用啊。
如果它本來做的到,但由於我們不理解,所以以爲做不到,那去理解它是對的。
但如果它本來就無能,我們即便理解了,也只是知道它爲什麼做不到,最終任然是做不到啊😔。
本篇,我們一起來看看 Angular 在 "Query Elements" 上有哪些侷限,有沒有什麼方法可以去突破它。
不支持 viewChildren descendants
App Template 裏有一個 Parent 組件
Parent Template 裏有一個 Child 組件
結構
嘗試在 App 組件裏 query Child 組件
export class AppComponent { child = viewChild(ChildComponent); parent = viewChild(ParentComponent); constructor() { afterNextRender(() => { console.log(this.child()); console.log(this.parent()); }) } }
不奇怪,因爲 Angular 也有 Shadow DOM 概念,組件是封閉的,上層無法直接 query 到 ShadowRoot 內的 elements。
by default 不行,OK!我可以接受,但如果我真的想要 query,你總要給我個方法吧?
Layer by layer query
方法是有,只是...
把 viewChild Child 移到 Parent 組件裏
export class ParentComponent { child = viewChild(ChildComponent); }
現在變成 App query Parent query Child 😂
如果有很多層,那每一層組件都需要添加這個 viewChild 邏輯...
雖然我們確實做到了,但這種寫法在管理上是很有問題的。
假設,有 A 到 Z 組件 (一層一層,共 26 層)。
A 想 query Z。
那在管理上,這件事就不可以影響到除了 A 和 Z 以外的人。
但按照上述的方法,B 到 Y 組件都需要添加 viewChild 邏輯才能讓 A query 到 Z,這顯然已經影響到了許多不相干的人,管理直接不及格😡!
原生的 Web Component ShadowRoot 都沒有這麼糟糕
雖然也是一層一層往下 query,但至少 B - Y 組件內不需要增加不相干的代碼。
Use inject instead of query
Angular query child element 有限制,但很神奇的 query parent 卻沒有限制。(這和 ShadowRoot 不一樣,ShadowRoot query parent 和 child 都限制)
於是我們可以嘗試反過來做。
App 組件
export class AppComponent { public child!: ChildComponent; constructor() { afterNextRender(() => { console.log(this.child); }) } }
準備一個空的 child 屬性
接着在 Child 組件 inject App,然後填入 Child 實例。
export class ChildComponent { constructor() { const app = inject(AppComponent); app.child = this; } }
這樣 App 組件就 "query" 到 Child 了。
間中,Parent 組件完全不需要配合做任何事情。這個方案,管理...過👍
上難度 の 動態組件
看似我們好像是突破了 query 的侷限,但其實不然...☹️
假設 Child 組件要支持動態插入。
首先,我們需要把 children 改成 signal (這樣可以監聽變化)
export class AppComponent { public children = signal<ChildComponent[]>([]); constructor() { effect(() => console.log(this.children())); // 監聽變化 } }
接着
export class ChildComponent { constructor() { // 初始化時添加 const app = inject(AppComponent); app.children.set([...app.children(), this]); // destroy 時刪除 const destroyRef = inject(DestroyRef); destroyRef.onDestroy(() => app.children.set(app.children().filter(c => c === this))) } }
嗯...只是改一改,方案視乎還可以,沒有翻車...😨
上難度 の maintain sequence
假設,Parent 組件裏有 3 個 Child 組件 (代號 A, B, C)
@if (show()) { <app-child /> } <app-child /> <app-child />
第一次 console.log 結果是 B, C,因爲 A 被 @if hide 起來了。
接着我們 show = true。
第二次的 console.log 結果是 B, C, A。
注意不是 A, B, C 而是 B, C, A。
因爲在 Child 初始化的時候,Child instance 是被 "push" 到 query results 裏。
這就導致了 sequence 不一致。
爲什麼用 push?換成 unshift, splice 可以嗎?
可以,但是沒有用,因爲 Child 不可能知道自己的位置,所以它也無法確定要用 push, unshift 還是 splice。
是的...這個方案翻車了😢,這種時候,我們還得用回 layer by layer query 方案...
好,這個問題我們先隔着,繼續往下看看其它 Query Elements 的問題。
不支持 viewChildren <ng-container /> 和 <ng-content />
相關 Github Issue:
Github – ContentChildren doesn't get children created with NgTemplateOutlet (2017)
App Template
<ng-template #template> <app-child /> </ng-template> <ng-container #container />
一個 ng-template 和一個 <ng-container />
App 組件
export class AppComponent implements OnInit { readonly templateRef = viewChild.required('template', { read: TemplateRef }); readonly viewContainerRef = viewChild.required('container', { read: ViewContainerRef }); readonly child = viewChild(ChildComponent); constructor() { // 2. 查看是否可以 query 到 Child 組件 afterNextRender(() => console.log(this.child())) } ngOnInit() { // 1. create <ng-template> and insert to <ng-container /> this.viewContainerRef().createEmbeddedView(this.templateRef()); } }
App 組件成功 query 到 Child 組件。
好,我們改一改結構
App Template
<app-parent> <ng-template #template> <app-child /> </ng-template> </app-parent>
把 ng-template transclude 給 Parent 組件
Parent Template
<p>parent works!</p> <ng-container #container />
Parent 組件
export class ParentComponent implements AfterContentInit { readonly templateRef = contentChild.required(TemplateRef); readonly viewContainerRef = viewChild.required('container', { read: ViewContainerRef }); readonly child = viewChild(ChildComponent); constructor() { // 2. 查看是否可以 query 到 Child 組件 afterNextRender(() => console.log('Parent query Child succeeded?', this.child())); // undefined } ngAfterContentInit() { // 1. create <ng-template> and insert to <ng-container /> this.viewContainerRef().createEmbeddedView(this.templateRef()); } }
Parent 組件無法 query 到 Child 組件。
但是 App 組件任然可以 query 到 Child 組件。
Why?!
因爲 Angular 的 query 機制是 based on declare 的地方,而不是 insert 的方法。
我們可以 query <ng-template>,但是不可以 query <ng-container />。
我們經常有一種錯誤,覺得 <ng-container /> 可以被 query。
<ng-container *ngIf="true"> <app-child /> </ng-container>
這是因爲被 *ngIf,*ngFor 誤導了。
上面 *ngIf 只是語法糖,它的正確解讀是
<ng-template [ngIf]="true"> <ng-container> <app-child /> </ng-container> </ng-template>
我們之所以能 query 到 Child 是因爲,它在 ng-template 裏面,
另外上面這段代碼裏, <ng-container /> 只是一個擺設而已,正真作爲 ViewContainerRef 的 element 也是 ng-template (任何一種 element 都可以成爲 ViewContainerRef,不一定是 <ng-container />,<ng-template> 也可以的)。
除了 <ng-container /> 不能被 query,<ng-content /> 也不能被 query。
viewChildren ng-template 順序的問題
相關 Github Issue – QueryList not sorted according to the actual state
我在 <<Angular 高級教程 – 學以致用>> 文章中詳細講解過這個問題了,這裏不再複述。
How to solve?
<app-parent> <ng-template #template> <app-child /> </ng-template> </app-parent>
上面這種情況,雖然 Parent 無法 viewChild Child 組件,但是可以 contentChild Child 組件。
另外 Child 也可以 inject 到 Parent,所以也可以像上一 part 那樣 use inject instead of query。
但這些方式也都有侷限。假如 ng-template 被丟到千里之外的 <ng-container /> 那 contentChild 和 inject 都可能連不上它們,這樣就真的無計可施了。
Direct DOM query 方案
即便是 ShadowRoot,我也可以一層一層的往裏面進。