Angular 17+ 高級教程 – Angular 的侷限 の Query Elements

前言

熟悉 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());
    })
  }
}
View Code

不奇怪,因爲 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)

Github – Accessing @ViewChild and @ViewChildren of a template declared in a different component than the one where it is host (2021)

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());
  }
}
View Code

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());
  }
}
View Code

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,我也可以一層一層的往裏面進。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章