Angular 17+ 高级教程 – Component 组件 の ng-template

发布时间 2024-01-03 21:36:09作者: 兴杰

前言

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, 所以会用到这些.