Angular9 高级教程(七)
十七、了解组件
组件是拥有自己的模板的指令,而不是依赖于从其他地方提供的内容。组件可以访问前面章节中描述的所有指令特性,仍然有一个宿主元素,仍然可以定义输入和输出属性,等等。但是它们也定义了自己的内容。
很容易低估模板的重要性,但是属性和结构指令有局限性。指令可以做有用和强大的工作,但是它们对应用它们的元素没有太多的洞察力。指令在作为通用工具时最有用,例如ngModel指令,它可以应用于任何数据模型属性和任何表单元素,而不考虑数据或元素的用途。
相比之下,组件与其模板的内容紧密相关。组件提供数据和逻辑,这些数据和逻辑将由应用于模板中 HTML 元素的数据绑定使用,这些数据和逻辑提供用于评估数据绑定表达式的上下文,并充当指令和应用其余部分之间的粘合剂。组件也是一个有用的工具,允许将大 Angular 的项目分解成可管理的块。
在这一章中,我将解释组件是如何工作的,并解释如何通过引入一些额外的组件来重构一个应用。表 17-1 将组件放在上下文中。
表 17-1。
将组件放在上下文中
|问题
|
回答
|
| --- | --- |
| 它们是什么? | 组件是定义它们自己的 HTML 内容和 CSS 样式(可选)的指令。 |
| 它们为什么有用? | 组件使得定义自包含的功能块成为可能,这使得项目更易于管理,并且允许功能更容易被重用。 |
| 它们是如何使用的? | @Component decorator 应用于一个类,该类注册在应用的 Angular 模块中。 |
| 有什么陷阱或限制吗? | 不需要。组件提供了指令的所有功能,还提供了自己的模板。 |
| 有其他选择吗? | Angular 应用必须包含至少一个用于引导过程的组件。除此之外,您不必添加额外的组件,尽管最终的应用变得难以管理。 |
表 17-2 总结了本章内容。
表 17-2。
章节总结
|问题
|
解决办法
|
列表
|
| --- | --- | --- |
| 创建组件 | 将@Component指令应用于一个类 | 1–5 |
| 定义组件显示的内容 | 创建内联或外部模板 | 6–8 |
| 在模板中包含数据 | 在组件的模板中使用数据绑定 | nine |
| 组件之间的协调 | 使用输入或输出属性 | 10–16 |
| 在应用了组件的元素中显示内容 | 投影主体元素的内容 | 17–21 |
| 样式组件内容 | 创建组件样式 | 22–30 |
| 查询组件模板中的内容 | 使用@ViewChildren装饰器 | Thirty-one |
准备示例项目
在这一章中,我继续使用我在第十一章中创建的示例项目,并且一直在修改。准备本章不需要做任何改动。
Tip
你可以从 https://github.com/Apress/pro-angular-9 下载本章以及本书其他章节的示例项目。如果在运行示例时遇到问题,请参见第一章获取帮助。
在example文件夹中运行以下命令,启动 Angular 开发工具:
ng serve
打开新的浏览器并导航至http://localhost:4200以查看图 17-1 中的内容。
图 17-1。
运行示例项目
用组件构建应用
目前,示例项目只包含一个组件和一个模板。Angular 应用需要至少一个组件,称为根组件,它是 Angular 模块中指定的入口点。
只有一个组件的问题是,它最终包含了应用所有功能所需的逻辑,其模板包含了向用户公开这些功能所需的所有标记。结果是单个组件及其模板负责处理大量任务。示例应用中的组件负责以下内容:
-
提供 Angular 作为应用的入口点,作为根组件
-
提供对应用数据模型的访问,以便可以在数据绑定中使用它
-
定义用于创建新产品的 HTML 表单
-
定义用于显示产品的 HTML 表格
-
定义包含表单和表格的布局
-
创建新产品时检查表单数据是否有效
-
维护用于防止无效数据被用来创建数据的状态信息
-
维护关于是否应该显示表格的状态信息
对于这样一个简单的应用来说,有很多事情要做,而且并非所有这些任务都是相关的。随着开发的进行,这种影响往往会逐渐增加,但这意味着应用更难测试,因为单个功能无法有效隔离,并且更难增强和维护,因为代码和标记变得越来越复杂。
将组件添加到应用中,可以将功能分成构建块,这些构建块可以在应用的不同部分重复使用,并可以单独测试。在接下来的小节中,我将创建一些组件,这些组件将示例应用中包含的功能分解为可管理的、可重用的和独立的单元。在这个过程中,我将解释组件提供的不同于指令的特性。为了准备这些变化,我简化了现有组件的模板,如清单 17-1 所示。
<div class="row text-white m-2">
<div class="col-4 p-2 bg-success">
Form will go here
</div>
<div class="col-8 p-2 bg-primary">
Table will go here
</div>
</div>
Listing 17-1.Simplifying the Content of the template.html File in the src/app Folder
当您保存对模板的更改时,您将看到图 17-2 中的内容。当我开发新组件并将它们添加到应用中时,占位符将被应用功能替换。
图 17-2。
简化现有模板
创建新组件
为了创建一个新的组件,我在src/app文件夹中添加了一个名为productTable.component.ts的文件,并用它来定义清单 17-2 中所示的组件。
import { Component } from "@angular/core";
@Component({
selector: "paProductTable",
template: "<div>This is the table component</div>"
})
export class ProductTableComponent {
}
Listing 17-2.The Contents of the productTable.component.ts File in the src/app Folder
组件是一个已经应用了@Component装饰器的类。这是一个组件所能得到的最简单的东西,它提供了足够的功能来作为一个组件,而不需要做任何有用的事情。
定义组件的文件的命名惯例是使用一个描述性的名称,表明组件的用途,后跟一个句点,然后是component.ts。对于这个将用于生成产品表的组件,文件名是productTable.component.ts。类名应该同样具有描述性。这个组件的类被命名为ProductTableComponent。
@Component装饰器描述并配置组件。表 17-3 中描述了最有用的装饰器属性,其中还包括描述它们的细节(本章并未涵盖所有属性)。
表 17-3。
组件装饰器属性
|名字
|
描述
|
| --- | --- |
| animations | 该属性用于配置动画,如第二十八章所述。 |
| encapsulation | 此属性用于更改视图封装设置,该设置控制组件样式如何与 HTML 文档的其余部分隔离。有关详细信息,请参见“设置视图封装”一节。 |
| selector | 此属性用于指定用于匹配宿主元素的 CSS 选择器,如表后所述。 |
| styles | 此属性用于定义仅应用于组件模板的 CSS 样式。样式是作为 TypeScript 文件的一部分内联定义的。有关详细信息,请参见“使用组件样式”一节。 |
| styleUrls | 此属性用于定义仅应用于组件模板的 CSS 样式。样式是在单独的 CSS 文件中定义的。有关详细信息,请参见“使用组件样式”一节。 |
| template | 该属性用于指定内联模板,如“定义模板”一节中所述。 |
| templateUrl | 该属性用于指定外部模板,如“定义模板”一节中所述。 |
| providers | 该属性用于为服务创建本地提供者,如第十九章所述。 |
| viewProviders | 该属性用于为仅可用于查看孩子的服务创建本地提供者,如第二十章所述。 |
对于第二个组件,我在src/app文件夹中创建了一个名为productForm.component.ts的文件,并添加了清单 17-3 中所示的代码。
import { Component } from "@angular/core";
@Component({
selector: "paProductForm",
template: "<div>This is the form component</div>"
})
export class ProductFormComponent {
}
Listing 17-3.The Contents of the productForm.component.ts File in the src/app Folder
这个组件同样简单,目前只是一个占位符。在本章的后面,我将添加一些更有用的特性。要启用这些组件,它们必须在应用的 Angular 模块中声明,如清单 17-4 所示。
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { ProductComponent } from "./component";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { PaAttrDirective } from "./attr.directive";
import { PaModel } from "./twoway.directive";
import { PaStructureDirective } from "./structure.directive";
import { PaIteratorDirective } from "./iterator.directive";
import { PaCellColor } from "./cellColor.directive";
import { PaCellColorSwitcher } from "./cellColorSwitcher.directive";
import { ProductTableComponent } from "./productTable.component";
import { ProductFormComponent } from "./productForm.component";
@NgModule({
imports: [BrowserModule, FormsModule, ReactiveFormsModule],
declarations: [ProductComponent, PaAttrDirective, PaModel,
PaStructureDirective, PaIteratorDirective,
PaCellColor, PaCellColorSwitcher, ProductTableComponent,
ProductFormComponent],
bootstrap: [ProductComponent]
})
export class AppModule { }
Listing 17-4.Enabling New Components in the app.module.ts File in the src/app Folder
使用一个import语句将组件类引入范围,并添加到NgModule装饰器的declarations数组中。最后一步是添加一个与组件的选择器属性相匹配的 HTML 元素,如清单 17-5 所示,这将为组件提供其主机元素。
<div class="row text-white m-2">
<div class="col-4 p-2 bg-success">
<paProductForm></paProductForm>
</div>
<div class="col-8 p-2 bg-primary">
<paProductTable></paProductTable>
</div>
</div>
Listing 17-5.Adding a Host Element in the template.html File in the src/app Folder
当所有的更改都被保存后,浏览器将显示如图 17-3 所示的内容,这表明 HTML 文档的一部分现在处于新组件的管理之下。
图 17-3。
添加新组件
了解新的应用结构
新组件改变了应用的结构。以前,根组件负责应用显示的所有 HTML 内容。然而,现在有了三个组件,一些 HTML 内容的责任已经委托给了新添加的组件,如图 17-4 所示。
图 17-4。
新的应用结构
当浏览器加载index.html文件时,Angular 引导程序启动,Angular 处理应用的模块,该模块提供了应用所需的组件列表。Angular 检查其配置中每个组件的装饰器,包括用于标识哪些元素将成为宿主的selector属性的值。
Angular 然后开始处理index.html文件的主体,并找到由ProductComponent组件的selector属性指定的app元素。Angular 用组件的模板填充app元素,该模板包含在template.html文件中。Angular 检查template.html文件的内容,并找到paProductForm和paProductTable元素,它们与新添加组件的选择器属性相匹配。Angular 用每个组件的模板填充这些元素,产生如图 17-3 所示的占位符内容。
有一些重要的新关系需要理解。首先,浏览器窗口中显示的 HTML 内容现在由几个模板组成,每个模板由一个组件管理。其次,ProductComponent现在是ProductFormComponent和ProductTableComponent对象的父组件,这种关系是由新组件的主机元素在template.html文件(即ProductComponent模板)中定义的事实形成的。同样,新组件是ProductComponent的子组件。当涉及到 Angular 组件时,父子关系是一个重要的关系,正如我在后面的章节中描述组件如何工作时所看到的。
定义模板
尽管应用中有新的组件,但它们目前没有太大的影响,因为它们只显示占位符内容。每个组件都有自己的模板,该模板定义了用于替换 HTML 文档中其宿主元素的内容。有两种不同的方法来定义模板:在@Component装饰器中内联或者在 HTML 文件中外部定义。
我添加的新组件使用模板,其中一段 HTML 被分配给@Component装饰器的template属性,如下所示:
...
template: "<div>This is the form component</div>"
...
这种方法的优点是简单:组件和模板在一个文件中定义,它们之间的关系不会混淆。内联模板的缺点是,如果包含很多 HTML 元素,它们会失去控制,难以阅读。
Note
另一个问题是,在您键入时突出显示语法错误的编辑器通常依赖于文件扩展名来判断应该执行哪种类型的检查,而不会意识到template属性的值是 HTML,只会将其视为字符串。
如果您正在使用 TypeScript,那么您可以使用多行字符串使内联模板更具可读性。多行字符串用反斜杠字符表示(```ts 字符,也称为重音符,它们允许字符串跨多行,如清单 17-6 所示。
import { Component } from "@angular/core";
@Component({
selector: "paProductTable",
template: `<div class='bg-info p-2'>
This is a multiline template
</div>`
})
export class ProductTableComponent {
}
Listing 17-6.Using a Multiline String in the productTable.component.ts File in the src/app Folder
```ts
多行字符串允许保留模板中 HTML 元素的结构,这使得阅读更容易,并增加了模板的大小,在变得难以管理之前,实际上可以内联包含模板。图 17-5 显示了清单 17-6 中模板的效果。

图 17-5。
使用多行内联模板
Tip
我的建议是对任何包含两三个以上简单元素的模板使用外部模板(在下一节中解释),主要是为了利用现代编辑器提供的 HTML 编辑和语法突出显示功能,这可以大大减少运行应用时发现的错误数量。
#### 定义外部模板
外部模板在不同于组件其余部分的文件中定义。这种方法的优点是代码和 HTML 没有混合在一起,这使得阅读和单元测试都更容易,这也意味着当您处理模板文件时,代码编辑器将知道他们正在处理 HTML 内容,这可以通过突出显示错误来帮助减少编码时的错误。
外部模板的缺点是,您必须管理项目中的更多文件,并确保每个组件都与正确的模板文件相关联。最好的方法是遵循一致的文件命名策略,这样文件包含给定组件的模板就显而易见了。Angular 的惯例是使用惯例`<componentname>.component.<type>`创建成对的文件,这样当您看到一个名为`productTable.component.ts`的文件时,您就知道它包含一个用 TypeScript 编写的名为`Products`的组件,当您看到一个名为`productTable.component.html`的文件时,您就知道它包含一个用于`Products`组件的外部模板。
Tip
这两种类型的模板的语法和功能是相同的,唯一的区别是内容存储在哪里,是与组件代码存储在同一个文件中,还是存储在一个单独的文件中。
为了使用命名约定定义外部模板,我在`src/app`文件夹中创建了一个名为`productTable.component.html`的文件,并添加了清单 17-7 中所示的标记。
Listing 17-7.The Contents of the productTable.component.html File in the src/app Folder
这是我从第十一章开始就一直用于根组件的模板。为了指定一个外部模板,在`@Component`装饰器中使用了`templateURL`属性,如清单 17-8 所示。
import { Component } from "@angular/core";
@Component({ selector: "paProductTable", templateUrl: "productTable.component.html" }) export class ProductTableComponent {
}
Listing 17-8.Using an External Template in the productTable.component.ts File in the src/app Folder
注意使用了不同的属性:`template`用于内联模板,`templateUrl`用于外部模板。图 17-6 显示了使用外部模板的效果。

图 17-6。
使用外部模板
#### 在组件模板中使用数据绑定
一个组件的模板可以包含所有的数据绑定,并以任何内置指令或已在应用的 Angular 模块中注册的自定义指令为目标。每个组件类都提供了用于评估其模板中的数据绑定表达式的上下文,并且默认情况下,每个组件都是相互隔离的。这意味着组件不必担心使用其他组件使用的相同的属性和方法名,并且可以依靠 Angular 来保持一切独立。作为一个例子,清单 17-9 显示了一个名为`model`的属性被添加到表单子组件中,如果它们没有保持分离的话,就会与根组件中同名的属性发生冲突。
import { Component } from "@angular/core";
@Component({ selector: "paProductForm", template: "
model: string = "This is the model";
}
Listing 17-9.Adding a Property in the productForm.component.ts File in the src/app Folder
component 类使用`model`属性存储一条消息,该消息使用字符串插值绑定显示在模板中。图 17-7 显示了结果。

图 17-7。
在子组件中使用数据绑定
#### 使用输入属性在组件之间进行协调
很少有组件是孤立存在的,需要与应用的其他部分共享数据。组件可以定义输入属性来接收其宿主元素上的数据绑定表达式的值。表达式将在父组件的上下文中进行计算,但结果将传递给子组件的属性。
为了演示,清单 17-10 向表格组件添加了一个输入属性,它将使用该属性来接收应该显示的模型数据。
import { Component, Input } from "@angular/core"; import { Model } from "./repository.model"; import { Product } from "./product.model";
@Component({ selector: "paProductTable", templateUrl: "productTable.component.html" }) export class ProductTableComponent {
@Input("model")
dataModel: Model;
getProduct(key: number): Product {
return this.dataModel.getProduct(key);
}
getProducts(): Product[] {
return this.dataModel.getProducts();
}
deleteProduct(key: number) {
this.dataModel.deleteProduct(key);
}
showTable: boolean = true;
}
Listing 17-10.Defining an Input Property in the productTable.component.ts File in the src/app Folder
该组件现在定义了一个输入属性,该属性将被赋予分配给主机元素上的`model`属性的值表达式。`getProduct`、`getProducts`和`deleteProduct`方法使用输入属性来提供对组件模板中绑定的数据模型的访问,这在清单 17-11 中进行了修改。在本章后面的清单 17-14 中,当我增强模板时会用到`showTable`属性。
There are {{getProducts().length}} items in the model
Listing 17-11.Adding a Data Binding in the productTable.component.html File in the src/app Folder
向子组件提供它所需要的数据意味着向它的宿主元素添加一个绑定,这是在父组件的模板中定义的,如清单 17-12 所示。
Listing 17-12.Adding a Data Binding in the template.html File in the src/app Folder
这个绑定的作用是为子组件提供对父组件的`model`属性的访问。这可能是一个令人困惑的特性,因为它依赖于这样一个事实,即主机元素是在父组件的模板中定义的,而输入属性是由子组件定义的,如图 17-8 所示。

图 17-8。
在父组件和子组件之间共享数据
子组件的 host 元素作为父组件和子组件之间的桥梁,input 属性允许组件向子组件提供它需要的数据,产生如图 17-9 所示的结果。

图 17-9。
将数据从父组件共享到子组件
##### 在子组件模板中使用指令
一旦定义了输入属性,子组件就可以使用所有的数据绑定和指令,要么使用父组件提供的数据,要么定义自己的数据。在清单 17-13 中,我恢复了前面章节中的原始表格功能,它显示了数据模型中的`Product`对象列表,以及一个决定是否显示表格的复选框。该功能以前由根组件及其模板管理。
| Name | Category | Price | ||
|---|---|---|---|---|
| {{i + 1}} | {{item.name}} | {{item.category}} | {{item.price}} | Delete |
Listing 17-13.Restoring the Table in the productTable.component.html File in the src/app Folder
使用相同的 HTML 元素、数据绑定和指令(包括像`paIf`和`paFor`这样的自定义指令),产生如图 17-10 所示的结果。关键区别不在于表的外观,而在于它现在由一个专用组件管理的方式。

图 17-10。
恢复表格显示
#### 使用输出属性在组件之间进行协调
子组件可以使用定义自定义事件的输出属性,这些事件表示重要的更改,并允许父组件在事件发生时做出响应。清单 17-14 展示了向表单组件添加一个输出属性,当用户创建一个新的`Product`对象时将触发该属性。
import { Component, Output, EventEmitter } from "@angular/core"; import { Product } from "./product.model";
@Component({ selector: "paProductForm", templateUrl: "productForm.component.html" }) export class ProductFormComponent { newProduct: Product = new Product();
@Output("paNewProduct")
newProductEvent = new EventEmitter<Product>();
submitForm(form: any) {
this.newProductEvent.emit(this.newProduct);
this.newProduct = new Product();
form.reset();
}
}
Listing 17-14.Defining an Output Property in the productForm.component.ts File in the src/app Folder
输出属性称为`newProductEvent`,组件在调用`submitForm`方法时触发它。除了 output 属性之外,清单中增加的内容基于根控制器中的逻辑,它以前管理表单。我还移除了内联模板,在`src/app`文件夹中创建了一个名为`productForm.component.html`的文件,内容如清单 17-15 所示。
Listing 17-15.The Contents of the productForm.component.html File in the src/app Folder
该表单包含使用双向绑定配置的标准元素。
子组件的主机元素充当到父组件的桥梁,父组件可以在自定义事件中注册兴趣,如清单 17-16 所示。
Listing 17-16.Registering for the Custom Event in the template.html File in the src/app Folder
新绑定通过将事件对象传递给`addProduct`方法来处理自定义事件。子组件负责管理表单元素并验证其内容。当数据通过验证时,自定义事件被触发,数据绑定表达式在父组件的上下文中进行计算,父组件的`addProduct`方法将新对象添加到模型中。由于模型已经通过其 input 属性与 table 子组件共享,新的数据显示给用户,如图 17-11 所示。

图 17-11。
在子组件中使用自定义事件
#### 投影主体元素内容
如果组件的主机元素包含内容,可以使用特殊的`ng-content`元素将其包含在模板中。这被称为*内容投影*,它允许创建将模板中的内容与宿主元素中的内容相结合的组件。为了演示,我在`src/app`文件夹中添加了一个名为`toggleView.component.ts`的文件,并用它来定义清单 17-17 中所示的组件。
import { Component } from "@angular/core";
@Component({ selector: "paToggleView", templateUrl: "toggleView.component.html" }) export class PaToggleView {
showContent: boolean = true;
}
Listing 17-17.The Contents of the toggleView.component.ts File in the src/app Folder
该组件定义了一个`showContent`属性,该属性将用于确定主机元素的内容是否会显示在模板中。为了提供模板,我在`src/app`文件夹中添加了一个名为`toggleView.component.html`的文件,并添加了清单 17-18 中所示的元素。
Listing 17-18.The Contents of the toggleView.component.html File in the src/app Folder
重要的元素是`ng-content`,Angular 将用宿主元素的内容替换。`ngIf`指令已经应用到了`ng-content`元素,因此只有当模板中的复选框被选中时,它才可见。清单 17-19 向 Angular 模块注册组件。
import { NgModule } from "@angular/core"; import { BrowserModule } from "@angular/platform-browser"; import { ProductComponent } from "./component"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { PaAttrDirective } from "./attr.directive"; import { PaModel } from "./twoway.directive"; import { PaStructureDirective } from "./structure.directive"; import { PaIteratorDirective } from "./iterator.directive"; import { PaCellColor } from "./cellColor.directive"; import { PaCellColorSwitcher } from "./cellColorSwitcher.directive"; import { ProductTableComponent } from "./productTable.component"; import { ProductFormComponent } from "./productForm.component"; import { PaToggleView } from "./toggleView.component";
@NgModule({ imports: [BrowserModule, FormsModule, ReactiveFormsModule], declarations: [ProductComponent, PaAttrDirective, PaModel, PaStructureDirective, PaIteratorDirective, PaCellColor, PaCellColorSwitcher, ProductTableComponent, ProductFormComponent, PaToggleView], bootstrap: [ProductComponent] }) export class AppModule { }
Listing 17-19.Registering the Component in the app.module.ts File in the src/app Folder
最后一步是将新组件应用于包含内容的主机元素,如清单 17-20 所示。
Listing 17-20.Adding a Host Element with Content in the template.html File in the src/app Folder
`paToggleView`元素是新组件的宿主,它包含`paProductTable`元素,该元素应用创建产品表的组件。结果是有一个控制表格可见性的复选框,如图 17-12 所示。新组件不知道其宿主元素的内容,只有通过`ng-content`元素才能将其包含在模板中。

图 17-12。
在模板中包含主体元素内容
### 完成组件重组
以前包含在根组件中的功能已经分配给新的子组件。剩下的就是整理根组件,删除不再需要的代码,如清单 17-21 所示。
import { ApplicationRef, Component } from "@angular/core"; import { Model } from "./repository.model"; import { Product } from "./product.model"; import { ProductFormGroup } from "./form.model";
@Component({ selector: "app", templateUrl: "template.html" }) export class ProductComponent { model: Model = new Model();
addProduct(p: Product) {
this.model.saveProduct(p);
}
}
Listing 17-21.Removing Obsolete Code in the component.ts File in the src/app Folder
根组件的许多职责已经转移到应用的其他地方。从本章开始的原始列表中,只有以下内容仍然是根组件的责任:
* 提供 Angular 作为应用的入口点,作为根组件
* 提供对应用数据模型的访问,以便可以在数据绑定中使用它
子组件承担了其余的责任,提供了更简单、更易于开发、更易于维护的自包含功能块,并且可以根据需要重用。
## 使用组件样式
组件可以定义仅应用于其模板中的内容的样式,这允许组件设置内容的样式,而不受其父组件或其他前身定义的样式的影响,也不会影响其子组件和其他后代组件中的内容。可以使用`@Component`装饰器的`styles`属性内联定义样式,如清单 17-22 所示。
import { Component, Output, EventEmitter } from "@angular/core"; import { Product } from "./product.model";
@Component({ selector: "paProductForm", templateUrl: "productForm.component.html", styles: ["div { background-color: lightgreen }"] }) export class ProductFormComponent { newProduct: Product = new Product();
@Output("paNewProduct")
newProductEvent = new EventEmitter<Product>();
submitForm(form: any) {
this.newProductEvent.emit(this.newProduct);
this.newProduct = new Product();
form.reset();
}
}
Listing 17-22.Defining Inline Styles in the productForm.component.ts File in the src/app Folder
属性被设置为一个数组,其中每个条目包含一个 CSS 选择器和一个或多个属性。在清单中,我指定了将`div`元素的背景色设置为`lightgreen`的样式。即使整个组合的 HTML 文档中有`div`个元素,这种样式也只会影响定义它们的组件的模板中的元素,在本例中是表单组件,如图 17-13 所示。

图 17-13。
定义内联组件样式
Tip
开发工具创建的包中包含的样式仍然适用,这就是为什么元素仍然使用 Bootstrap 进行样式化。
### 定义外部组件样式
内联样式提供了与内联模板相同的优点和缺点:它们很简单,并且将所有内容保存在一个文件中,但是它们很难阅读、管理,并且会使代码编辑器感到困惑。
另一种方法是在单独的文件中定义样式,并使用装饰器中的`styleUrls`属性将它们与组件关联起来。外部样式文件遵循与模板和代码文件相同的命名约定。我在`src/app`文件夹中添加了一个名为`productForm.component.css`的文件,并用它来定义清单 17-23 中显示的样式。
div { background-color: lightcoral; }
Listing 17-23.The Contents of the productForm.component.css File in the src/app Folder
这与内联定义的样式相同,但使用了不同的颜色值来确认这是组件使用的 CSS。在清单 17-24 中,组件的装饰器已经被更新以指定样式文件。
import { Component, Output, EventEmitter } from "@angular/core"; import { Product } from "./product.model";
@Component({ selector: "paProductForm", templateUrl: "productForm.component.html", styleUrls: ["productForm.component.css"] }) export class ProductFormComponent { newProduct: Product = new Product();
@Output("paNewProduct")
newProductEvent = new EventEmitter<Product>();
submitForm(form: any) {
this.newProductEvent.emit(this.newProduct);
this.newProduct = new Product();
form.reset();
}
}
Listing 17-24.Using External Styles in the productForm.component.ts File in the src/app Folder
属性被设置为一个字符串数组,每个字符串指定一个 CSS 文件。图 17-14 显示添加外部样式文件的效果。

图 17-14。
定义外部组件样式
### 使用高级样式功能
在组件中定义样式是一个有用的特性,但是您不会总是得到您期望的结果。一些高级功能允许您控制组件样式的工作方式。
#### 设置视图封装
默认情况下,特定于组件的样式是通过编写已应用于组件的 CSS 来实现的,这样它就以特殊属性为目标,然后 Angular 将这些属性添加到组件模板中包含的所有顶级元素中。如果您使用浏览器的 F12 开发工具检查 DOM,您将会看到清单 17-23 中的外部 CSS 文件的内容被改写成这样:
...
div[_ngcontent-jwe-c40] { background-color: lightcoral; }...
选择器已经过修改,因此它匹配具有名为`_ngcontent-jwe-c40`的属性的`div`元素(尽管您可能会在浏览器中看到不同的名称,因为属性的名称是由 Angular 动态生成的)。
为了确保`style`元素中的 CSS 只影响由组件管理的 HTML 元素,模板中的元素被修改,因此它们具有相同的动态生成的属性,如下所示:
...
这被称为组件的*视图封装*行为,Angular 正在做的是模拟一个被称为*阴影 DOM* 的特性,它允许域对象模型的各个部分被隔离,因此它们有自己的范围,这意味着 JavaScript、样式和模板可以应用于 HTML 文档的一部分。Angular 模拟这种行为的原因是它仅由最新版本的现代浏览器实现,但还有两个其他封装选项,它们是使用`@Component`装饰器中的`encapsulation`属性设置的。
Tip
可以在 [`http://developer.mozilla.org/en-US/docs/Web/Web_Components/Shadow_DOM`](http://developer.mozilla.org/en-US/docs/Web/Web_Components/Shadow_DOM) 了解更多暗影 DOM。在 [`http://caniuse.com/#feat=shadowdom`](http://caniuse.com/#feat=shadowdom) 可以看到哪些浏览器支持阴影 DOM 特性。
从`ViewEncapsulation`枚举中为`encapsulation`属性赋值,该枚举在`@angular/core`模块中定义,它定义了表 17-4 中描述的值。
表 17-4。
视图封装值
<colgroup><col class="tcol1 align-left"> <col class="tcol2 align-left"></colgroup>
|
名字
|
描述
|
| --- | --- |
| `Emulated` | 指定该值后,Angular 通过编写内容和样式来添加属性,从而模拟阴影 DOM,如前所述。如果在`@Component`装饰器中没有指定`encapsulation`值,这是默认行为。 |
| `ShadowDom` | 指定该值时,Angular 使用浏览器的阴影 DOM 功能。只有在浏览器实现阴影 DOM 或使用多填充时,这种方法才有效。 |
| `None` | 当指定了这个值时,Angular 只是将未修改的 CSS 样式添加到 HTML 文档的 head 部分,并让浏览器使用普通的 CSS 优先规则来确定如何应用这些样式。 |
应谨慎使用`ShadowDom`和`None`值。浏览器对 shadow DOM 特性的支持是有限的,并且变得更加复杂,因为有一个 shadow DOM 特性的早期版本为了支持当前的方法而被放弃了。
`None`选项将组件定义的所有样式添加到 HTML 文档的`head`部分,并让浏览器决定如何应用它们。这样做的好处是可以在所有浏览器中工作,但是结果是不可预测的,并且不同组件定义的样式之间没有隔离。
为了完整起见,清单 17-25 显示了被设置为`Emulated`的`encapsulation`属性,这是缺省值,并且在 Angular 支持的所有浏览器中都有效,不需要 polyfills。
import { Component, Output, EventEmitter, ViewEncapsulation } from "@angular/core"; import { Product } from "./product.model";
@Component({ selector: "paProductForm", templateUrl: "productForm.component.html", styleUrls: ["productForm.component.css"], encapsulation: ViewEncapsulation.Emulated }) export class ProductFormComponent { newProduct: Product = new Product();
@Output("paNewProduct")
newProductEvent = new EventEmitter<Product>();
submitForm(form: any) {
this.newProductEvent.emit(this.newProduct);
this.newProduct = new Product();
form.reset();
}
}
Listing 17-25.Setting View Encapsulation in the productForm.component.ts File in the src/app Folder
#### 使用阴影 DOM CSS 选择器
使用影子 DOM 意味着存在常规 CSS 选择器无法跨越的界限。为了帮助解决这个问题,有一些特殊的 CSS 选择器在使用依赖于阴影 DOM 的样式时很有用(即使它正在被模拟),如表 17-5 中所述,并在下面的章节中演示。
表 17-5。
阴影 DOM CSS 选择器
<colgroup><col class="tcol1 align-left"> <col class="tcol2 align-left"></colgroup>
|
名字
|
描述
|
| --- | --- |
| `:host` | 该选择器用于匹配组件的宿主元素。 |
| `:host-context(classSelector)` | 该选择器用于匹配作为特定类成员的宿主元素的祖先。 |
| `/deep/ or >>>` | 父组件使用该选择器来定义影响子组件模板中元素的样式。这个选择器应该只在`@Component`装饰器的`encapsulation`属性被设置为`emulated`时使用,如“设置视图封装”一节所述。 |
##### 选择主体元素
组件的宿主元素出现在其模板之外,这意味着其样式中的选择器只应用于宿主元素包含的元素,而不是元素本身。这可以通过使用与主机元素匹配的`:host`选择器来解决。清单 17-26 定义了一个仅当鼠标指针悬停在主机元素上时才应用的样式,这是通过组合`:host`和`:hover`选择器指定的。
div { background-color: lightcoral; } :host:hover { font-size: 25px; }
Listing 17-26.Matching the Host Element in the productForm.component.css File in the src/app Folder
当鼠标指针在 host 元素上时,其`font-size`属性将被设置为 25px,这将使表单中所有元素的文本大小增加到 25 磅,如图 17-15 所示。

图 17-15。
在构件样式中选择主体元素
##### 选择主体元素的祖先
`:host-context`选择器用于根据宿主元素祖先(在模板之外)的类成员来设计组件模板中元素的样式。这是一个比`:host`更有限的选择器,不能用于指定除类选择器之外的任何东西,不支持匹配标签类型、属性或任何其他选择器。清单 17-27 显示了`:host-context`选择器的使用。
div { background-color: lightcoral; } :host:hover { font-size: 25px; } :host-context(.angularApp) input { background-color: lightgray; }
Listing 17-27.Selecting Ancestors in the productForm.component.css File in the src/app Folder
只有当宿主元素的祖先元素之一是名为`angularApp`的类的成员时,清单中的选择器才会将组件模板中`input`元素的`background-color`属性设置为`lightgrey`。在清单 17-28 中,我将`index.html`文件中的`app`元素添加到了`angularApp`类中,它是根组件的宿主元素。
Example
Listing 17-28.Adding the Host Element to a Class in the index.html File in the src/app Folder
图 17-16 显示了清单 17-28 变更前后选择器的效果。

图 17-16。
选择主体元素的祖先
##### 将样式推入子组件的模板
组件定义的样式不会自动应用于子组件模板中的元素。作为示范,清单 17-29 向根组件的`@Component`装饰器添加了一个样式。
import { ApplicationRef, Component } from "@angular/core"; import { Model } from "./repository.model"; import { Product } from "./product.model"; import { ProductFormGroup } from "./form.model";
@Component({ selector: "app", templateUrl: "template.html", styles: ["div { border: 2px black solid; font-style:italic }"] }) export class ProductComponent { model: Model = new Model();
addProduct(p: Product) {
this.model.saveProduct(p);
}
}
Listing 17-29.Defining Styles in the component.ts File in the src/app Folder
选择器匹配所有的`div`元素,应用边框并改变字体样式。图 17-17 显示了结果。

图 17-17。
应用常规 CSS 样式
有些 CSS 样式属性,如`font-style`,默认情况下是继承的,这意味着在父组件中设置这样的属性会影响子组件模板中的元素,因为浏览器会自动应用该样式。
其他属性,比如`border`,默认情况下不被继承,在父组件中设置这样的属性对子组件模板没有影响,除非使用`/deep/`或`>>>`选择器,如清单 17-30 所示。(这些选择器是彼此的别名,具有相同的效果。)
import { ApplicationRef, Component } from "@angular/core"; import { Model } from "./repository.model"; import { Product } from "./product.model"; import { ProductFormGroup } from "./form.model";
@Component({ selector: "app", templateUrl: "template.html", styles: ["/deep/ div { border: 2px black solid; font-style:italic }"] }) export class ProductComponent { model: Model = new Model();
addProduct(p: Product) {
this.model.saveProduct(p);
}
}
Listing 17-30.Pushing a Style into Child Templates in the component.ts File in the src/app Folder
样式选择器使用`/deep/`将样式推送到子组件的模板中,这意味着所有的`div`元素都被赋予了一个边框,如图 17-18 所示。

图 17-18。
将样式推入子组件模板
## 查询模板内容
组件可以查询其模板的内容来定位指令或组件的实例,这被称为*视图子代*。这些类似于第十六章中描述的指令内容子查询,但有一些重要的区别。
在清单 17-31 中,我向管理查询`PaCellColor`指令的表的组件添加了一些代码,创建该表是为了演示指令内容查询。该指令仍然在 Angular 模块中注册,并选择`td`元素,因此 Angular 会将它应用到表格组件内容的单元格中。
import { Component, Input, ViewChildren, QueryList } from "@angular/core"; import { Model } from "./repository.model"; import { Product } from "./product.model"; import { PaCellColor } from "./cellColor.directive";
@Component({ selector: "paProductTable", templateUrl: "productTable.component.html" }) export class ProductTableComponent {
@Input("model")
dataModel: Model;
getProduct(key: number): Product {
return this.dataModel.getProduct(key);
}
getProducts(): Product[] {
return this.dataModel.getProducts();
}
deleteProduct(key: number) {
this.dataModel.deleteProduct(key);
}
showTable: boolean = true;
@ViewChildren(PaCellColor)
viewChildren: QueryList<PaCellColor>;
ngAfterViewInit() {
this.viewChildren.changes.subscribe(() => {
this.updateViewChildren();
});
this.updateViewChildren();
}
private updateViewChildren() {
setTimeout(() => {
this.viewChildren.forEach((child, index) => {
child.setColor(index % 2 ? true : false);
})
}, 0);
}
}
Listing 17-31.Selecting View Children in the productTable.component.ts File in the src/app Folder
有两个属性装饰器用于查询模板中定义的指令或组件,如表 17-6 所述。
表 17-6。
视图子查询属性装饰者
<colgroup><col class="tcol1 align-left"> <col class="tcol2 align-left"></colgroup>
|
名字
|
描述
|
| --- | --- |
| `@ViewChild(class)` | 这个装饰器告诉 Angular 查询指定类型的第一个指令或组件对象,并将其分配给属性。类名可以用模板变量替换。多个类或模板可以用逗号分隔。 |
| `@ViewChildren(class)` | 这个装饰器将指定类型的所有指令和组件对象分配给属性。可以用模板变量代替类,多个值之间用逗号分隔。结果在第十六章中描述的`QueryList`对象中提供。 |
在清单中,我使用了`@ViewChildren`装饰器从组件的模板中选择所有的`PaCellColor`对象。除了不同的属性装饰器,组件有两种不同的生命周期方法,用于提供关于模板如何被处理的信息,如表 17-7 所述。
表 17-7。
其他组件生命周期方法
<colgroup><col class="tcol1 align-left"> <col class="tcol2 align-left"></colgroup>
|
名字
|
描述
|
| --- | --- |
| `ngAfterViewInit` | 当组件的视图已经初始化时,调用此方法。视图查询的结果是在调用此方法之前设置的。 |
| `ngAfterViewChecked` | 在作为更改检测过程的一部分检查了组件视图之后,调用此方法。 |
在清单中,我实现了`ngAfterViewInit`方法来确保 Angular 已经处理了组件的模板并设置了查询结果。在方法中,我执行了对`updateViewChildren`方法的初始调用,该方法对`PaCellColor`对象进行操作,并使用`QueryList.changes`属性设置了当查询结果改变时将被调用的函数,如第十六章所述。如第十六章所述,子视图在对`setTimeout`函数的调用中被更新。结果是每隔一个表格单元格的颜色发生变化,如图 17-19 所示。

图 17-19。
查询视图子级
Tip
如果您使用了`ng-content`元素,您可能需要组合视图子查询和内容子查询。模板中定义的内容使用清单 17-31 中显示的技术进行查询,但是项目内容——替换了`ng-content`元素——使用第十六章中描述的子查询进行查询。
## 摘要
在这一章中,我回顾了组件的主题,并解释了如何将指令的所有特性与提供它们自己的模板的能力结合起来。我解释了如何构建应用来创建小模块组件,以及组件如何使用输入和输出属性在它们之间进行协调。我还向您展示了组件如何定义只应用于其模板而不应用于应用其他部分的 CSS 样式。在下一章中,我将介绍管道,它用于准备在模板中显示的数据。
# 十八、使用和创建管道
管道是转换数据值的小段代码,因此它们可以在模板中显示给用户。管道允许在自包含的类中定义转换逻辑,这样就可以在整个应用中一致地应用它。表 18-1 将管道放在上下文中。
表 18-1。
将管道放在上下文中
<colgroup><col class="tcol1 align-left"> <col class="tcol2 align-left"></colgroup>
|
问题
|
回答
|
| --- | --- |
| 它们是什么? | 管道是用于准备向用户显示的数据的类。 |
| 它们为什么有用? | 管道允许在单个类中定义准备逻辑,该类可以在整个应用中使用,从而确保数据以一致的方式呈现。 |
| 它们是如何使用的? | `@Pipe` decorator 应用于一个类,用于指定一个名称,通过这个名称管道可以在模板中使用。 |
| 有什么陷阱或限制吗? | 管道应该简单,并专注于准备数据。让功能渗透到由其他构建块负责的领域,比如指令或组件,是很诱人的。 |
| 还有其他选择吗? | 您可以在组件或指令中实现数据准备代码,但是这使得在应用的其他部分重用变得更加困难。 |
表 18-2 总结了本章内容。
表 18-2。
章节总结
<colgroup><col class="tcol1 align-left"> <col class="tcol2 align-left"> <col class="tcol3 align-left"></colgroup>
|
问题
|
解决办法
|
列表
|
| --- | --- | --- |
| 格式化包含在模板中的数据值 | 在数据绑定表达式中使用管道 | 1–6 |
| 创建自定义管道 | 将`@Pipe`装饰器应用于一个类 | 7–9 |
| 使用多个管道格式化数据值 | 使用竖线字符将管道名称连接在一起 | Ten |
| 指定 Angular 应何时重新评估管道的输出 | 使用`@Pipe`装饰器的`pure`属性 | 11–14 |
| 格式化数值 | 使用`number`管道 | 15, 16 |
| 格式化货币值 | 使用`currency`管道 | 17, 18 |
| 格式化百分比值 | 使用`percent`管道 | 19–22 |
| 更改字符串的大小写 | 使用`uppercase`或`lowercase`管 | 23, 24 |
| 将对象序列化为 JSON 格式 | 使用`json`管道 | Twenty-five |
| 从数组中选择元素 | 使用`slice`管道 | Twenty-six |
| 将对象或映射格式化为键/值对 | 使用`keyvalue`管道 | Twenty-seven |
| 选择要为字符串或数字值显示的值 | 使用`i18nSelect`或`i18nPlural`管 | 28–30 |
## 准备示例项目
我将继续使用在第十一章中首次创建的示例项目,该项目已经在以后的章节中进行了扩展和修改。在前一章的最后几个例子中,组件样式和视图子查询让应用看起来非常花哨,我将在本章中淡化这一点。在清单 18-1 中,我禁用了应用于表单元素的内嵌组件样式。
Tip
你可以从 [`https://github.com/Apress/pro-angular-9`](https://github.com/Apress/pro-angular-9) 下载本章以及本书其他章节的示例项目。如果在运行示例时遇到问题,请参见第一章获取帮助。
```ts
import { Component, Output, EventEmitter, ViewEncapsulation } from "@angular/core";
import { Product } from "./product.model";
@Component({
selector: "paProductForm",
templateUrl: "productForm.component.html",
// styleUrls: ["productForm.component.css"],
// encapsulation: ViewEncapsulation.Emulated
})
export class ProductFormComponent {
newProduct: Product = new Product();
@Output("paNewProduct")
newProductEvent = new EventEmitter<Product>();
submitForm(form: any) {
this.newProductEvent.emit(this.newProduct);
this.newProduct = new Product();
form.reset();
}
}
Listing 18-1.Disabling CSS Styles in the productForm.component.ts File in the src/app Folder
为了禁用表格单元格的棋盘颜色,我更改了PaCellColor指令的选择器,使其匹配一个当前没有应用于 HTML 元素的属性,如清单 18-2 所示。
import { Directive, HostBinding } from "@angular/core";
@Directive({
selector: "td[paApplyColor]"
})
export class PaCellColor {
@HostBinding("class")
bgClass: string = "";
setColor(dark: Boolean) {
this.bgClass = dark ? "bg-dark" : "";
}
}
Listing 18-2.Changing the Selector in the cellColor.directive.ts File in the src/app Folder
清单 18-3 禁用了根组件定义的深度样式。
import { ApplicationRef, Component } from "@angular/core";
import { Model } from "./repository.model";
import { Product } from "./product.model";
import { ProductFormGroup } from "./form.model";
@Component({
selector: "app",
templateUrl: "template.html",
//styles: ["/deep/ div { border: 2px black solid; font-style:italic }"]
})
export class ProductComponent {
model: Model = new Model();
addProduct(p: Product) {
this.model.saveProduct(p);
}
}
Listing 18-3.Disabling CSS Styles in the component.ts File in the src/app Folder
下一个变化是简化ProductTableComponent类,删除不再需要的方法和属性,并添加将在后面的示例中使用的新属性,如清单 18-4 所示。
import { Component, Input, ViewChildren, QueryList } from "@angular/core";
import { Model } from "./repository.model";
import { Product } from "./product.model";
@Component({
selector: "paProductTable",
templateUrl: "productTable.component.html"
})
export class ProductTableComponent {
@Input("model")
dataModel: Model;
getProduct(key: number): Product {
return this.dataModel.getProduct(key);
}
getProducts(): Product[] {
return this.dataModel.getProducts();
}
deleteProduct(key: number) {
this.dataModel.deleteProduct(key);
}
taxRate: number = 0;
categoryFilter: string;
itemCount: number = 3;
}
Listing 18-4.Simplifying the Code in the productTable.component.ts File in the src/app Folder
最后,我从根组件的模板中移除了一个组件元素,以禁用显示和隐藏表格的复选框,如清单 18-5 所示。
<div class="row m-2">
<div class="col-4 p-2">
<paProductForm (paNewProduct)="addProduct($event)"></paProductForm>
</div>
<div class="col-8 p-2">
<paProductTable [model]="model"></paProductTable>
</div>
</div>
Listing 18-5.Simplifying the Elements in the template.html File in the src/app Folder
在example文件夹中运行以下命令,启动 Angular 开发工具:
ng serve
打开一个新的浏览器选项卡并导航至http://localhost:4200以查看如图 18-1 所示的内容。
图 18-1。
运行示例应用
了解管道
管道是在指令或组件接收数据之前转换数据的类。这听起来可能不像是一项重要的工作,但是管道可以用来轻松、一致地执行一些最常见的开发任务。
作为一个演示如何使用管道的简单例子,清单 18-6 应用了一个内置管道来转换应用显示的表的Price列中的值。
<table class="table table-sm table-bordered table-striped">
<tr><th></th><th>Name</th><th>Category</th><th>Price</th><th></th></tr>
<tr *paFor="let item of getProducts(); let i = index; let odd = odd;
let even = even" [class.bg-info]="odd" [class.bg-warning]="even">
<td style="vertical-align:middle">{{i + 1}}</td>
<td style="vertical-align:middle">{{item.name}}</td>
<td style="vertical-align:middle">{{item.category}}</td>
<td style="vertical-align:middle">
{{item.price | currency:"USD":"symbol" }}
</td>
<td class="text-center">
<button class="btn btn-danger btn-sm" (click)="deleteProduct(item.id)">
Delete
</button>
</td>
</tr>
</table>
Listing 18-6.Using a Pipe in the productTable.component.html File in the src/app Folder
应用管道的语法类似于命令提示符使用的样式,其中使用竖线符号(|字符)将值“管道化”以进行转换。图 18-2 显示了包含管道的数据绑定的结构。
图 18-2。
用管道进行数据绑定的剖析
清单 18-6 中使用的管道名称是currency,它将数字格式化为货币值。管道的参数由冒号(:字符)分隔。第一个管道参数指定应该使用的货币代码,在本例中是USD,代表美元。第二个管道参数是symbol,它指定是否应该显示货币符号,而不是它的代码。
当 Angular 处理表达式时,它获取数据值并将其传递给管道进行转换。管道产生的结果然后被用作数据绑定的表达式结果。在本例中,绑定是字符串插值,图 18-3 显示了结果。
图 18-3。
使用货币管道的效果
创建自定义管道
我将在本章的后面回到 Angular 提供的内置管道,但是理解管道如何工作以及它们能够做什么的最好方法是创建一个自定义管道。我在src/app文件夹中添加了一个名为addTax.pipe.ts的文件,并定义了清单 18-7 中所示的类。
import { Pipe } from "@angular/core";
@Pipe({
name: "addTax"
})
export class PaAddTaxPipe {
defaultRate: number = 10;
transform(value: any, rate?: any): number {
let valueNumber = Number.parseFloat(value);
let rateNumber = rate == undefined ?
this.defaultRate : Number.parseInt(rate);
return valueNumber + (valueNumber * (rateNumber / 100));
}
}
Listing 18-7.The Contents of the addTax.pipe.ts File in the src/app Folder
管道是应用了@Pipe装饰器的类,它实现了一个叫做transform的方法。@Pipe装饰器定义了两个属性,用于配置管道,如表 18-3 所述。
表 18-3。
@Pipe 装饰器属性
|名字
|
描述
|
| --- | --- |
| name | 此属性指定在模板中应用管道的名称。 |
| pure | 当true时,仅当其输入值或参数改变时,该管道才被重新评估。这是默认值。请参见“创建不纯管道”部分了解详细信息。 |
示例管道在一个名为PaAddTaxPipe的类中定义,其装饰器name属性指定管道将使用模板中的addTax来应用。transform方法必须接受至少一个参数,Angular 用它来提供管道格式化的数据值。管道在transform方法中工作,其结果由 Angular 在绑定表达式中使用。在本例中,transform方法接受一个数字值,其结果是收到的值加上销售税。
转换方法还可以定义用于配置管道的附加参数。在示例中,可选的rate参数可用于指定销售税率,默认为 10%。
Caution
在处理由transform方法接收的参数时要小心,并确保将它们解析或转换成您需要的类型。运行时不强制执行 TypeScript 类型注释,Angular 将传递给你它正在处理的任何数据值。
注册自定义管道
管道使用 Angular 模块的declarations属性注册,如清单 18-8 所示。
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { ProductComponent } from "./component";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { PaAttrDirective } from "./attr.directive";
import { PaModel } from "./twoway.directive";
import { PaStructureDirective } from "./structure.directive";
import { PaIteratorDirective } from "./iterator.directive";
import { PaCellColor } from "./cellColor.directive";
import { PaCellColorSwitcher } from "./cellColorSwitcher.directive";
import { ProductTableComponent } from "./productTable.component";
import { ProductFormComponent } from "./productForm.component";
import { PaToggleView } from "./toggleView.component";
import { PaAddTaxPipe } from "./addTax.pipe";
@NgModule({
imports: [BrowserModule, FormsModule, ReactiveFormsModule],
declarations: [ProductComponent, PaAttrDirective, PaModel,
PaStructureDirective, PaIteratorDirective,
PaCellColor, PaCellColorSwitcher, ProductTableComponent,
ProductFormComponent, PaToggleView, PaAddTaxPipe],
bootstrap: [ProductComponent]
})
export class AppModule { }
Listing 18-8.Registering a Custom Pipe in the app.module.ts File in the src/app Folder
应用自定义管道
一旦注册了自定义管道,就可以在数据绑定表达式中使用它。在清单 18-9 中,我将管道应用于表格中的price值,并添加了一个select元素,允许指定税率。
<div>
<label>Tax Rate:</label>
<select [value]="taxRate || 0" (change)="taxRate=$event.target.value">
<option value="0">None</option>
<option value="10">10%</option>
<option value="20">20%</option>
<option value="50">50%</option>
</select>
</div>
<table class="table table-sm table-bordered table-striped">
<tr><th></th><th>Name</th><th>Category</th><th>Price</th><th></th></tr>
<tr *paFor="let item of getProducts(); let i = index; let odd = odd;
let even = even" [class.bg-info]="odd" [class.bg-warning]="even">
<td style="vertical-align:middle">{{i + 1}}</td>
<td style="vertical-align:middle">{{item.name}}</td>
<td style="vertical-align:middle">{{item.category}}</td>
<td style="vertical-align:middle">
{{item.price | addTax:(taxRate || 0) }}
</td>
<td class="text-center">
<button class="btn btn-danger btn-sm" (click)="deleteProduct(item.id)">
Delete
</button>
</td>
</tr>
</table>
Listing 18-9.Applying the Custom Pipe in the productTable.component.html File in the src/app Folder
为了方便起见,我完全在模板中定义了税率。select元素有一个绑定,将它的value属性设置为一个名为taxRate的组件变量,或者如果属性没有被定义,则默认为0。event绑定处理change事件并设置taxRate属性的值。当使用ngModel指令时,您不能指定一个回退值,这就是我拆分绑定的原因。
在应用定制管道时,我使用了竖线字符,后跟管道装饰器中的name属性指定的值。管道的名称后面跟一个冒号,冒号后面跟一个表达式,表达式的计算结果是为管道提供它的参数。在这种情况下,如果已经定义了taxRate属性,将使用该属性,回退值为零。
管道是 Angular 数据绑定的动态特性的一部分,如果底层数据值发生变化或者用于参数的表达式发生变化,将调用管道的transform方法来获取更新的值。通过改变 select 元素显示的值可以看出管道的动态性质,这将定义或改变taxRate属性,这将依次更新自定义管道添加到price属性的数量,如图 18-4 所示。
图 18-4。
使用自定义管道
组合管道
addTax管道应用税率,但是由计算产生的小数金额是难看的——并且没有帮助,因为很少有税务当局坚持精确到 15 个小数位数。
我可以通过添加对自定义管道的支持来将数字值格式化为货币来解决这个问题,但是这需要复制我在本章前面使用的内置currency管道的功能。一个更好的方法是将两个管道的功能结合起来,这样来自定制addTax管道的输出就被输入到内置的currency管道中,然后用于产生显示给用户的值。
管道以这种方式使用竖线字符链接在一起,管道的名称按照数据流动的顺序指定,如清单 18-10 所示。
...
<td style="vertical-align:middle">
{{item.price | addTax:(taxRate || 0) | currency:"USD":"symbol" }}
</td>
...
Listing 18-10.Combining Pipes in the productTable.component.html File in the src/app Folder
item.price属性的值被传递给addTax管道,它添加销售税,然后传递给currency管道,它将数字值格式化为货币金额,如图 18-5 所示。
图 18-5。
结合管道的功能
制造不纯的管道
pure decorator 属性用于告诉 Angular 何时调用管道的transform方法。pure属性的缺省值是true,它告诉 Angular,只有当输入数据值(模板中竖线字符之前的数据值)改变或者当它的一个或多个参数被修改时,管道的转换方法才会生成一个新值。这被称为纯管道,因为它没有独立的内部状态,并且它的所有依赖关系都可以使用 Angular 变化检测过程来管理。
将pure装饰器属性设置为false会创建一个不纯管道,并告知 Angular 该管道有其自己的状态数据,或者当有新值时,该管道依赖于在变化检测过程中可能不会被拾取的数据。
当 Angular 执行其变化检测过程时,它将不纯管道视为数据值的来源,并调用transform方法,即使没有数据值或参数变化。
不纯管道最常见的需求是当它们处理数组的内容并且数组中的元素发生变化时。正如您在第十六章中看到的,Angular 不会自动检测数组中发生的变化,也不会在添加、编辑或删除数组元素时调用 pure pipe 的 transform 方法,因为它只看到相同的数组对象被用作输入数据值。
Caution
不纯管道应该谨慎使用,因为 Angular 必须在应用中有任何数据更改或用户交互时调用transform方法,以防它可能导致与pipe不同的结果。如果你创建了一个不纯的管道,那么尽可能保持简单。执行复杂的操作,比如对数组进行排序,会严重影响 Angular 应用的性能。
作为演示,我在src/app文件夹中添加了一个名为categoryFilter.pipe.ts的文件,并用它来定义清单 18-11 中所示的管道。
import { Pipe } from "@angular/core";
import { Product } from "./product.model";
@Pipe({
name: "filter",
pure: true
})
export class PaCategoryFilterPipe {
transform(products: Product[], category: string): Product[] {
return category == undefined ?
products : products.filter(p => p.category == category);
}
}
Listing 18-11.The Contents of the categoryFilter.pipe.ts File in the src/app Folder
这是一个纯粹的过滤器,接收一组Product对象,只返回那些category属性与category参数匹配的对象。清单 18-12 显示了在 Angular 模块中注册的新管道。
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { ProductComponent } from "./component";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { PaAttrDirective } from "./attr.directive";
import { PaModel } from "./twoway.directive";
import { PaStructureDirective } from "./structure.directive";
import { PaIteratorDirective } from "./iterator.directive";
import { PaCellColor } from "./cellColor.directive";
import { PaCellColorSwitcher } from "./cellColorSwitcher.directive";
import { ProductTableComponent } from "./productTable.component";
import { ProductFormComponent } from "./productForm.component";
import { PaToggleView } from "./toggleView.component";
import { PaAddTaxPipe } from "./addTax.pipe";
import { PaCategoryFilterPipe } from "./categoryFilter.pipe";
@NgModule({
imports: [BrowserModule, FormsModule, ReactiveFormsModule],
declarations: [ProductComponent, PaAttrDirective, PaModel,
PaStructureDirective, PaIteratorDirective,
PaCellColor, PaCellColorSwitcher, ProductTableComponent,
ProductFormComponent, PaToggleView, PaAddTaxPipe,
PaCategoryFilterPipe],
bootstrap: [ProductComponent]
})
export class AppModule { }
Listing 18-12.Registering a Pipe in the app.module.ts File in the src/app Folder
清单 18-13 显示了新管道在绑定表达式中的应用,该绑定表达式以ngFor指令以及允许选择过滤器类别的新select元素为目标。
<div>
<label>Tax Rate:</label>
<select [value]="taxRate || 0" (change)="taxRate=$event.target.value">
<option value="0">None</option>
<option value="10">10%</option>
<option value="20">20%</option>
<option value="50">50%</option>
</select>
</div>
<div>
<label>Category Filter:</label>
<select [(ngModel)]="categoryFilter">
<option>Watersports</option>
<option>Soccer</option>
<option>Chess</option>
</select>
</div>
<table class="table table-sm table-bordered table-striped">
<tr><th></th><th>Name</th><th>Category</th><th>Price</th><th></th></tr>
<tr *paFor="let item of getProducts() | filter:categoryFilter;
let i = index; let odd = odd; let even = even"
[class.bg-info]="odd" [class.bg-warning]="even">
<td style="vertical-align:middle">{{i + 1}}</td>
<td style="vertical-align:middle">{{item.name}}</td>
<td style="vertical-align:middle">{{item.category}}</td>
<td style="vertical-align:middle">
{{item.price | addTax:(taxRate || 0) | currency:"USD":"symbol" }}
</td>
<td class="text-center">
<button class="btn btn-danger btn-sm" (click)="deleteProduct(item.id)">
Delete
</button>
</td>
</tr>
</table>
Listing 18-13.Applying a Pipe in the productTable.component.html File in the src/app Folder
要查看问题,使用select元素过滤表中的产品,以便只显示那些在Soccer类别中的产品。然后使用表单元素在该类别中创建一个新产品。点击创建按钮会将产品添加到数据模型中,但是新产品不会显示在表格中,如图 18-6 所示。
图 18-6。
纯管道引起的问题
该表没有更新,因为就 Angular 而言,filter管道的任何输入都没有改变。组件的getProducts方法返回相同的数组对象,并且categoryFilter属性仍然设置为Soccer。Angular 没有意识到在由getProducts方法返回的数组中有一个新的对象。
解决方法是将管道的pure属性设置为false,如清单 18-14 所示。
import { Pipe } from "@angular/core";
import { Product } from "./product.model";
@Pipe({
name: "filter",
pure: false
})
export class PaCategoryFilterPipe {
transform(products: Product[], category: string): Product[] {
return category == undefined ?
products : products.filter(p => p.category == category);
}
}
Listing 18-14.Marking a Pipe as Impure in the categoryFilter.pipe.ts File in the src/app Folder
如果重复测试,您将看到新产品现在正确显示在表格中,如图 18-7 所示。
图 18-7。
使用不纯的管子
使用内置管道
Angular 包括一组内置管道,用于执行通常需要的任务。这些管道在表 18-4 中描述,并在以下章节中演示。
表 18-4。
内置管道
|名字
|
描述
|
| --- | --- |
| number | 这个管道执行数字值的区分位置的格式化。有关详细信息,请参见“格式化数字”一节。 |
| currency | 这个管道对货币金额执行区分位置的格式化。有关详细信息,请参见“格式化货币值”一节。 |
| percent | 这个管道执行百分比值的区分位置的格式化。有关详细信息,请参见“格式化百分比”一节。 |
| date | 这个管道执行日期的区分位置的格式化。有关详细信息,请参见“格式化日期”一节。 |
| uppercase | 这个管道将字符串中的所有字符转换为大写。有关详细信息,请参见“更改字符串大小写”一节。 |
| Lowercase | 这个管道将字符串中的所有字符转换成小写。有关详细信息,请参见“更改字符串大小写”一节。 |
| titlecase | 这个管道将字符串中的所有字符转换成大小写。有关详细信息,请参见“更改字符串大小写”一节。 |
| json | 这个管道将一个对象转换成一个 JSON 字符串。有关详细信息,请参见“将数据序列化为 JSON”一节。 |
| slice | 这个管道从数组中选择项或从字符串中选择字符,如“数据数组切片”一节所述。 |
| keyvalue | 这个管道将一个对象或映射转换成一系列的键/值对,如“格式化键/值对”一节所述。 |
| i18nSelect | 这个管道为一组值选择要显示的文本值,如“选择值”一节中所述。 |
| i18nPlural | 这个管道为一个值选择一个多元字符串,如“多元值”一节所述。 |
| async | 这个管道订阅一个可观察值或一个承诺,并显示它产生的最新值。该管道在第二十三章中演示。 |
格式化数字
number管道使用区域敏感规则格式化number值。清单 18-15 展示了number管道的使用,以及指定将要使用的格式的参数。我已经从模板中移除了定制管道和相关的select元素。
<table class="table table-sm table-bordered table-striped">
<tr><th></th><th>Name</th><th>Category</th><th>Price</th><th></th></tr>
<tr *paFor="let item of getProducts(); let i = index; let odd = odd;
let even = even" [class.bg-info]="odd" [class.bg-warning]="even">
<td style="vertical-align:middle">{{i + 1}}</td>
<td style="vertical-align:middle">{{item.name}}</td>
<td style="vertical-align:middle">{{item.category}}</td>
<td style="vertical-align:middle">{{item.price | number:"3.2-2" }}</td>
<td class="text-center">
<button class="btn btn-danger btn-sm" (click)="deleteProduct(item.id)">
Delete
</button>
</td>
</tr>
</table>
Listing 18-15.Using the number Pipe in the productTable.component.html File in the src/app Folder
number管道接受一个参数,该参数指定格式化结果中包含的位数。该参数采用以下格式(请注意分隔这些值的句点和连字符,并且整个参数以字符串形式引用):
"<minIntegerDigits>.<minFactionDigits>-<maxFractionDigits>"
表 18-5 描述了格式参数的每个元素。
表 18-5。
数字管道参数的元素
|名字
|
描述
|
| --- | --- |
| minIntegerDigits | 该值指定最小位数。默认值为 1。 |
| minFractionDigits | 该值指定小数位数的最小值。默认值为 0。 |
| maxFractionDigits | 该值指定小数位数的最大值。默认值为 3。 |
清单中使用的参数是"3.2-2",它指定至少应该使用三位数字来显示数字的整数部分,并且应该始终使用两位小数。这产生了如图 18-8 所示的结果。
图 18-8。
格式化数字值
number管道是位置敏感的,这意味着相同的格式参数将根据用户的区域设置产生不同格式的结果。Angular 应用默认使用en-US语言环境,并要求显式加载其他语言环境,如清单 18-16 所示。
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { ProductComponent } from "./component";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { PaAttrDirective } from "./attr.directive";
import { PaModel } from "./twoway.directive";
import { PaStructureDirective } from "./structure.directive";
import { PaIteratorDirective } from "./iterator.directive";
import { PaCellColor } from "./cellColor.directive";
import { PaCellColorSwitcher } from "./cellColorSwitcher.directive";
import { ProductTableComponent } from "./productTable.component";
import { ProductFormComponent } from "./productForm.component";
import { PaToggleView } from "./toggleView.component";
import { PaAddTaxPipe } from "./addTax.pipe";
import { PaCategoryFilterPipe } from "./categoryFilter.pipe";
import { LOCALE_ID } from "@angular/core";
import localeFr from '@angular/common/locales/fr';
import { registerLocaleData } from '@angular/common';
registerLocaleData(localeFr);
@NgModule({
imports: [BrowserModule, FormsModule, ReactiveFormsModule],
declarations: [ProductComponent, PaAttrDirective, PaModel,
PaStructureDirective, PaIteratorDirective,
PaCellColor, PaCellColorSwitcher, ProductTableComponent,
ProductFormComponent, PaToggleView, PaAddTaxPipe,
PaCategoryFilterPipe],
providers: [{ provide: LOCALE_ID, useValue: "fr-FR" }],
bootstrap: [ProductComponent]
})
export class AppModule { }
Listing 18-16.Setting the Locale in the app.module.ts File in the src/app Folder
设置语言环境需要从包含每个地区数据的模块中导入所需的语言环境,并通过调用从@angular/common模块中导入的registerLocaleData函数来注册它。在清单中,我已经导入了fr-FR地区,这是针对法语的,因为在法国人们说法语。最后一步是配置providers属性,我在第二十章中描述过,但是清单 18-16 中配置的效果是启用fr-FR区域设置,这改变了数值的格式,如图 18-9 所示。
图 18-9。
区分区域设置的格式
格式化货币值
currency管道格式化代表货币数量的number值。清单 18-6 使用这个管道引入主题,清单 18-17 显示了相同管道的另一个应用,但是增加了数字格式说明符。
<table class="table table-sm table-bordered table-striped">
<tr><th></th><th>Name</th><th>Category</th><th>Price</th><th></th></tr>
<tr *paFor="let item of getProducts(); let i = index; let odd = odd;
let even = even" [class.bg-info]="odd" [class.bg-warning]="even">
<td style="vertical-align:middle">{{i + 1}}</td>
<td style="vertical-align:middle">{{item.name}}</td>
<td style="vertical-align:middle">{{item.category}}</td>
<td style="vertical-align:middle">
{{item.price | currency:"USD":"symbol":"2.2-2" }}
</td>
<td class="text-center">
<button class="btn btn-danger btn-sm" (click)="deleteProduct(item.id)">
Delete
</button>
</td>
</tr>
</table>
Listing 18-17.Using the currency Pipe in the productTable.component.html File in the src/app Folder
可以使用四个参数配置货币管道,如表 18-6 所述。
表 18-6。
Web 窗体代码块的类型
|名字
|
描述
|
| --- | --- |
| currencyCode | 此字符串参数使用 ISO 4217 代码指定货币。如果省略该参数,默认值为USD。在 http://en.wikipedia.org/wiki/ISO_4217 可以看到货币代码列表。 |
| display | 该字符串指示是否应显示货币符号或代码。支持的值有code(使用货币代码)symbol(使用货币符号)symbol-narrow(显示货币有窄宽符号时的简洁形式)。默认值为symbol。 |
| digitInfo | 该字符串参数指定了数字的格式,使用了与number管道支持的相同的格式指令,如“格式化数字”一节所述。 |
| locale | 此字符串参数指定货币的区域设置。这默认为LOCALE_ID值,其配置如清单 18-16 所示。 |
清单 18-17 中指定的参数告诉管道使用美元作为货币(它有 ISO 代码USD),在输出中显示符号而不是代码,并格式化数字,使其至少有两位整数和两位小数。
这个管道依赖国际化 API 来获取货币的详细信息——尤其是它的符号——但是不会自动选择货币来反映用户的区域设置。
这意味着数字的格式和货币符号的位置受应用的区域设置影响,而不管管道指定的货币是什么。示例应用仍然被配置为使用fr-FR语言环境,它产生如图 18-10 所示的结果。
图 18-10。
区分位置的货币格式
为了恢复到默认的语言环境,清单 18-18 从应用的根模块中删除了fr-FR设置。
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { ProductComponent } from "./component";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { PaAttrDirective } from "./attr.directive";
import { PaModel } from "./twoway.directive";
import { PaStructureDirective } from "./structure.directive";
import { PaIteratorDirective } from "./iterator.directive";
import { PaCellColor } from "./cellColor.directive";
import { PaCellColorSwitcher } from "./cellColorSwitcher.directive";
import { ProductTableComponent } from "./productTable.component";
import { ProductFormComponent } from "./productForm.component";
import { PaToggleView } from "./toggleView.component";
import { PaAddTaxPipe } from "./addTax.pipe";
import { PaCategoryFilterPipe } from "./categoryFilter.pipe";
import { LOCALE_ID } from "@angular/core";
import localeFr from '@angular/common/locales/fr';
import { registerLocaleData } from '@angular/common';
registerLocaleData(localeFr);
@NgModule({
imports: [BrowserModule, FormsModule, ReactiveFormsModule],
declarations: [ProductComponent, PaAttrDirective, PaModel,
PaStructureDirective, PaIteratorDirective,
PaCellColor, PaCellColorSwitcher, ProductTableComponent,
ProductFormComponent, PaToggleView, PaAddTaxPipe,
PaCategoryFilterPipe],
//providers: [{ provide: LOCALE_ID, useValue: "fr-FR" }],
bootstrap: [ProductComponent]
})
export class AppModule { }
Listing 18-18.Removing the locale Setting in the app.module.ts File in the src/app Folder
图 18-11 显示了结果。
图 18-11。
格式化货币值
格式化百分比
percent管道将number值格式化为百分比,其中 0 到 1 之间的值格式化为 0 到 100%。这个管道有可选参数,用于指定数字格式选项,使用与number管道相同的格式,并覆盖默认的区域设置。清单 18-19 重新引入了定制销售税过滤器,并用内容被百分比过滤器格式化的option元素填充关联的select元素。
<div>
<label>Tax Rate:</label>
<select [value]="taxRate || 0" (change)="taxRate=$event.target.value">
<option value="0">None</option>
<option value="10">{{ 0.1 | percent }}</option>
<option value="20">{{ 0.2 | percent }}</option>
<option value="50">{{ 0.5 | percent }}</option>
<option value="150">{{ 1.5 | percent }}</option>
</select>
</div>
<table class="table table-sm table-bordered table-striped">
<tr><th></th><th>Name</th><th>Category</th><th>Price</th><th></th></tr>
<tr *paFor="let item of getProducts(); let i = index; let odd = odd;
let even = even" [class.bg-info]="odd" [class.bg-warning]="even">
<td style="vertical-align:middle">{{i + 1}}</td>
<td style="vertical-align:middle">{{item.name}}</td>
<td style="vertical-align:middle">{{item.category}}</td>
<td style="vertical-align:middle">
{{item.price | addTax:(taxRate || 0) | currency:"USD":"symbol":"2.2-2" }}
</td>
<td class="text-center">
<button class="btn btn-danger btn-sm" (click)="deleteProduct(item.id)">
Delete
</button>
</td>
</tr>
</table>
Listing 18-19.Formatting Percentages in the productTable.component.html File in the src/app Folder
大于 1 的值被格式化为大于 100%的百分比。你可以在图 18-12 所示的最后一项中看到这一点,其中值 1.5 产生 150%的格式化值。
图 18-12。
格式化百分比值
百分比值的格式是区分位置的,尽管不同地区之间的差异可能很细微。例如,虽然en-US语言环境会产生 10%这样的结果,数字和百分号相邻,但包括fr-FR在内的许多语言环境会产生10 %这样的结果,数字和百分号之间有一个空格。
格式化日期
date管道执行日期的位置敏感格式化。日期可以用 JavaScript Date对象来表示,比如一个代表自 1970 年以来的毫秒数的number值,或者一个格式良好的字符串。清单 18-20 向ProductTableComponent类添加了三个属性,每个属性都以date管道支持的格式之一对日期进行编码。
import { Component, Input, ViewChildren, QueryList } from "@angular/core";
import { Model } from "./repository.model";
import { Product } from "./product.model";
@Component({
selector: "paProductTable",
templateUrl: "productTable.component.html"
})
export class ProductTableComponent {
@Input("model")
dataModel: Model;
getProduct(key: number): Product {
return this.dataModel.getProduct(key);
}
getProducts(): Product[] {
return this.dataModel.getProducts();
}
deleteProduct(key: number) {
this.dataModel.deleteProduct(key);
}
taxRate: number = 0;
categoryFilter: string;
itemCount: number = 3;
dateObject: Date = new Date(2020, 1, 20);
dateString: string = "2020-02-20T00:00:00.000Z";
dateNumber: number = 1582156800000;
}
Listing 18-20.Defining Dates in the productTable.component.ts File in the src/app Folder
这三个属性描述了同一个日期,即 2020 年 2 月 20 日,但没有指定时间。在清单 18-21 中,我使用了date管道来格式化所有三个属性。
<div class="bg-info p-2 text-white">
<div>Date formatted from object: {{ dateObject | date }}</div>
<div>Date formatted from string: {{ dateString | date }}</div>
<div>Date formatted from number: {{ dateNumber | date }}</div>
</div>
<table class="table table-sm table-bordered table-striped">
<tr><th></th><th>Name</th><th>Category</th><th>Price</th><th></th></tr>
<tr *paFor="let item of getProducts(); let i = index; let odd = odd;
let even = even" [class.bg-info]="odd" [class.bg-warning]="even">
<td style="vertical-align:middle">{{i + 1}}</td>
<td style="vertical-align:middle">{{item.name}}</td>
<td style="vertical-align:middle">{{item.category}}</td>
<td style="vertical-align:middle">
{{item.price | addTax:(taxRate || 0) | currency:"USD":"symbol":"2.2-2" }}
</td>
<td class="text-center">
<button class="btn btn-danger btn-sm" (click)="deleteProduct(item.id)">
Delete
</button>
</td>
</tr>
</table>
Listing 18-21.Formatting Dates in the productTable.component.html File in the src/app Folder
管道计算出它正在处理的数据类型,解析该值以获得日期,然后格式化它,如图 18-13 所示。
图 18-13。
格式化日期
date管道接受一个指定应该使用的日期格式的参数。可以使用表 18-7 中描述的符号选择输出的单个日期部分。
表 18-7。
日期管道格式符号
|名字
|
描述
|
| --- | --- |
| y,yy | 这些符号选择年份。 |
| M、MMM、MMMM | 这些符号选择月份。 |
| d,dd | 这些符号选择日期(作为一个数字)。 |
| E、EE、EEEE | 这些符号选择日期(作为名称)。 |
| j,jj | 这些符号选择小时。 |
| h、hh、H、HH | 这些符号以 12 小时和 24 小时形式选择小时。 |
| m,mm | 这些符号选择分钟。 |
| s,ss | 这些符号选择秒。 |
| Z | 此符号选择时区。 |
表 18-7 中的符号以不同的简洁程度提供了对日期组件的访问,因此如果月份是二月,M将返回2,MM将返回02,MMM将返回Feb,而MMMM将返回February,假设您使用的是en-US区域设置。date管道还支持常用组合的预定义日期格式,如表 18-8 所述。
表 18-8。
预定义的日期管道格式
|名字
|
描述
|
| --- | --- |
| short | 这种格式相当于组件字符串yMdjm。它以简洁的格式显示日期,包括时间部分。 |
| medium | 这种格式相当于组件字符串yMMMdjms。它以更广泛的格式显示日期,包括时间部分。 |
| shortDate | 这种格式相当于组件字符串yMd。它以简洁的格式显示日期,不包括时间部分。 |
| mediumDate | 这种格式相当于组件字符串yMMMd。它以更广泛的格式显示日期,并排除了时间部分。 |
| longDate | 这种格式相当于组件字符串yMMMMd。它显示日期,但不包括时间部分。 |
| fullDate | 这种格式相当于组件字符串yMMMMEEEEd。它完全显示日期,但不包含日期格式。 |
| shortTime | 这种格式相当于组件字符串jm。 |
| mediumTime | 这种格式相当于组件字符串jms。 |
date管道还接受一个指定时区的参数和一个可用于覆盖区域设置的参数。清单 18-22 显示了使用预定义格式作为date管道的参数,以不同的方式呈现相同的日期。
...
<div class="bg-info p-2 text-white">
<div>Date formatted as shortDate: {{ dateObject | date:"shortDate" }}</div>
<div>Date formatted as mediumDate: {{ dateObject | date:"mediumDate" }}</div>
<div>Date formatted as longDate: {{ dateObject | date:"longDate" }}</div>
</div>
...
Listing 18-22.Formatting Dates in the productTable.component.html File in the src/app Folder
格式化参数被指定为文字字符串。注意格式字符串的大小写要正确,因为shortDate将被解释为表 18-8 中的预定义格式之一,但是shortdate(带有小写字母d)将被解释为表 18-7 中的一系列字符并产生无意义的输出。
Caution
日期解析/格式化是一个复杂而耗时的过程。因此,date管道的pure属性是true;因此,对Date对象的单个组件的更改不会触发更新。如果您需要反映日期显示方式的变化,那么您必须更改包含date管道的绑定所引用的Date对象。
日期格式是区分位置的,这意味着您将收到不同地区的不同组件。不要假设在一个地区有意义的日期格式在另一个地区会有任何意义。图 18-14 显示了en-US和fr-FR地区的格式化日期。
图 18-14。
区分位置的日期格式
更改字符串大小写
uppercase、lowercase和titlecase管道分别将字符串中的所有字符转换成大写或小写。清单 18-23 显示了应用于产品表中单元格的前两个管道。
<table class="table table-sm table-bordered table-striped">
<tr><th></th><th>Name</th><th>Category</th><th>Price</th><th></th></tr>
<tr *paFor="let item of getProducts(); let i = index; let odd = odd;
let even = even" [class.bg-info]="odd" [class.bg-warning]="even">
<td style="vertical-align:middle">{{i + 1}}</td>
<td style="vertical-align:middle">{{item.name | uppercase }}</td>
<td style="vertical-align:middle">{{item.category | lowercase }}</td>
<td style="vertical-align:middle">
{{item.price | addTax:(taxRate || 0) | currency:"USD":"symbol":"2.2-2" }}
</td>
<td class="text-center">
<button class="btn btn-danger btn-sm" (click)="deleteProduct(item.id)">
Delete
</button>
</td>
</tr>
</table>
Listing 18-23.Changing Character Case in the productTable.component.html File in the src/app Folder
这些管道使用标准的 JavaScript 字符串方法toUpperCase和toLowerCase,对区域设置不敏感,如图 18-15 所示。
图 18-15。
改变字符大小写
titlecase管道将每个单词的第一个字符大写,其余字符使用小写。清单 18-24 将titlecase管道应用于表格单元格。
<table class="table table-sm table-bordered table-striped">
<tr><th></th><th>Name</th><th>Category</th><th>Price</th><th></th></tr>
<tr *paFor="let item of getProducts(); let i = index; let odd = odd;
let even = even" [class.bg-info]="odd" [class.bg-warning]="even">
<td style="vertical-align:middle">{{i + 1}}</td>
<td style="vertical-align:middle">{{item.name | titlecase }}</td>
<td style="vertical-align:middle">{{item.category | lowercase }}</td>
<td style="vertical-align:middle">
{{item.price | addTax:(taxRate || 0) | currency:"USD":"symbol":"2.2-2" }}
</td>
<td class="text-center">
<button class="btn btn-danger btn-sm" (click)="deleteProduct(item.id)">
Delete
</button>
</td>
</tr>
</table>
Listing 18-24.Applying the Pipe in the productTable.component.html File in the src/app Folder
图 18-16 显示了管道的效果。
图 18-16。
使用标题盒管道
将数据序列化为 JSON
json管道创建一个数据值的 JSON 表示。这个管道不接受任何参数,它使用浏览器的JSON.stringify方法来创建 JSON 字符串。清单 18-25 应用这个管道来创建数据模型中对象的 JSON 表示。
<div class="bg-info p-2 text-white">
<div>{{ getProducts() | json }}</div>
</div>
<table class="table table-sm table-bordered table-striped">
<tr><th></th><th>Name</th><th>Category</th><th>Price</th><th></th></tr>
<tr *paFor="let item of getProducts(); let i = index; let odd = odd;
let even = even" [class.bg-info]="odd" [class.bg-warning]="even">
<td style="vertical-align:middle">{{i + 1}}</td>
<td style="vertical-align:middle">{{item.name | titlecase }}</td>
<td style="vertical-align:middle">{{item.category | lowercase }}</td>
<td style="vertical-align:middle">
{{item.price | addTax:(taxRate || 0) | currency:"USD":"symbol":"2.2-2" }}
</td>
<td class="text-center">
<button class="btn btn-danger btn-sm" (click)="deleteProduct(item.id)">
Delete
</button>
</td>
</tr>
</table>
Listing 18-25.Creating a JSON String in the productTable.component.html File in the src/app Folder
这个管道在调试过程中很有用,它的 decorator 的pure属性是false,因此应用中的任何更改都会导致管道的transform方法被调用,从而确保显示甚至是集合级别的更改。图 18-17 显示了从示例应用的数据模型中的对象生成的 JSON。
图 18-17。
生成用于调试的 JSON 字符串
切片数据数组
slice管道对数组或字符串进行操作,并返回它所包含的元素或字符的子集。这是一个不纯的管道,这意味着它将反映它所操作的数据对象中发生的任何变化,但也意味着切片操作将在应用中的任何变化之后执行,即使该变化与源数据无关。
由slice管道选择的对象或角色由两个参数指定,如表 18-9 所述。
表 18-9。
切片管道参数
|名字
|
描述
|
| --- | --- |
| start | 必须指定此参数。如果该值为正数,则结果中包含的项的起始索引从数组中的第一个位置开始计数。如果该值为负,则管道从数组末尾开始倒数。 |
| end | 该可选参数用于指定结果中应包含多少来自start索引的项目。如果省略该值,将包括所有在start索引之后(或在负值情况下之前)的项目。 |
清单 18-26 展示了slice管道与select元素的结合使用,后者指定了产品表中应该显示多少个项目。
<div>
<label>Number of items:</label>
<select [value]="itemCount || 1" (change)="itemCount=$event.target.value">
<option *ngFor="let item of getProducts(); let i = index" [value]="i + 1"
[selected]="(i + 1) === itemCount">
{{i + 1}}
</option>
</select>
</div>
<table class="table table-sm table-bordered table-striped">
<tr><th></th><th>Name</th><th>Category</th><th>Price</th><th></th></tr>
<tr *paFor="let item of getProducts() | slice:0:(itemCount || 1);
let i = index; let odd = odd; let even = even"
[class.bg-info]="odd" [class.bg-warning]="even">
<td style="vertical-align:middle">{{i + 1}}</td>
<td style="vertical-align:middle">{{item.name | titlecase }}</td>
<td style="vertical-align:middle">{{item.category | lowercase }}</td>
<td style="vertical-align:middle">
{{item.price | addTax:(taxRate || 0) | currency:"USD":"symbol":"2.2-2" }}
</td>
<td class="text-center">
<button class="btn btn-danger btn-sm" (click)="deleteProduct(item.id)">
Delete
</button>
</td>
</tr>
</table>
Listing 18-26.Using the slice Pipe in the productTable.component.html File in the src/app Folder
用通过ngFor指令创建的option元素填充select元素。这个指令不直接支持特定次数的迭代,所以我使用了index变量来生成所需的值。select元素设置了一个名为itemCount的属性,用作slice管道的第二个参数,如下所示:
...
<tr *paFor="let item of getProducts() | slice:0:(itemCount || 1);
let i = index; let odd = odd; let even = even"
[class.bg-info]="odd" [class.bg-warning]="even">
...
其效果是通过改变select元素显示的值来改变产品表中显示的商品数量,如图 18-18 所示。
图 18-18。
使用切片管道
格式化键/值对
keyvalue管道对一个对象或一个地图进行操作,并返回一系列键/值对。序列中的每个对象都被表示为具有key和value属性的对象,清单 18-27 演示了如何使用管道来枚举由getProducts方法返回的数组内容。
<table class="table table-sm table-bordered table-striped">
<tr><th>Key</th><th>Value</th></tr>
<tr *paFor="let item of getProducts() | keyvalue">
<td>{{ item.key }}</td>
<td>{{ item.value | json }}</td>
</tr>
</table>
Listing 18-27.Using the keyvalue Pipe in the productTable.component.html File in the src/app Folder
在数组上使用时,键是数组索引,值是数组中的对象。使用json过滤器格式化数组中的对象,产生如图 18-19 所示的结果。
图 18-19。
使用键值管道
选择值
i18nSelect管道根据一个值选择一个字符串值,允许向用户显示上下文相关的值。值和字符串之间的映射被定义为一个简单的映射,如清单 18-28 所示。
import { Component, Input, ViewChildren, QueryList } from "@angular/core";
import { Model } from "./repository.model";
import { Product } from "./product.model";
@Component({
selector: "paProductTable",
templateUrl: "productTable.component.html"
})
export class ProductTableComponent {
@Input("model")
dataModel: Model;
getProduct(key: number): Product {
return this.dataModel.getProduct(key);
}
getProducts(): Product[] {
return this.dataModel.getProducts();
}
deleteProduct(key: number) {
this.dataModel.deleteProduct(key);
}
taxRate: number = 0;
categoryFilter: string;
itemCount: number = 3;
dateObject: Date = new Date(2020, 1, 20);
dateString: string = "2020-02-20T00:00:00.000Z";
dateNumber: number = 1582156800000;
selectMap = {
"Watersports": "stay dry",
"Soccer": "score goals",
"other": "have fun"
}
}
Listing 18-28.Mapping Values to Strings in the productTable.component.ts File in the src/app Folder
当与其他值不匹配时,另一个映射用作后备。在清单 18-29 中,我已经应用了管道来选择向用户显示的消息。
<table class="table table-sm table-bordered table-striped">
<tr><th>Name</th><th>Category</th><th>Message</th></tr>
<tr *paFor="let item of getProducts()">
<td>{{ item.name }}</td>
<td>{{ item.category }}</td>
<td>Helps you {{ item.category | i18nSelect:selectMap }} </td>
</tr>
</table>
Listing 18-29.Using the Pipe in the productTable.component.html File in the src/app Folder
管道以地图作为参数,并产生如图 18-20 所示的响应。
图 18-20。
使用 i18n 选择管道选择值
多元化价值观
i18nPlural管道用于选择描述数值的表达式。值和表达式之间的映射被表示为一个简单的映射,如清单 18-30 所示。
import { Component, Input, ViewChildren, QueryList } from "@angular/core";
import { Model } from "./repository.model";
import { Product } from "./product.model";
@Component({
selector: "paProductTable",
templateUrl: "productTable.component.html"
})
export class ProductTableComponent {
// ...other statments omitted for brevity...
selectMap = {
"Watersports": "stay dry",
"Soccer": "score goals",
"other": "have fun"
}
numberMap = {
"=1": "one product",
"=2": "two products",
"other": "# products"
}
}
Listing 18-30.Mapping Numbers to Strings in the productTable.component.ts File in the src/app Folder
每个映射都用等号后跟数字来表示。other值是一个后备,它产生的结果可以引用使用#占位符的数值。清单 18-31 显示了使用示例映射可以产生的结果。
<table class="table table-sm table-bordered table-striped">
<tr><th>Name</th><th>Category</th><th>Message</th></tr>
<tr *paFor="let item of getProducts()">
<td>{{ item.name }}</td>
<td>{{ item.category }}</td>
<td>Helps you {{ item.category | i18nSelect:selectMap }} </td>
</tr>
</table>
<div class="bg-info text-white p-2">
<div>There are {{ 1 | i18nPlural:numberMap }} </div>
<div>There are {{ 2 | i18nPlural:numberMap }} </div>
<div>There are {{ 100 | i18nPlural:numberMap }} </div>
</div>
Listing 18-31.Using the Pipe in the productTable.component.html File in the src/app Folder
映射被指定为管道的参数,列表 18-31 中的值产生如图 18-21 所示的结果。
图 18-21。
使用 i18n 管道选择值
摘要
在这一章中,我介绍了管道,并解释了如何使用它们来转换数据值,以便在模板中呈现给用户。我演示了创建定制管道的过程,解释了一些管道是纯的而另一些不是,并演示了 Angular 提供的用于处理常见任务的内置管道。在下一章中,我将介绍服务,它可以用来简化 Angular 应用的设计,并允许构建模块轻松地进行协作。