前言

在 Attribute Directives 属性型指令 文章中,我们学习过了指令。指令是没有 HTML 和 CSS 的组件,它单纯用于封装 JS 的部分。

这一篇我们将继续学习另一种指令 -- Structural Directive 结构型指令。

就代码而言,Structural Directive 和 Attribute Directives 是完全一样的,只是用途不同,因此进行了区分。

Attribute Directives 通常用于监听事件,修改 class,styles 等等。

而 Structural Directive 则主要用于修改 DOM 结构。

在 Dynamic Componentng-template 文章中,我们学习了如何用 MVVM 的方式动态输出/移除组件和模板,

在本文中,我们将继续利用 Structural Directive 来封装这一过程,使其变得可复用。

ngComponentOutlet

ngComponentOutlet 是 Angular built-in 的 Structural Directive,它的作用就是输出 Dynamic Component。

App Template

<button (click)="showComponent = SayHiComponent">show say hi</button>
<button (click)="showComponent = HelloWorldComponent">show hello world</button> <ng-container [ngComponentOutlet]="showComponent">

有 2 个 Dynamic Component,一个是 SayHi 组件,一个是 HelloWorld 组件。

点击其中一个按钮它就会 createComponent -> insert to <ng-container />。如果 <ng-container /> 已经有组件,它还会先 remove 掉组件再插入新的。

AppComponent

export class AppComponent {
showComponent!: typeof SayHiComponent | typeof HelloWorldComponent; SayHiComponent = SayHiComponent;
HelloWorldComponent = HelloWorldComponent;
}

效果

如果我们自己写代码实现的话,我们要 query template/container,要 create hostView,要 insert to container,要 remove from container,非常的繁琐,用 ngComponentOutlet 就方便多了。

它也支持传入其它的 options,比如 projectableNodes、inputs 等等。

ngTemplateOutlet

ngTemplateOutlet 也是 Angular built-in 的 Structure Directive,它的用途是输出 ng-template。

App Template

<ng-template #template let-name="name">
<h1>Hi, {{ name }}</h1>
</ng-template> <ng-container [ngTemplateOutlet]="template" [ngTemplateOutletContext]="{ name: 'Derrick' }" />
<ng-container [ngTemplateOutlet]="template" [ngTemplateOutletContext]="{ name: 'Alex' }" />
<ng-container [ngTemplateOutlet]="template" [ngTemplateOutletContext]="{ name: 'Richard' }" />

AppComponent 无需任何代码。

效果

底层原理很简单,就是 createEmbeddedView

令人好奇的是,ngTemplateOutletContext 是 @Input,整个对象是可以替换的。

但我们知道在 createEmbededView 时,我们只能传入一次 TemplateContext 对象,之后只能修改这个对象的属性值,无法替换整个对象。

那 ngTemplateOutlet 是如何做到替换对象的呢?难不成每一次替换都 re-createEmbededView?

不,是它用了 Proxy

相关源码在 ng_template_outlet.ts

Control Flow 特别提示

下面教的 NgIf、NgForOf、NgSwtich 指令在 Angular v17 版本后已经被 Control Flow 取代了,Control Flow 下一篇会教。

虽然如此,我觉得作为学习 Angular,这 3 个指令是相当不错的,大家不妨看一看。

Show/Hide Element の NgIf 指令

效果

App Template

<button (click)="toggle()">Toggle</button>
<ng-template #template>
<h1>Hi, Derrick</h1>
</ng-template>

AppComponent

export class AppComponent {
@ViewChild('template')
templateRef!: TemplateRef<void>; @ViewChild('template', { read: ViewContainerRef })
viewContainerRef!: ViewContainerRef; toggle() {
if (this.viewContainerRef.length === 0) {
this.viewContainerRef.createEmbeddedView(this.templateRef);
} else {
this.viewContainerRef.clear();
}
}
}

很简单的代码,但有一个点值得注意,<ng-template> 这个节点也被用作 ViewContainer。

