简介
单一职责原则是指应用程序的各个部分应该只有一个目的。遵循这个原则可以使您的 Angular 应用程序更容易测试和开发。
在 Angular 中,使用 NgTemplateOutlet
而不是创建特定组件,可以使组件在不修改组件本身的情况下轻松修改为各种用例。
在本文中,您将接受一个现有组件并重写它以使用 NgTemplateOutlet
。
先决条件
要完成本教程,您需要:
- 本地安装了 Node.js,您可以按照《如何安装 Node.js 并创建本地开发环境》进行操作。
- 一些关于设置 Angular 项目的熟悉程度。
本教程已使用 Node v16.6.2、npm
v7.20.6 和 @angular/core
v12.2.0 进行验证。
步骤 1 -- 构建 CardOrListViewComponent
考虑 CardOrListViewComponent
,它根据其 mode
在 'card'
或 'list'
格式中显示 items
。
它由一个 card-or-list-view.component.ts
文件组成:
typescript
import {
Component,
Input
} from '@angular/core';
@Component({
selector: 'card-or-list-view',
templateUrl: './card-or-list-view.component.html'
})
export class CardOrListViewComponent {
@Input() items: {
header: string,
content: string
}[] = [];
@Input() mode: string = 'card';
}
以及一个 card-or-list-view.component.html
模板:
html
<ng-container [ngSwitch]="mode">
<ng-container *ngSwitchCase="'card'">
<div *ngFor="let item of items">
<h1>{{item.header}}</h1>
<p>{{item.content}}</p>
</div>
</ng-container>
<ul *ngSwitchCase="'list'">
<li *ngFor="let item of items">
{{item.header}}: {{item.content}}
</li>
</ul>
</ng-container>
这是该组件的使用示例:
typescript
import { Component } from '@angular/core';
@Component({
template: `
<card-or-list-view
[items]="items"
[mode]="mode">
</card-or-list-view>
`
})
export class UsageExample {
mode = 'list';
items = [
{
header: 'Creating Reuseable Components with NgTemplateOutlet in Angular',
content: 'The single responsibility principle...'
} // ... more items
];
}
该组件没有单一职责,也不够灵活。它需要跟踪其 mode
并知道如何在 card
和 list
视图中显示 items
。它只能显示具有 header
和 content
的 items
。
让我们通过使用模板将组件分解为单独的视图来改变这一点。
步骤 2 -- 理解 ng-template
和 NgTemplateOutlet
为了让 CardOrListViewComponent
能够显示任何类型的 items
,我们需要告诉它如何显示它们。我们可以通过给它一个模板来实现这一点,它可以用来生成 items
。
模板将使用 <ng-template>
和从 TemplateRefs
创建的 EmbeddedViewRefs
。EmbeddedViewRefs
代表具有自己上下文的 Angular 视图,是最小的基本构建块。
Angular 提供了一种使用这个从模板生成视图的概念的方法,即使用 NgTemplateOutlet
。
NgTemplateOutlet
是一个指令,它接受一个 TemplateRef
和上下文,并使用提供的上下文生成一个 EmbeddedViewRef
。可以通过 let-{``{templateVariableName}}="contextProperty"
属性在模板上访问上下文,以创建模板可以使用的变量。如果未提供上下文属性名称,它将选择 $implicit
属性。
这是一个示例:
typescript
import { Component } from '@angular/core';
@Component({
template: `
<ng-container *ngTemplateOutlet="templateRef; context: exampleContext"></ng-container>
<ng-template #templateRef let-default let-other="aContextProperty">
<div>
$implicit = '{{default}}'
aContextProperty = '{{other}}'
</div>
</ng-template>
`
})
export class NgTemplateOutletExample {
exampleContext = {
$implicit: 'default context property when none specified',
aContextProperty: 'a context property'
};
}
这是示例的输出:
html
<div>
$implicit = 'default context property when none specified'
aContextProperty = 'a context property'
</div>
default
和 other
变量由 let-default
和 let-other="aContextProperty"
属性提供。
第三步 -- 重构 CardOrListViewComponent
为了使 CardOrListViewComponent
更加灵活,并允许它显示任何类型的 items
,我们将创建两个结构型指令来作为模板。这些模板将分别用于卡片和列表项。
这是 card-item.directive.ts
:
typescript
import { Directive } from '@angular/core';
@Directive({
selector: '[cardItem]'
})
export class CardItemDirective {
constructor() { }
}
这是 list-item.directive.ts
:
typescript
import { Directive } from '@angular/core';
@Directive({
selector: '[listItem]'
})
export class ListItemDirective {
constructor() { }
}
CardOrListViewComponent
将导入 CardItemDirective
和 ListItemDirective
:
typescript
import {
Component,
ContentChild,
Input,
TemplateRef
} from '@angular/core';
import { CardItemDirective } from './card-item.directive';
import { ListItemDirective } from './list-item.directive';
@Component({
selector: 'card-or-list-view',
templateUrl: './card-or-list-view.component.html'
})
export class CardOrListViewComponent {
@Input() items: {
header: string,
content: string
}[] = [];
@Input() mode: string = 'card';
@ContentChild(CardItemDirective, {read: TemplateRef}) cardItemTemplate: any;
@ContentChild(ListItemDirective, {read: TemplateRef}) listItemTemplate: any;
}
这段代码将读取我们的结构型指令作为 TemplateRefs
。
html
<ng-container [ngSwitch]="mode">
<ng-container *ngSwitchCase="'card'">
<ng-container *ngFor="let item of items">
<ng-container *ngTemplateOutlet="cardItemTemplate"></ng-container>
</ng-container>
</ng-container>
<ul *ngSwitchCase="'list'">
<li *ngFor="let item of items">
<ng-container *ngTemplateOutlet="listItemTemplate"></ng-container>
</li>
</ul>
</ng-container>
这是该组件的使用示例:
typescript
import { Component } from '@angular/core';
@Component({
template: `
<card-or-list-view
[items]="items"
[mode]="mode">
<div *cardItem>
静态卡片模板
</div>
<li *listItem>
静态列表模板
</li>
</card-or-list-view>
`
})
export class UsageExample {
mode = 'list';
items = [
{
header: '使用 NgTemplateOutlet 在 Angular 中创建可重用组件',
content: '单一职责原则...'
} // ... 更多项
];
}
通过这些更改,CardOrListViewComponent
现在可以根据提供的模板以卡片或列表形式显示任何类型的项。目前,模板是静态的。
我们需要做的最后一件事是通过为它们提供上下文来使模板变得动态:
html
<ng-container [ngSwitch]="mode">
<ng-container *ngSwitchCase="'card'">
<ng-container *ngFor="let item of items">
<ng-container *ngTemplateOutlet="cardItemTemplate; context: {$implicit: item}"></ng-container>
</ng-container>
</ng-container>
<ul *ngSwitchCase="'list'">
<li *ngFor="let item of items">
<ng-container *ngTemplateOutlet="listItemTemplate; context: {$implicit: item}"></ng-container>
</li>
</ul>
</ng-container>
这是该组件的使用示例:
typescript
import { Component } from '@angular/core';
@Component({
template: `
<card-or-list-view
[items]="items"
[mode]="mode">
<div *cardItem="let item">
<h1>{{item.header}}</h1>
<p>{{item.content}}</p>
</div>
<li *listItem="let item">
{{item.header}}: {{item.content}}
</li>
</card-or-list-view>
`
})
export class UsageExample {
mode = 'list';
items = [
{
header: '使用 NgTemplateOutlet 在 Angular 中创建可重用组件',
content: '单一职责原则...'
} // ... 更多项
];
}
有趣的是,我们使用了星号前缀和微语法来实现语法糖。这与以下代码是相同的:
html
<ng-template cardItem let-item>
<div>
<h1>{{item.header}}</h1>
<p>{{item.content}}</p>
</div>
</ng-template>
就是这样!我们拥有了原始功能,但现在可以通过修改模板来显示任何我们想要的内容,而 CardOrListViewComponent
的责任更少了。我们可以向项上下文中添加更多内容,比如类似于 ngFor
的 first
或 last
,或者显示完全不同类型的 items
。
结论
在本文中,您将一个现有的组件重写,以使用 NgTemplateOutlet
。
如果您想了解更多关于 Angular 的内容,请查看我们的 Angular 专题页面,了解相关练习和编程项目。