Angular 17+ 高級教程 – Component 組件 の ng-template


前言

Angular 的動態組件博大精深, 沒有認真學一下的話, 在開發中經常會掉坑裏. 所以這篇大家要認真看一下哦.

 

參考

angular2 學習筆記 ( Dynamic Component 動態組件) 早年我寫的文章

Angular 學習筆記 (動態組件 & Material Overlay & Dialog 分析) 早年我寫的文章

Ivy’s internal data structures Angular 創始人寫的 TView, LView, RView 詳解

 

ng-template & ng-container

原生 DOM 要搞動態輸出 Element, 有兩大招. 第一是用 template, 第二是用 createElement

我們先來看看 template 的例子

原生 DOM template

<body>
  <template>
    <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Cum, et!</p>
  </template>
  <h1>Hello World</h1>
</body>

把 template 內容複製然後 append to body

const template = document.querySelector<HTMLTemplateElement>('template')!;
const node1 = template.content.cloneNode(true);
const node2 = template.content.cloneNode(true);
document.body.appendChild(node1);
document.body.appendChild(node2);

兩個步驟

1. clone template

2. append

Angular ng-template & ng-container

Angular 也是借鑑了原生 DOM template 的做法. 只是它需要有 MVVM 的概念, 所以魔改了一些, 有讀過我這 3 篇的應該就可以悟到了. 這篇這篇這篇.

<ng-template #template>
  <p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Soluta, asperiores!</p>
</ng-template>

<button (click)="append()">append</button>

<ng-container #container></ng-container>

首先 Angular 沒有用原生的 template, 它用的是自定義的 tag <ng-template>. 

另外還有一個自定義的 tag 是 <ng-container>, 你可以把它當成一個卡位的 element. 

待會我們要 clone ng-template 然後 append 到 ng-container 的位置.

ng-template & ng-container as a comment

如果這時我們打開渲染好的 HTML, 會發現 ng-template 和 ng-container 最終被 compile 成了 2 行註釋而已

所有 Component 的 HTML 都會被 compile 成 JS, ng-template 的內容也不例外. 而 ng-container 也只是爲了卡位, 所以它也不是一個 element.

clone & append

接下來看看組件代碼

export class TestDynamicComponent {
  @ViewChild('template')
  template!: TemplateRef<any>;

  @ViewChild('container', { read: ViewContainerRef })
  container!: ViewContainerRef;

  append(): void {
    var view = this.template.createEmbeddedView(null);
    this.container.insert(view);
  }
}

撇開 query element, 最重要的兩句是

var view = this.template.createEmbeddedView(null);
this.container.insert(view);

第一句負責 clone template

第二局負責 append

效果

注意看哦, append 的位置是在 ng-container comment 的上方. 

append, prepend, remove

container.insert 有一個參數 index

如果想 prepend 的話, 可以設置 index = 0

想指定插入某行數, index = specifyNumber 就可以了

默認什麼都不填, 它會插入最後一行, 也就是 append 的效果.

想 remove 之前插入的 element

this.container.remove(0); // 刪除第 0 個 element
this.container.clear(); // 清空

element as ng-container 

注意, 這個是一個坑哦, 沒搞清楚很容易掉下去的.

<div #container></div>

我把 ng-container 換成了一個具體的 div element

直覺告訴我們, template 會被 append 到 div 裏面. 但其實 Angular compile 出來是這樣的

div 依然存在, 但是 div 下方多出了一個 container comment.

依據上面的規則, append 的 element 會在 container comment 的上方. 所以最終效果是

內容都 append 到 div 和 container comment 的中間了 (也就是 div 的下方)

記住, 最終的位置是在 div 的下方或 comment 的上方.

component as ng-container

component 本身也可以作爲 ng-container

直接注入 ViewContainerRef 就行了.

和 element as comment 一樣, compile 後 container comment 在 component 的下方

所以最終 element 被 append 到 component 和 container comment 中間

小結

到這裏, 我們學會了如何用 Angular 替代原生的 define template, clone template, query/append/prepend/insertBefore