还记得吗?ViewContainer 可以是任何节点,不一定要是 <ng-container />。其它的比如 <div>, <my-component>, <ng-template> 都是可以作为 ViewContainer 的。

接着把它封装成 Structure Directive。

创建 ShowHide 指令

ng g d show-hide

App Template

<button (click)="show = !show">Toggle</button>
<ng-template [appShowHide]="show">
<h1>Hi, Derrick</h1>
</ng-template>

我们用一个 show 属性来表示 show or hide。

ShowHideDirective

@Directive({
selector: '[appShowHide]',
standalone: true,
})
export class ShowHideDirective implements OnChanges {
@Input('appShowHide')
show = false; private templateRef: TemplateRef<void> = inject(TemplateRef);
private viewContainerRef = inject(ViewContainerRef); ngOnChanges(changes: SimpleChanges): void {
if (changes['show'].currentValue) {
this.viewContainerRef.createEmbeddedView(this.templateRef);
} else {
this.viewContainerRef.clear();
}
}
}

3个点:

  1. 利用 @Input 与外部联系,看什么时候要 show 什么时候要 hide

  2. 利用 OnChanges Hook 监听 @input 变化

  3. 利用 inject 获取 TemplateRef 和 ViewContainerRef,之前是在 App 所以是用 @ViewChildren,现在位置换了,所以改成用 inject。

效果

NgIf 指令

show / hide 实在是一个太过普遍的需求了,所以 Angular 也有 built-in 指令 -- ngIf,我们不需要自己封装。

<button (click)="show = !show">Toggle</button>
<ng-template [ngIf]="show">
<h1>Hi, Derrick</h1>
</ng-template>

把指令改成 [ngIf] 就可以了。

另外,我们不需要在 AppComponent imports NgIf 指令哦,因为它已经包含在 CommomModule 里了,至于什么是 Module? 这个在之后的 NgMoudle 章节会教。

ngIfElse

<button (click)="show = !show">Toggle</button>

<ng-template #elseTemplate>
<h1>else template</h1>
</ng-template> <ng-template [ngIf]="show" [ngIfElse]="elseTemplate">
<h1>Hi, Derrick</h1>
</ng-template>

除了 true 的模板,我们还可以提供一个 else 的模板,在 false 的情况下它就会显示 elseTemplate。

效果

NgIfContext

ngIf 除了可以传入 true / false 以外,还可以传入一个 Context 对象,作为判断条件。

<ng-template [ngIf]="{ name: 'Derrick' }" [ngIfElse]="elseTemplate" let-person>
<h1>Hi, {{ person.name }}</h1>
</ng-template>

当 ngIf 是 false、0、empty string、null 或 undefined 时,它会进入 elseTemplate,

其它情况会作为 ng-template 的 context.$implicit。

再一个例子

<ng-template #loadingTemplate>
<p>loading...</p>
</ng-template> <ng-template [ngIf]="person" [ngIfElse]="loadingTemplate" let-person>
<h1>Hi, {{ person.name }}</h1>
</ng-template>

一开始 person 是 null,所以会显示 loading,当 person 有值后就显示 say hi template。

export class AppComponent {
person!: { name: string };
constructor() {
setTimeout(() => {
this.person = {
name: 'Derrick',
};
}, 3000);
}
}

效果

ngTemplateContextGuard

ngIf 指令设置了 static ngTemplateContextGuard 属性,所以 context 是有类型的,ngTemplateContextGuard 我们在 ng-template 文章中里有学过。

NgIf 的源码在 ng_if.ts

Repeating Element の NgForOf 指令

App Template

<div class="card-list">
<div class="card">
<p>index: 0</p>
<p>name: Derrick</p>
<p>age: 10</p>
</div> <div class="card">
<p>index: 1</p>
<p>name: Alex</p>
<p>age: 15</p>
</div> <div class="card">
<p>index: 2</p>
<p>name: Richard</p>
<p>age: 18</p>
</div>
</div>

上面有 3 张卡片,element 的结构是相同的,只是数据不同,这种情况就可以使用 NgForOf 指令封装模板。

AppComponent

interface Person {
name: string;
age: number;
} @Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
})
export class AppComponent {
people: Person[] = [
{ name: 'Derrick', age: 11 },
{ name: 'Alex', age: 18 },
{ name: 'Richard', age: 15 },
];
}

把数据抽出来,接着写模板

<div class="card-list">
<ng-template let-person let-index="index">
<div class="card">
<p>index: {{ index }}</p>
<p>name: {{ person.name }}</p>
<p>age: {{ person.age }}</p>
</div>
</ng-template>
</div>

接着添加 NgForOf 指令

<ng-template ngFor [ngForOf]="people" let-person let-index="index">

NgForOf 指令需要 2 个 attributes 哦,一个 ngFor 和一个 ngForOf。

效果

NgForOf 指令除了 index 和 data,它还包含了许多常用的 variables,比如: count (总数)、first (是不是第一条, boolean)、last、odd (是不是单数)、even。

TrackByFunction

trackByFunction 是用来帮助 NgForOf 指令内部做优化的,我们需要提供一个识别对象的方式。

export class AppComponent {
people: Person[] = [
{ name: 'Derrick', age: 11 },
{ name: 'Alex', age: 18 },
{ name: 'Richard', age: 15 },
]; trackByName: TrackByFunction<Person> = (_index, person) => {
return person.name;
};
}
<ng-template ngFor [ngForOf]="people" [ngForTrackBy]="trackByName" let-person let-index="index">

NgForOf 指令会以 person.name 作为识别 person 对象的 key,在 people 发生变成 (比如排序) 时,以 ViewContainerRef.move 取代 ViewContainerRef.createEmbededView 来进行需改,这样性能就会比较好。

NgSwitch 指令

NgSwitch 也是 Angular built-in 指令,顾名思义。

<button (click)="status = 'Pending'">status to completed</button>
<button (click)="status = 'Completed'">status to pending</button> <ng-container [ngSwitch]="status"> <ng-template [ngSwitchCase]="'Completed'">
<p>complete</p>
</ng-template> <ng-template [ngSwitchCase]="'Pending'">
<p>pending</p>
</ng-template> <ng-template ngSwitchDefault>
<p>none</p>
</ng-template> </ng-container>

当 status 变化,它会选择对应的模板作为展现。

有 2 点要注意:

  1. [ngSwtich] 可以用于任何 element,不一定要是 <ng-container />

    另外,ViewElementRef 并不是 <ng-container /> 哦,其实是 [ngSwitchCase] 和 ngSwtichDefault 的 ng-template。

  2. ngSwtich 指令没有 fallthrough 的概念,match 到后会自动 break。

指令微语法(Syntax Reference)

微语法是 Angular 为了让 Structure Directive 写起来比较好看而发明的。它的唯一用途就是比较好看而已。

我们来看一个 NgIf 指令的例子

AppComponent

interface Person {
name: string;
} export class AppComponent {
person$ = new Observable<Person>((subscriber) => {
setTimeout(() => {
subscriber.next({ name: 'Derrick' });
subscriber.complete();
}, 2000);
});
}

App Template

<ng-template #loadingTemplate>
<p>loading...</p>
</ng-template> <ng-template [ngIf]="person$ | async" let-person [ngIfElse]="loadingTemplate">
<p>Hi, {{ person.name }}</p>
</ng-template>

person$ 是一个 RxJS 的 stream,我们用 AsyncPipe 去 subscribe 它,在它还没有 next 之前它是 null,所以会显示 loading template。

下面是用微语法的表达方式

<!-- 普通的 Structure Directive -->
<ng-template [ngIf]="person$ | async" let-person [ngIfElse]="loadingTemplate">
<p>Hi, {{ person.name }}</p>
</ng-template> <!-- 微语法 -->
<p *ngIf="person$ | async, let person, else loadingTemplate">Hi, {{ person.name }}</p>