接下來我們看看 ng-template 配上 MVVM 的強大能力.

 

ng-template & MVVM

單純的 clone and append 當然不是 template 真正誕生的意義. 我們可以把一個 template 當作一個函數, 它是最終 HTML 的工廠.

它封裝了大部分的內容, 同時必須允許調用者通過類似參數的方式去修改每一次生產出的最終內容。這纔是一個合格的 template 用法.

在原生的 DOM template, 我們一般上是 clone 了 template 以後, 直接做 DOM manipulation 來達到最終效果. 

比如下面這樣

這種開發體驗...一言難盡...

幸好 Angular 替我們彌補了這些缺失. 作爲 MVVM, Angular 還是有點稱職的.

Define and passing parameter to template (TemplateContext)

<ng-template #template>
  <p>Hi {{ name }}</p>
</ng-template>

模板封裝了 Hi, 而調用者需要傳入 name 變量.

首先, Angular 把 template 所需的 parameters 交由一個對象負責, 我們把它稱爲 TemplateContext 對象.

爲了正規一點, 我就定義一個 interface for 這個 template 唄

interface TemplateContext {
  name: string
}

上面這個 interface 聲明瞭 template 傳遞的參數有一個 name, 類型是 string

在定義 TemplateRef 時, 它有一個泛型. 這個就是給我們傳入 TemplateContext 類型的. (上面一開始的例子中, 我們放的是 any)

@ViewChild('template')
template!: TemplateRef<TemplateContext>;

然後, 在 createEmbededView 時, 我們把參數傳進去. 也就是傳入 TemplateContext 對象咯 (上面一開始的例子中, 我們放的是 null)

var view = this.template.createEmbeddedView({ name: 'Derrick' });

最後回到 template 定義參數

<ng-template #template let-name="name">
  <p>Hi {{ name }}</p>
</ng-template>

let-name="name" 的意思是, declare 一個 variable name, 它的值來自於 TemplateContext 對象中的 name 屬性.

這裏是一個 mapping 機制. variable name 不一定要完全等價於 property name. 我們可以自由 mapping.

比如極端一點的, 我們甚至可以提供一個 path. 它也會 mapping 成功.

<ng-template #template let-name="person.name" let-age="people[0].age">
  <p>Hi {{ name }}, I am {{ age }} years old</p>
</ng-template>

TemplateContext 是這樣

interface TemplateContext {
  person: { name: string };
  people: [{ age: number }];
}

最終效果

template 閉包變量

既然 template 類似於函數, 那它自然有閉包的概念

value 不是一個 parameter. 但它不會報錯. 因爲它來自當前組件的 property.

類似於這樣的結構

function Componet() {
  const value = 'component value';

  function Template(name: string) {
    return `hi ${name}, this is component value: ${value}`;
  }
}

 

Too many paramters 和 $implicit 的使用

每當參數過多的時候, 我們通常會把它們 collect 起來變成一個對象.

<ng-template #template let-firstName="firstName" let-lastName="lastName" let-fullName="fullName" let-age="age" let-salary="salary">

這樣一堆重複的 let- 就很煩, 很多餘.

我們可以改成

<ng-template #template let-person="person">
interface TemplateContext {
  person: { firstName: string, lastName: string, fullName: string, age: number, salary: number };
}

另外 let-person="person" 有時候也顯得很重複很多餘. 所以 Angular 特地設計了一個 $implicit (它的意思是含蓄)

把 TemplateContext 改成

interface TemplateContext {
  $implicit: { firstName: string, lastName: string, fullName: string, age: number, salary: number };
}

我們就可以去掉 ="person" 了

<ng-template #template let-person>
  <p>{{ person.firstName }}</p>
</ng-template>

 

Typed ng-template Variable

喜歡 TypeScript 的朋友可能已經注意到了, 上面我們定義的 TemplateContext 只在 Component 內起作用.

在 template 中, Angular 並沒有推導出類型.

在 ng-template 裏 person 是 any