语法解析是这样的:

  • * 星号会在 p 的外层 wrap 一个 <ng-template>

  • ngIf 就是指令 [ngIf],只是省略了方括弧

  • person$ | async 作为传递给 NgIf 指令的参数

  • let person 就是 let-person

  • , 逗号是分割符,用分号 ; 也是可以,不放也是可以

  • else 会变成 [ngIfElse],它以 ngIf 作为 prefix 把 else 拼接上去

  • loadingTemplate 是 [ngIfElse] 的参数

下面这些写法也是正确的

<p *ngIf="person$ | async let person else loadingTemplate">Hi, {{ person.name }}</p>
<p *ngIf="person$ | async else loadingTemplate let person">Hi, {{ person.name }}</p>

除了开头一定要是传递给指令的参数 expression 或者一个 let,后面的语法顺序就不太重要了。

再看一个 NgForOf 指令的微语法

<ng-template #loadingTemplate>
<p>loading...</p>
</ng-template> <ng-template [ngIf]="people$ | async" let-people [ngIfElse]="loadingTemplate">
<!-- 普通的 Structure Directive -->
<ng-template ngFor [ngForOf]="people" let-person let-index="index" let-isFirst="first" >
<p>index: {{ index }}</p>
<p>name: {{ person.name }}</p>
<p>is first: {{ isFirst }}</p>
</ng-template> </ng-template>

外部一层 NgIf 指令负责 loading,有资料了才 for loop。

微语法长这样

<ng-container *ngFor="let person of people, let index = index, let isFirst = first">
<p>index: {{ index }}</p>
<p>name: {{ person.name }}</p>
<p>is first: {{ isFirst }}</p>
</ng-container>

注意,ng-template 换成了 <ng-container>,因为 * 星号解析后会自动 wrap 一层 ng-template 在外面,wrap 了后里面不可以再是一个 ng-template,

所以只好借助 <ng-container>。相等于下面这个写法

<ng-template ngFor [ngForOf]="people" let-person let-index="index" let-isFirst="first">
<ng-container>
<p>index: {{ index }}</p>
<p>name: {{ person.name }}</p>
<p>is first: {{ isFirst }}</p>
</ng-container>
</ng-template>

虽然 <ng-container> 没有任何作用,但也没有任何副作用。

另外,切记开头第一句一定要是 let 或者是给指令的 expression 哦,像下面这样是错误的语法。

<ng-container *ngFor="of people, let person, let index = index, let isFirst = first">

下面这些则都可以

<ng-container *ngFor="let person let index = index let isFirst = first of people">

<ng-container *ngFor="let index = index let person let isFirst = first of people">

as Syntax

这句

<ng-container *ngFor="let person of people, let index = index, let isFirst = first">

和这句是等价的

<ng-container *ngFor="let person of people, index as index, first as isFirst">

first as isFirst 等价于 let isFirst = first

as syntax 除了可以当 let 用,还有一个特别的玩法

<ng-container *ngFor="let value of [1, 2, 3] as numbers">
<pre>{{ numbers | json }}</pre>
</ng-container>

numbers 指向 array [1, 2, 3],什么原理?

of ... as 是一套的,它会变成 let-numbers="ngForOf"

NgForOfContext 特别设置了这么一个属性。

再一个例子

*ngIf="person$ | async as person"

ngIf...as person,它会变成 let-person="ngIf"

下面这两句的结果是一样的

<p *ngIf="person$ | async, let person">Hi, {{ person.name }}</p>
<p *ngIf="person$ | async as person">Hi, {{ person.name }}</p>

但原理其实不一样,let-person 指的是 context.$implicit,而 as person 指的是 context.ngIf,只是 NgIf 指令的 context.$implicit 刚巧等于 context.ngIf 而已。

小心坑

如果 pipe async 后要判断值,该怎么写?

<h1 *ngIf="(number$ | async) === 5">Hello World 123</h1>
<h1 *ngIf="number$ | async; as number === 5">Hello World 1234</h1>

哪一句是正确的?

答案是第一句。

第二句完全是错误的语法 (虽然没有 error)

它 compile 后长这样

所以,不要写错哦。

一个节点只能有一个微语法

 <!-- 正确 -->
<ng-template
*ngIf="people$ | async as people else loadingTemplate"
ngFor [ngForOf]="people" let-person let-index="index" let-isFirst="first">
<p>index: {{ index }}</p>
<p>name: {{ person.name }}</p>
<p>is first: {{ isFirst }}</p>
</ng-template> <!-- 错误 -->
<ng-template
*ngIf="people$ | async as people else loadingTemplate"
*ngFor="let person of people; let index = index, let isFirst=first">
<p>index: {{ index }}</p>
<p>name: {{ person.name }}</p>
<p>is first: {{ isFirst }}</p>
</ng-template>

微语法与 ng-template 的小区别

<ng-template [ngIf]="true">
<h1>Hello World</h1>
</ng-template> <h1 *ngIf="true">Hello World</h1>

这两句几乎是等价的,唯一的区别是它们的 tag name 的。

我们看看 compile 之后的代码

我们通常可以忽视这种小区别,但在某些场景,它可能会出现一起反直觉的现象哦。

我们来看个奇葩例子,体会一下。

App Template

<app-test>
<ng-template [ngIf]="true">
<h1>Hello World</h1>
</ng-template>
</app-test>

Test Template

<ng-content select="h1" />

问:<ng-content /> 可以 select 到 h1 吗?

答:不可以,因为 h1 被 ng-template wrap 起来了,<ng-content /> 只能 select 到 first layer child (这个 limitation 我们以前讲过的)。

好,我们换成微语法

<app-test>
<h1 *ngIf="true">Hello World</h1>
</app-test>

这样 <ng-content /> 就可以 select 到 h1 了,因为微语法后,它的 tag 不再是 ng-template 而是 h1。

微语法总结

微语法只是一种简化的写法而已,其原理依旧是 Structure Directive ng-template + 指令 + [Input] + let。

总结

Structure Directive 其实是一个灵活的方案,而且也不难理解。

只是大部分人入门时都是直接学微语法和 built-in 的 Structure Directive,没有学习 ng-template,

所以往往不懂原理,无法灵活运用。

Angular 为此也推出了 Control Flow 来替代 NgIf、NgForOf、NgSwtich 指令。这种高度的封装虽然牺牲了灵活性,但也降低了新手的学习和试错成本。

下一篇就让我们一起来学习 Control Flow 吧。

目录

上一篇 Angular 18+ 高级教程 – Component 组件 の ng-template

下一篇 Angular 18+ 高级教程 – Component 组件 の Control Flow

想查看目录,请移步 Angular 18+ 高级教程 – 目录

喜欢请点推荐,若发现教程内容以新版脱节请评论通知我。happy coding