這當然不是我們期望的. 不過也可以理解, 要從 @ViewChild TemplateRef 中提取類型反射到 let-person 對 Angular 來講視乎還是太難了.

那有沒有辦法可以讓我們去聲明類型呢? 即便 Angular 不智能, 那也應該給條路讓我們手動去配置吧.

還是有的...至少 DX 不太好而已. 參考 : Docs – Improving template type checking for custom directives

首先聲明一個指令

@Directive({
  selector: 'ng-template[myTemplate]',
  standalone: true,
})
export class MyTemplateDirective {
  static ngTemplateContextGuard(_dir: MyTemplateDirective, ctx: unknown): ctx is TemplateContext {
    return true;
  }
}

指令裏面帶有一個 static 方法. 關鍵就在 ctx is TemplateContext 這一句.

這個是 TypeScript 的 Type Guards 語法, 不熟悉的可以看這篇.

記得哦, 一定要 static, 方法名字一定要是 ngTemplateContextGuard, return type 一定要是 ctx is YourTemplateContextType

這個是 Angular 的潛規則.

定義好指令後, 但凡匹配 template[myTemplate] selector 的 ng-template 都會 apply 到

至此 IDE 就可以推導出正確類型了.

Shared TemplateContextType Directive

爲每一個 template 定義一個指令很煩, 很重複. 我們可以通過泛型來做一些調整 (雖然效果也沒有真的很好 /.\)

首先把所有 hardcode 的 TemplateContext type 換成泛型 T

然後添加一個 input 屬性. (因爲最終還是要有一個類型聲明, 不然 Angular 怎麼推到呢?)

<ng-template #template [myTemplate]="templateContextType" let-person>
  <p>{{ person.name }}</p>
</ng-template>

在 ng-template 傳入一個值, 讓它做類型推導.

注意, 這裏不要搞混哦. templateContextType 在 JS 角度看, 它只是一個 undefined value 而已.

但在 TypeScript 的角度它是一個 TemplateContext 類型. 而 Angular 只是需要它的 Type 而已. 所以最後傳入 undefined value 是沒有問題的.

Angular 是用 type declare 來推導, 而不是 value.

 

Structural Directives

Structural Directives 專門指那些用來處理 element 結構的指令.

這些指令通常和 ng-template, ng-container 有很密切的關係. 我們來看一個具體的例子.

有一個 toggle button, 點擊以後下方會 append 出一個 card, 再點擊 card 會被 remove 掉.

注: 這不是通過 CSS display:none 實現的哦, 這個是 template + append 實現的.

我們上面已經學過了 ng-template, createEmbeddedView, insert. 要實現這個效果挺簡單的.

<button (click)="toggle()">Toggle</button>
<ng-template #template>
  <div class="card">
    <h1>Hello World</h1>
    <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Minus, minima.</p>
  </div>
</ng-template>

由於 template 和 append 的位置相同, 所以這裏可以省略掉 ng-container. 因爲任何 element 都可以被當成 ng-container. ng-template 自然也可以.

組件代碼是這樣的

export class TestDynamicComponent {
  @ViewChild('template')
  template!: TemplateRef<any>;

  @ViewChild('template', { read: ViewContainerRef })
  container!: ViewContainerRef;

  toggle(): void {
    if (this.container.length === 0) {
      const view = this.template.createEmbeddedView(null);
      this.container.insert(view);
    } else {
      this.container.clear();
    }
  }
}

toggle 時判斷當前 container 是否有內容就知道要 append 還是 remove 了.

封裝成 Structural Directives

難題來了. 如果我想把 component 內的代碼做封裝, 該怎麼弄呢?

上面全部都要封裝起來.

這時就需要使用指令了.

ng g d toggle

把組件的代碼搬到指令內

export class ToggleDirective {
  template = inject(TemplateRef);
  container = inject(ViewContainerRef);

  public toggle(): void {
    if (this.container.length === 0) {
      const view = this.template.createEmbeddedView(null);
      this.container.insert(view);
    } else {
      this.container.clear();
    }
  }
}

有做了一些微調整, 組件用的是 @ViewChild, 指令則用 inject, 因爲它兩位置不同嘛. query 的方式自然也就不一樣了.

然後把指令 apply 到 ng-template 上.

接下來是 toggle. 我們把 toggle 方法也搬到了指令內. 所以外面需要調用到指令內的 toggle 方法. 

這時就需要利用 template variable 了.

直覺告訴我們這樣就可以了. 但卻報錯了...

當 Template Variable 遇上指令

原因是 #templateVariable 只能選出一個對象. 而這裏默認選出的對象是 TemplateRef 而不是 ToggleDirective.

我們需要特意聲明才能準確的拿到 ToggleDirective 對象.

先在指令 metadata 中加入 exportAs, 這個用來表示指令的 export 名稱.

@Directive({
  selector: '[appToggle]',
  standalone: true,
  exportAs: 'appToggle'
})

然後把 template variable 改成 #appToggle="exportAsValue"

這樣就拿到 ToggleDirective 對象可以調用 toggle 方法了.

效果

和之前的一摸一樣, 指令完美封裝.

 

結構型指令微語法 和 Angular 內置指令 (Structural directive syntax and ngIf, ngFor, ngSwitch) 

我們透過 Angular build-in 的幾個指令, 來學習一下結構型指令微語法

Angular 提供了一些常用的結構指令給我們. 它們是 ngIf, ngFor, ngSwitch

對應 JS 就是 if, for of, switch 咯. 真的非常非常的 common.

ngIf

我們先來看看 ngIf

<button (click)="show = !show">toggle</button>
<div *ngIf="show" class="card">
  <h1>Title</h1>
  <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse, odio.</p>
</div>

從語法中, 可以大致上猜到, 當 show = true, card 被 append, 反之被 remove. 

提問 1 : ng-template 哪去了?

提問 2 : * 是啥?

其實這個是 Angular 的結構指令微語法 (Structural directive syntax reference)

因爲許多時候 ng-template 就像一個多餘的 wrapper, 還有它經常搭配 指令, @Input, let- 這些 attributes. 搞得看上去很亂, 沒有邏輯.

於是 Angular 就搞了一些微語法來優化, 美觀一下調用, 提升 DX.

*ngIf="show" 會被視爲

<ng-template [ngIf]="show">
  <div class="card">
    <h1>Title</h1>
    <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse, odio.</p>
  </div>
</ng-template>

ngIf 是一個指令, 同時它有一個 @Input 接受 show 變量. 當 onChanges 時內部會負責 createEmbeddedView, insert, remove.

ngIf 還可以搭配 'as' 和 'else' 微語法, 比較複雜一些, 我們先來看看 ngFor, 後面再繼續介紹

ngFor

<div *ngFor="let value of ['a', 'b', 'c']; let index = index" class="card">
  <h1>{{ index }}</h1>
  <p>{{ value }}</p>
</div>

從語法上看, 可以推測出, card 會被 append 3 次, 因爲 array ['a', 'b', 'c'] 有 3 給值.

每一次的 createEmbeddedView 會傳入不同的 TemplateContext.

比如第一次應該是 { $implicit : 'a', index : 0 } 

第二次是 { $implicit: 'b', index : 1 }

而 ng-template 最終的長相是

<ng-template ngFor [ngForOf]="['a', 'b', 'c']" let-value let-index="index">
  <div class="card">
    <h1>{{ index }}</h1>
    <p>{{ value }}</p>
  </div>
</ng-template>

* 變成 ng-template

ngFor 是指令

let value 變成了 let-value

let index = index 變成了 let-index="index"

of ['a', 'b', 'c'] 變成了 [ngForOf]="['a', 'b', 'c']". ngForOf 也是指令, 同時是 @Input

從上面我們大致可以看出 Angular 微語法的模式了

主要就是 ng-template, 指令, @Input, let- 這 4 點.

ngForTrackBy

<div *ngFor="let person of people; let index = index; trackBy: trackByIdFn" class="card">
  <h1>{{ index }}</h1>
  <h1>{{ person.id }}</h1>
  <p>{{ person.name }}</p>