Angular 18+ 高级教程 – Component 组件 の Structural Directive (结构型指令) & Syntax Reference (微语法)的更多相关文章

  1. Angular结构型指令,模块和样式

    结构型指令 *是一个语法糖,<a *ngIf="user.login">退出</a>相当于 <ng-template [ngIf]="use ...

  2. angularjs中directive指令与component组件有什么区别?

     壹 ❀ 引 我在前面花了两篇博客分别系统化介绍了angularjs中的directive指令与component组件,当然directive也能实现组件这点毋庸置疑.在了解完两者后,即便我们知道co ...

  3. Vue教程:组件Component详解(六)

    一.什么是组件? 组件 (Component) 是 Vue.js 最强大的功能之一.组件可以扩展 HTML 元素,封装可重用的代码.在较高层面上,组件是自定义元素,Vue.js 的编译器为它添加特殊功 ...

  4. [Angular Directive] Write a Structural Directive in Angular 2

    Structural directives enable you to use an element as a template for creating additional elements. C ...

  5. angular2 学习笔记 ( Component 组件)

    refer : https://angular.cn/docs/ts/latest/guide/template-syntax.html https://angular.cn/docs/ts/late ...

  6. Angular CLI 使用教程指南参考

    Angular CLI 使用教程指南参考 Angular CLI 现在虽然可以正常使用但仍然处于测试阶段. Angular CLI 依赖 Node 4 和 NPM 3 或更高版本. 安装 要安装Ang ...

  7. 一篇文章看懂angularjs component组件

     壹 ❀ 引 我在 angularjs 一篇文章看懂自定义指令directive 一文中详细介绍了directive基本用法与完整属性介绍.directive是个很神奇的存在,你可以不设置templa ...

  8. vue 基础-->进阶 教程(3):组件嵌套、组件之间的通信、路由机制

    前面的nodejs教程并没有停止更新,因为node项目需要用vue来实现界面部分,所以先插入一个vue教程,以免不会的同学不能很好的完成项目. 本教程,将从零开始,教给大家vue的基础.高级操作.组件 ...

  9. 分享25个新鲜出炉的 Photoshop 高级教程

    网络上众多优秀的 Photoshop 实例教程是提高 Photoshop 技能的最佳学习途径.今天,我向大家分享25个新鲜出炉的 Photoshop 高级教程,提高你的设计技巧,制作时尚的图片效果.这 ...

  10. Siki_Unity_2-9_C#高级教程(未完)

    Unity 2-9 C#高级教程 任务1:字符串和正则表达式任务1-1&1-2:字符串类string System.String类(string为别名) 注:string创建的字符串是不可变的 ...

随机推荐

  1. Dawwin首位人工智能编程师,未来又会怎么样?

    Darwinai是一家快速发展的视觉质量检测公司,为制造商提供端到端解决方案,以提高产品质量并提高生产效率.该公司的专利可解释人工智能(XAI)平台已被众多财富500强公司采用,可以轻松集成值得信赖的 ...

  2. oeasy教您玩转vim - 23 - 配置文件

    配置文件 回忆上节课内容 我们上次找到配置文件的位置 ~/.vimrc 了解各种配置开关 修改配置文件并应用 这次想了解和配色方案相关的内容 colorscheme vi ~/.vimrc.old 中 ...

  3. 前端太卷了,不玩了,写写node.js全栈涨工资,赶紧学起来吧!!!!!

    首先聊下node.js的优缺点和应用场景 Node.js的优点和应用场景 Node.js作为后端开发的选择具有许多优点,以下是其中一些: 高性能: Node.js采用了事件驱动.非阻塞I/O模型,使得 ...

  4. 【教程】重启Windows文件资源管理器

    [教程]重启Windows文件资源管理器 打开任务管理器 以下方法任选其一: 方法一 :组合键 Ctrl + Shift + ESC (个人推荐) 方法二 :组合键 Win + X (或右键Windo ...

  5. nats 简介和使用

    nats 简介和使用 nats 有 3 个产品 core-nats: 不做持久化的及时信息传输系统 nats-streaming: 基于 nats 的持久化消息队列(已弃用) nats-jetstre ...

  6. mysql报错:ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/lib/mysql/mysql.sock' (2)

    mysql报错:ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/lib/mysql/mysql ...

  7. Jmeter函数助手41-unescapeHtml

    unescapeHtml函数用于将HTML转义过的字符串反转义为Unicode字符串. String to unescape:填入字符 1.escapeHtml函数是将字符进行HTML转义,unesc ...

  8. 8、SpringMVC之RESTful案例

    阅读本文前,需要先阅读SpringMVC之RESTful概述 8.1.前期工作 8.1.1.创建实体类Employee package org.rain.pojo; import java.io.Se ...

  9. 【Mybatis-Plus】联表分页查询实现

    参考文章: https://blog.csdn.net/weixin_43847283/article/details/125822614 上上周写的SQL案例确实可以重构,所以搬到Demo里面测试看 ...

  10. 【JavaWeb】接口请求404的问题排查

    响应状态404:404 Page Not Found 根本原因: 服务器找不到这个地址描述的页面资源, 注意是页面资源 可能的出现的开发情况: 1.请求的资源可能真的不存在,是接口,也可以是页面 2. ...