</div>

組件代碼

people: Person[] = [
  { id: 1, name: 'Derrick' },
  { id: 2, name: 'Stefanie' },
];
trackByIdFn: TrackByFunction<Person> = (_index, item) => item.id;

trackBy 是爲了性能優化而設計的. 具體怎麼優化, 這裏我不想擴展. 我只是想給微語法的例子

<ng-template ngFor [ngForOf]="people" [ngForTrackBy]="trackByIdFn" let-person let-index="index">
  <div class="card">
    <h1>{{ index }}</h1>
    <h1>{{ person.id }}</h1>
    <p>{{ person.name }}</p>
  </div>
</ng-template>

ngForTrackBy 是 ngFor 和 ngForOf 指令中的 @Input. 

另外, 微語法的位置是有講究的. 比如開頭一定是 let person

但是後面的部分就沒有那麼講究了. 比如下面這行的寫法都是 ok 的

<div *ngFor="let person of people; let index = index; trackBy: trackByIdFn" class="card"> <!--最 common 寫法-->
<div *ngFor="let person; of people; let index = index; trackBy: trackByIdFn" class="card"></div> <!-- of people 被獨立出來 -->
<div *ngFor="let person; of : people; let index = index; trackBy: trackByIdFn" class="card"></div> <!-- of : people 中間加了分號  -->
<div *ngFor="let person; of people; let index = index; trackBy trackByIdFn" class="card"></div> <!-- of 和 trackBy 都不需要分號 -->
<div *ngFor="let person trackBy trackByIdFn; of people; let index = index;" class="card"></div> <!-- trackBy 和 of 換位子 -->

各種奇葩寫法都是可以接受的...哈哈 (核心就是 @Input, let- 指令, ng-template)

我給一個更極端的例子

<div *="let person; let index = index" class="card">

這句會被視爲

<ng-template let-person let-index="index">
  <div class="card">
    <h1>{{ person.id }}</h1>
  </div>
</ng-template>

沒有指令, 也沒有 @Input, 只有 ng-template 和 let-. 你悟道了嗎?

ngForTemplate

ngForTemplate 是 ngFor 指令的一個 @Input, 它讓我們可以把 template 獨立出來.

<ng-template #template let-person let-index="index">
  <div class="card">
    <h1>{{ index }}</h1>
    <p>{{ person.name }}</p>
  </div>
</ng-template>

<ng-template ngFor [ngForOf]="people" [ngForTrackBy]="trackByIdFn" [ngForTemplate]="template">
</ng-template>

這樣的解耦可以讓管理更加靈活.

ngSwitch

<ng-container [ngSwitch]="'z'">
  <h1 *ngSwitchCase="'a'">a</h1>
  <h1 *ngSwitchCase="'b'">b</h1>
  <h1 *ngSwitchCase="'c'">c</h1>
  <h1 *ngSwitchCase="'d'">d</h1>
  <h1 *ngSwitchDefault="'e'">e</h1>
</ng-container>

應該看得出它的邏輯吧. 我就不多解釋了.

有一個點值得注意

ngSwitch 不是搭配 * 來使用的. 而且它不可以 apply 到 ng-template 上哦.

ngIf advanced

<ng-template #loading>loading...</ng-template>
<div *ngIf="person$ | async as person; else loading" class="card">
  <h1>{{ person.name }}</h1>
  <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Cupiditate, quibusdam!</p>
</div>

Script

person$ = timer(2000).pipe(
  map<number, Person>(() => ({ id: 1, name: 'Derrick' }))
);

效果

首先 append loading html, 當資料來了以後再 append card.

我們分析它的語法

*ngIf="person$ | async as person; else loading"

| async 是 Angular build-in 的 pipe, 主要用於 transform RxJS 的 stream. 

當 stream 還沒有 dispatch 它返回的值是 null. 當 dispatch 以後就是 person 對象.

as syntax

as person 是另一個微語法, 上面的語法 transpile 去 ng-template 是這樣的.

<ng-template [ngIf]="person$ | async" let-person="ngIf" [ngIfElse]="loading">

as 變成了 let-person="ngIf"

"ngIf" 是 TemplateContext 中的一個屬性

這個屬性值裝的就是 person$ | async 後的 person 對象.

由於 $implicit 裝的也是 person 對象. 所以我們省略掉後面, 只寫 let-person 也是可以的.

在一個 ngFor 的例子

<div *ngFor="let person of people$ | async as people" class="card">
  <h1>{{ person.name }}</h1>
  <p>{{ people.length }}</p>
</div>

利用 as people 可以獲取到 | async 後的 people

transpile 去 ng-template 是這樣的

<ng-template ngFor [ngForOf]="people$ | async" let-person let-people="ngForOf">
  <div class="card">
    <h1>{{ person.name }}</h1>
    <p>{{ people.length }}</p>
  </div>
</ng-template>

as people 變成了 let-people="ngForOf"

NgForOf 的 template context 長這樣

$implicit 對應 let-person 它是 person 對象

ngForOf 對應 let-people="ngForOf" 它是 people$ | async 後的 people array

總結

1. 結構型指令負責封裝 createEmbeddedView, insert, remove 邏輯.

2. 微語法只是語法糖, 最終都會 transpile 成 ng-template + 指令 + @Input + let-

 

ngTemplateOutlet

ngTemplateOutlet 是 Angular build-in 的指令, 它是一個簡單的小功能, 我們透過例子學習

<h1>Hi, Derrick</h1>
<p>Lorem ipsum dolor sit amet.</p>
<h1>Hi, Stefanie</h1>
<p>Lorem ipsum dolor sit amet.</p>

上面有 2 set 重複性很高的 element 結構. 有沒有一種簡單快速的方法可以封裝? 

提煉出模板

<ng-template let-name>
  <h1>Hi, {{ name }}</h1>
  <p>Lorem ipsum dolor sit amet.</p>
</ng-template>

只有 name 不同, 所以 name 就是一個 parameter.

那要怎樣使用它呢? ng-container + @ViewChild + createEmbeddedView + TemplateContext + insert ? 太繁瑣了吧...

<ng-container *ngTemplateOutlet="template; context: { $implicit: 'Derrick' }"></ng-container>
<ng-container *ngTemplateOutlet="template; context: { $implicit: 'Stefanie' }"></ng-container>

沒錯 ngTemplateOutlet 就是 Angular 替我們封裝的一系列繁瑣操作.

 

Template & Injector

提問: 假設我有 aa, bb, cc 三個組件

aa 組件內有一個 ng-template, template 內是一個 cc 組件

<ng-template #template>
  <app-cc></app-cc>
</ng-template>

同時 aa 有一個 viewProviders

viewProviders: [{ provide: VALUE_TOKEN, useValue: 'aa value' }]

aa 雖然擁有 template 但它不負責 create 和 append.

反之我們把它交給 bb 組件

<app-aa #aa></app-aa>
<app-bb [template]="aa.template"></app-bb>

bb 組件內有一個 ng-container, 它負責 create 和 append

同時 bb 也有一個 viewProviders 

@Component({
  selector: 'app-bb',
  standalone: true,
  imports: [CommonModule],
  template: `
    <ng-container #container></ng-container>
  `,
  viewProviders: [{ provide: VALUE_TOKEN, useValue: 'bb value' }],
})
export class BbComponent implements AfterViewInit {
  @Input()
  template!: TemplateRef<any>;

  @ViewChild('container', { read: ViewContainerRef })
  container!: ViewContainerRef;

  ngAfterViewInit() {
    const view = this.template.createEmbeddedView(null);
    this.container.insert(view);
  }
}

問: 

cc 組件 inject 的 VALUE_TOKEN 會拿到 aa value 還是 bb value? 

也就是說 cc 組件是 under aa (定義的地方), 還是 under bb (append 的地方)?

答案是 aa 組件 (定義的地方).

不過呢, v14.0 後, Angular 允許我們在 createEmbeddedView 時輸入多一個 injector. (比如輸入 bb 組件的 injector)

這樣 cc 就有了 2 個 injector 可以注入到 aa 和 bb 的 services 了. 

注: bb 後者 injector 優先查找.

 

TView, LView, RView

參考: Medium – Ivy’s internal data structures

Template View (TView)

TView 類似於一個 class. 每一個組件的 html 都是一個 TView. 每一個 ng-template 也是一個 TView.

總是它是一個摸具.

Logical View (LView)

TView 是 class, 那 LView 就是 new 出來的 instance.

比如 aa.component.html 的內容就是 aa 的 TView.

而在 app.component.html 裏.

<app-aa></app-aa>
<app-aa></app-aa>

這樣寫, 我們就 new 了 2 個 aa 的 LView.

從 app 到最底層, 所以 LView 放一起看就是一棵 Logical Tree.

如果沒有 ng-content, ng-template 這種 cut and paste 的 node 操作. Logical Tree 就等同於最終的 HTML node tree.

Logical Tree 最主要的功能就是 inject & query. 當我們 @ViewChild 時, Angular 不是 document.querySelector 

Angular 是依據 Logical Tree 的結果去找的. 

比如上面的例子中, 我在 aa 組件 @ViewChild cc 組件, 一開始是拿不到的. 因爲 cc 在 ng-template 內, 

這時它只是 TView, 還沒有生成任何 LView.

直到 cc 被 bb create and append 以後, aa 組件就可以 @ViewChild 到 cc 了. 

重點!!!

cc 的 LView 是 under aa (定義的地方) 而不是 under bb (append 的地方). 

createLView 第一個參數是 parentLView. 而在 template.createEmbeddedView 裏頭, 它傳入的第一個參數正是 declarationLView (也就是定義的地方)

所以 bb @ViewChild 是拿不到 cc 的. 這個和你是否在 createEmbeddedView 時提供 injector 無關.

多一個 embededViewInjector 只是讓你的 cc 可以 inject bb, 但不能讓 bb @ViewChild 到 cc.

Render View

RView 就是組件最終渲染的地方.

transclude (aka ng-content) 的原理就是先 "渲染" 好了以後, "cut and paste" element 去另一個地方.

這種情況下 LView 和 RView 就會不相同. 

但我們要記住哦, Angular 的 inject, query, change detection 都是依賴 LView 而不是 RView.

 

Dynamic Component

除了 Template, DOM 另一種動態輸出的方式是 createElement.

在 Angular 就是 create component and append 了.

注: Angular 只有動態組件, 沒有動態指令. 即便是在動態創建組件的同時也無法加入指令, 相關 Issue.

我們直接看例子學習唄.

Static Component

<app-aa name="Derrick" (statusChanged)="0" appRedBorder>
  <h1>Hello World</h1>
  <app-bb></app-bb>
</app-aa>

這是一個靜態輸出的 aa 組件 

它有 @Input, @Output, 指令 和 transclude

我們要把它變成動態輸出.

ng-container

<button (click)="append()">append</button>
<ng-template #template>
  <h1>Hello World</h1>
</ng-template>
<ng-container #container></ng-container>

首先把 aa 組件刪了, 換成上面這些代碼.

點擊 button後, 要創建 hello world template, bb 組件, aa 組件, 並且 template 和 bb 要 transclude 到 aa 裏頭, 最後 append 到 ng-container.  (注: 指令沒有辦法動態創建, 這個目前無法實現, 我們忽略它)

create component

先 inject 和 query 需要的材料

export class TestDynamicComponent {
  @ViewChild('container', { read: ViewContainerRef })
  container!: ViewContainerRef;

  @ViewChild('template')
  template!: TemplateRef<any>;

  private environmentInjector = inject(EnvironmentInjector);
  private elementInjector = inject(Injector);

  append(): void {}
}

environmentInjector 就是 global injector,

elementInjector 就是當前組件的 injector. 

我們創建的組件需要鏈接上 Logical Tree, 所以會用到這些.

 

 

 

 

 

 

 

 

 

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