PrimeNG-Angular-UI-开发-全-

PrimeNG:Angular UI 开发(全)

原文:zh.annas-archive.org/md5/F2BA8B3AB075A37F3A10CF12CD37157B

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

PrimeNG 是一个领先的 Angular 单页应用程序的 UI 组件库,拥有 80 多个丰富的 UI 组件。PrimeNG 在 Angular 世界取得了巨大的成功,这要归功于其在短时间内的积极开发。它是一个快速发展的库,与最新的 Angular 版本保持一致。与竞争对手不同,PrimeNG 是专为企业应用程序而创建的。本书为那些想要使用这种流行的开发堆栈开发实时单页应用程序的读者提供了一个快速入门。

本书共有十章,以对单页应用程序的简短介绍开始。TypeScript 和 Angular 基础是 PrimeNG 主题的重要第一步。随后,它讨论了如何以不同的方式设置和配置 PrimeNG 应用程序以进行快速启动。一旦环境准备好,就是学习 PrimeNG 开发的时候了,从主题和响应式布局的概念开始。读者将学习增强的输入、选择和按钮组件,然后是各种面板、数据迭代、覆盖、消息和菜单组件。不会错过对表单元素的验证。额外的一章演示了如何为真实世界的应用程序创建地图和图表组件。除了内置的 UI 组件及其特性,读者还将看到如何根据自己的需求定制组件。

杂项用例在单独的章节中进行讨论。举几个例子:文件上传,拖放,阻止 AJAX 调用期间的页面片段,CRUD 示例实现等。本章超越常见主题,实现了自定义组件,并讨论了@ngrx/store 中一种流行的状态管理形式。最后一章描述了单元测试和端到端测试。为了确保 Angular 和 PrimeNG 开发无误,将通过系统示例解释完整的测试框架。加速单元测试和调试 Angular 应用程序的技巧将为本书画上句号。

本书还着重介绍了如何避免一些常见的陷阱,并展示了关于高效 Angular 和 PrimeNG 开发的最佳实践和技巧。在本书结束时,读者将了解如何在 Angular 应用程序中使用 PrimeNG,并准备好使用丰富的 PrimeNG 组件创建真实世界的 Angular 应用程序。

本书内容

第一章,开始使用 Angular 和 PrimeNG,为您提供了深入学习后续章节所需的知识。本章概述了本书中使用的 TypeScript 和 Angular 构造。不可能详细解释众多功能。相反,我们将集中讨论最重要的关键概念,如类型、模板语法、装饰器、组件通信场景、模块化和生命周期钩子。之后,本章将介绍 PrimeNG,其中包括丰富的 Angular 2+ UI 组件,并展示使用 SystemJS 和 Webpack 加载器以及 Angular CLI 三种可能的项目设置。

第二章,主题概念和布局,介绍了 PrimeNG 主题和相关概念。读者将了解 PrimeNG 组件的主题。本章将详细介绍结构和皮肤 CSS 之间的区别,使用 SASS 时的推荐项目结构,安装和自定义 PrimeNG 主题以及创建新主题。对响应式布局的两种变体、PrimeNG 自己的网格系统和 Bootstrap 的 flexbox 网格进行了讨论。

第三章,增强输入和选择,解释了如何使用 PrimeNG 中提供的输入和选择组件。这些组件是每个 Web 应用程序的主要部分。PrimeNG 提供了近 20 个数据输入组件,扩展了原生 HTML 元素,具有用户友好的界面、皮肤能力、验证和许多其他有用的功能。

第四章,按钮和面板组件,涵盖了各种按钮,如单选按钮、拆分按钮、切换按钮和选择按钮,以及面板组件,如工具栏、手风琴、字段集和选项卡视图。面板组件充当容器组件,允许对其他组件进行分组。本章详细介绍了配置面板组件的各种设置。

第五章,数据迭代组件,涵盖了使用 PrimeNG 提供的数据迭代组件来可视化数据的基本和高级功能,包括 DataTable、DataList、PickList、OrderList、Tree 和 TreeTable。讨论的功能包括排序、分页、过滤、延迟加载以及单个和多个选择。高级数据可视化与日程安排和 DataScroller 组件也将被演示。

第六章,令人惊叹的覆盖和消息,展示了内容的各种变化,显示在模态或非模态覆盖中,如对话框、LightBox 和覆盖面板。当内容显示在上述覆盖中时,用户不会离开页面流。覆盖组件会覆盖页面上的其他组件。PrimeNG 还提供通知组件来显示任何消息或咨询信息。这些组件也将被描述。

第七章,无尽的菜单变化,解释了几种菜单变化。PrimeNG 的菜单满足所有主要需求。它们具有各种方面--静态、动态、分层、混合、类似 iPod 等等,无所不包。读者将看到许多关于菜单结构、配置选项、自定义以及与其他组件集成的示例。

第八章,创建图表和地图,涵盖了使用 PrimeNG 丰富的图表功能和基于 Google 地图的地图来创建可视化图表的方法。PrimeNG 提供基本和高级的图表功能,具有易于使用和用户友好的图表基础设施。除了标准图表,本章还展示了用于可视化分层数据的特殊组织图表。在整个章节中,还将解释绘制折线、多边形、处理标记和事件等地图能力。

第九章,杂项用例和最佳实践,介绍了 PrimeNG 库的更多有趣功能。您将了解文件上传、拖放功能、显示图像集合、实际 CRUD 实现、延迟页面加载、阻止页面片段、显示带有受保护路由的确认对话框等。额外的部分将详细介绍构建可重用组件和开发自定义向导组件的完整过程。阅读完本章后,读者将了解使用@ngrx/store 进行最新状态管理,并看到 Redux 架构的好处。

第十章,创建健壮的应用程序,描述了单元测试和端到端测试。本章以使用 Jasmine 和 Karma 设置测试环境开始。为了确保 Angular 和 PrimeNG 开发无缺陷,将通过系统示例解释完整的测试框架。加快测试和调试 Angular 应用程序的技巧结束本章。

您需要为本书准备什么

本书将指导您安装所有需要遵循示例的工具。您需要安装 npm 来有效地运行本书中的代码示例。

这本书适合谁

这本书适用于所有希望学习使用 PrimeNG 组件库创建现代基于 Angular 的单页面应用程序的人。这本书是初学者到高级用户的不错选择,他们真正想要学习现代 Angular 应用程序。本书的先决条件是对 Angular 2+、以及 TypeScript 和 CSS 技能有一些基本的了解。

约定

在这本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是这些样式的一些示例及其含义的解释。文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:"接口以关键字interface开头。"

代码块设置如下:

let x: [string, number];
x = ["age", 40]; // ok
x = [40, "age"] ; // error

任何命令行输入或输出都按如下方式编写:

npm install -g typescript

新术语重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会在文本中以这种方式出现:"文件上传还提供了一个更简单的 UI,只需一个按钮选择。"

警告或重要提示会显示为这样。提示和技巧会显示为这样。

第一章:开始使用 Angular 和 PrimeNG

本书假定读者具有一些 TypeScript 和 Angular 2 的基本知识。无论如何,我们希望向读者概述本书中使用的最重要的 TypeScript 和 Angular 关键概念。我们将总结 TypeScript 和 Angular 的特性,并以可理解、简单但深入解释的方式呈现它们。撰写本书时,当前的 TypeScript 和 Angular 版本分别为 2.3.x 和 4.2.x。读者还将首次接触 PrimeNG UI 库,并通过三种不同的方式获得项目设置经验。在本章结束时,读者将能够运行第一个基于 Angular 和 PrimeNG 的 Web 应用程序。

在本章中,我们将涵盖以下主题:

  • TypeScript 基础知识

  • 高级类型、装饰器和编译器选项

  • Angular 速查表-关键概念概述

  • Angular 的模块化和生命周期钩子

  • 使用 SystemJS 运行 PrimeNG

  • 使用 Webpack 设置 PrimeNG 项目

  • 使用 Angular CLI 设置 PrimeNG 项目

TypeScript 基础知识

Angular 2 及更高版本使用了 ECMAScript 2015/2016 和 TypeScript 的功能。新的 ECMAScript 标准针对现代浏览器,并有助于编写更强大、干净和简洁的代码。您还可以在任何其他不太现代的浏览器中使用这些功能,例如core-jsgithub.com/zloirock/core-js)等 Polyfills。但是,为什么我们需要使用 TypeScript 呢?

TypeScript(www.typescriptlang.org)是由微软开发的一种带类型的语言,也是 JavaScript 的超集。可以说 TypeScript 是一种带有可选静态类型的高级 JavaScript。TypeScript 代码不会被浏览器处理,而是需要通过 TypeScript 编译器将其转换为 JavaScript。这种转换被称为编译转译。TypeScript 编译器将.ts文件转译为.js文件。TypeScript 的主要优点如下:

  • 类型有助于在开发过程中找到并修复许多错误。这意味着在运行时会有更少的错误。

  • 许多现代 ECMAScript 功能都被原生支持。根据路线图,预计会有更多功能被支持(github.com/Microsoft/TypeScript/wiki/Roadmap)。

  • 出色的工具和 IDE 支持使编码成为一种愉悦。

  • 维护和重构 TypeScript 应用比使用无类型的 JavaScript 编写的应用更容易。

  • 开发人员喜欢 TypeScript,因为它支持面向对象的编程模式,如接口、类、枚举、泛型等。

  • 最后但并非最不重要的是,Angular 2+和 PrimeNG 都是用 TypeScript 编写的。

还要记住以下几点很重要:

  • TypeScript 语言规范表示,每个 JavaScript 程序也是一个 TypeScript 程序。因此,从 JavaScript 迁移到 TypeScript 代码很容易。

  • 即使报告了任何错误,TypeScript 编译器也会生成输出。在下一节高级类型、装饰器和编译器选项中,我们将看到如何在出现错误时禁止生成 JavaScript。

学习 TypeScript 语言的最佳方法是什么?TypeScript 官方主页上有一本官方手册,与最新发布的版本保持一致。可以通过 TypeScript playground(www.typescriptlang.org/play)进行实践学习,该工具可以即时编译在浏览器中输入的 TypeScript 代码,并将其与生成的 JavaScript 代码并排显示:

或者,您可以在命令行中输入以下命令全局安装 TypeScript。

npm install -g typescript

全局安装意味着 TypeScript 编译器tsc可以在任何项目中被访问和使用。需要安装 Node.js 和 npm。Node.js 是 JavaScript 运行时环境(nodejs.org)。npm 是包管理器。它随 Node.js 一起发布,但也可以单独安装。之后,您可以通过输入以下命令将一个或多个.ts文件转译为.js文件:

tsc some.ts another.ts

这将生成两个文件,some.jsanother.js

基本类型

TypeScript 公开了基本类型,以及一些额外的类型。让我们通过这些例子来探索类型系统。

  • Boolean:该类型是原始的 JavaScript 布尔值:
let success: boolean = true;

  • Number:该类型是原始的 JavaScript 数字:
let count: number = 20;

  • String:该类型是原始的 JavaScript 字符串:
let message: string = "Hello world";

  • Array:该类型是一个值的数组。有两种等价的表示法:
let ages: number[] = [31, 20, 65];
let ages: Array<number> = [31, 20, 65];

  • Tuple:该类型表示值的异构数组。Tuple可以存储不同类型的多个字段:
let x: [string, number];
x = ["age", 40];    // ok
x = [40, "age"] ;   // error

  • Any:该类型是任何类型。在编写应用程序时需要描述不知道的变量类型时非常有用。您可以将任意类型的值赋给any类型的变量。反过来,any类型的值可以赋给任意类型的变量:
let some: any = "some";
some = 10000;
some = false;

let success: boolean = some;
let count: number = some;
let message: string = some;

  • Void:该类型表示没有any类型。这种类型通常用作函数的返回类型:
function doSomething(): void {
  // do something
}

  • Nullable:这些类型表示两种特定类型,nullundefined,它们是每种类型的有效值。这意味着它们可以分配给任何其他类型。这并不总是理想的。TypeScript 提供了一种通过将编译器选项strictNullChecks设置为true来更改此默认行为的可能性。现在,您必须显式使用联合类型(稍后解释)包含Nullable类型,否则将会出错:
let x: string = "foo";
x = null;    // error
let y: string | null = "foo";
y = null;    // ok

有时,您可能希望告诉编译器,您比它更了解类型,它应该信任您。例如,想象一种情况,您通过 HTTP 接收数据,并且确切地知道接收到的数据的结构。当然,编译器不知道这样的结构。在这种情况下,您希望在将数据分配给变量时关闭类型检查。这可以通过所谓的类型断言来实现。类型断言类似于其他语言中的类型转换,但不检查数据。您可以使用尖括号as语法来实现。

let element = <HTMLCanvasElement> document.getElementById('canvas');
let element = document.getElementById('canvas') as HTMLCanvasElement;

接口、类和枚举

接口是一种将特定结构/形状命名的方式,以便我们以后可以引用它作为一种类型。它在我们的代码中定义了一个合同。接口以关键字interface开头。让我们举个例子:

interface Person {
  name: string
  children?: number
  isMarried(): boolean
  (): void
}

指定的接口Person具有以下内容:

  • 类型为stringname属性。

  • 类型为number的可选属性children。可选属性由问号表示,可以省略。

  • 返回boolean值的isMarried方法。

  • 返回空值的匿名(未命名)方法。

Typescript 允许您使用[index: type]语法来指定基于stringnumber类型的键/值对集合。接口非常适合这样的数据结构。例如,考虑以下语法:

interface Dictionary {
  [index: number]: string
}

接口仅在编译时由 TypeScript 编译器使用,然后被移除。接口不会出现在最终的 JavaScript 输出中。通常,在输出中不会出现类型。您可以在前面提到的 TypeScript playground 中看到这一点!

除了接口,还有描述对象的。类充当实例化特定对象的模板。TypeScript 类的语法几乎与 ECMAScript 2015 中的原生类完全相同,并带有一些方便的附加功能。在 TypeScript 中,您可以使用publicprivateprotectedreadonly访问修饰符:

class Dog {
 private name: string;    // can only be accessed within this class
 readonly owner: string = "Max";    // can not be modified
 constructor(name: string) {this.name = name;}
 protected sayBark() { }
}

let dog = new Dog("Sam");
dog.sayBark();  // compiler error because method 'sayBark' is protected and
                // only accessible within class 'Dog' and its subclasses.

省略修饰符的成员默认为public。如果使用static关键字声明属性或方法,则无需创建实例即可访问它们。

类可以是抽象的,这意味着它可能不能直接实例化。抽象类以关键字abstract开头。类可以实现一个接口,也可以扩展另一个类。我们可以使用implementsextends关键字分别实现这一点。如果一个类实现了某个接口,它必须采用该接口的所有属性;否则,您将收到有关缺少属性的错误:

interface Animal {
  name: string;
}

class Dog implements Animal {
  name: string;
  // do specific things
}

class Sheepdog extends Dog {
  // do specific things }

包含构造函数的派生类必须调用super()super()调用在基类上执行构造函数。

可以使用修饰符声明constructor参数。结果,一个成员将在一个地方被创建和初始化:

class Dog {
  constructor(private name: string) { }

  // you can now access the property name by this.name
}

在 Angular 中,当我们将服务注入到组件中时,经常会使用这种简化的语法。Angular 的服务通常在组件的构造函数中使用private修饰符声明。

这里要提到的最后一个基本类型是枚举。枚举允许我们定义一组命名常量。枚举成员与数字值相关联(从 0 开始):

enum Color {
  Red,
  Green,
  Blue }

var color = Color.Red;    // color has value 0

函数

函数签名中的参数和返回值也可以进行类型化。类型保护您免受 JavaScript 错误的影响,因为编译器会在构建时及时警告您使用错误类型:

function add(x: number, y: number): number {
  return x + y;
}

函数类型是声明函数类型的一种方式。要显式声明函数类型,您应该使用关键字varlet,一个变量名,一个冒号,一个参数列表,一个 fat 箭头=>,和函数的返回类型:

var fetchName: (division: Division, customer: Customer) => string;

现在,您必须提供此声明的实现:

fetchName = function (division: Division, customer: Customer): string {
  // do something
}

这种技术对于回调特别有用。想象一个根据某些标准过滤数组的过滤函数。一个确切的标准可以封装在传入的回调函数中,作为谓词:

function filter(arr: number[], callback: (item: number) => boolean): number[] {
  let result: number[] = [];
  for (let i = 0; i < arr.length; i++) {
    if (callback(arr[i])) {
      result.push(arr[i]);
    }
  }
  return result;
}

可能的函数调用与特定回调可以如下所示:

let result = filter([1, 2, 3, 4], (item: number) => item > 3);

在 TypeScript 中,假定每个函数参数都是必需的。有两种方法可以将参数标记为可选的(可选参数在调用函数时可以省略)。

  • 在参数名称后使用问号:
function doSomething(param1: string, param2?: string) {
 // ... }

  • 使用参数的默认值(ECMAScript 2015 功能),当没有提供值时会应用默认值:
function doSomething(param1: string, param2 = "some value") {
 // ... }

现在,您可以只使用一个值调用此函数。

doSomething("just one value");

泛型

在 TypeScript 中,您可以像其他编程语言一样定义通用函数、接口和类。通用函数在尖括号中列出类型参数:

function reverseAndMerge<T>(arr1: T[], arr2: T[]): T[] {
  return arr1.reverse().concat(arr2.reverse());
}

let arr1: number[] = [1, 2, 3];
let arr2: number[] = [4, 5, 6];
let arr = reverseAndMerge(arr1, arr2);

这样的通用函数也可以使用通用接口来定义。reverseAndMerge的函数签名与以下通用接口兼容:

interface GenericArrayFn<T> {
  (arr1: T[], arr2: T[]): T[];
}

let arr: GenericArrayFn<number> = reverseAndMerge;

请注意,尖括号中的通用类型参数列表跟随函数和接口的名称。对于类也是如此:

class GenericValue<T> {
  constructor(private value: T) { }
  increment: (x: T) => T;
  decrement: (x: T) => T;
}

let genericValue = new GenericValue<number>(5);
genericValue.increment = function (x) {return ++x;};
genericValue.decrement = function (x) {return --x;};

模块

ECMAScript 2015 引入了内置模块。模块的特性如下:

  • 每个模块都在自己的文件中定义。

  • 模块中定义的函数或变量在外部是不可见的,除非你明确导出它们。

  • 您可以在任何变量、函数或类声明前放置export关键字来从模块中导出它。

  • 您可以使用import关键字来使用导出的变量、函数或类声明。

  • 模块是单例的。即使多次导入,模块的实例也只有一个。

以下列出了一些导出的可能性:

// export data
export let color: string = "red";

// export function
export function sum(num1: number, num2: number) {
  return num1 + num1;
}

// export class
export class Rectangle {
  constructor(private length: number, private width: number) { }
}

您可以声明一个变量、函数或类,并稍后导出它。您还可以使用as关键字来重命名导出。新名称是用于导入的名称:

class Rectangle {
  constructor(private height: number, private width: number) { }
}

export {Rectangle as rect};

一旦您有了带有导出的模块,您可以使用import关键字在另一个模块中访问其功能:

import {sum} from "./lib.js";
import {Rect, Circle} from "./lib.js";

let sum = sum(1, 2);
let rect = new Rect(10, 20);

有一种特殊情况允许您将整个模块作为单个对象导入。所有导出的变量、函数和类都作为该对象的属性可用:

import * as lib from "./lib.js";

let sum = lib.sum(1, 2);

导入可以使用as关键字重命名,并在新名称下使用:

import {sum as add} from "./lib.js";

let sum = add(1, 2);

高级类型、装饰器和编译器选项

TypeScript 具有更多类型和高级构造,例如装饰器和类型定义文件。本章概述了高级主题,并展示了如何自定义编译器配置。

联合类型和类型别名

联合类型描述了可以是多种类型之一的值。竖线|用作值可以具有的每种类型的分隔符。例如,number | string是一个可以是数字或字符串的值的类型。对于这样的值,我们只能访问联合中所有类型的公共成员。以下代码有效,因为length属性存在于字符串和数组中:

var value: string | string[] = 'some';
let length = value.length;

下一个代码片段出现错误,因为model属性在Bike类型上不存在:

interface Bike {
  gears: number;
}

interface Car {
  gears: number;
  model: string;
}

var transport: Bike | Car = {gears: 1};
transport.model = "Audi";    // compiler error

类型别名用作现有类型或类型组合的替代名称。它不创建新类型。类型别名以type关键字开头。

type PrimitiveArray = Array<string|number|boolean>;
type Callback = () => number;
type PrimitiveArrayOrCallback = PrimitiveArray | Callback;

类型别名可用于提高代码可读性,例如,在函数参数列表中。

function doSomething(n: PrimitiveArrayOrCallback): number {
  ...
}

类型别名也可以是通用的,并创建棘手的类型,这些类型无法使用接口创建。

类型推断

类型推断在类型没有明确提供时使用。例如在以下语句中:

var x = "hello";
var y = 99;

这些没有显式类型注释。TypeScript 可以推断出x是一个字符串,y是一个数字。正如你所看到的,如果编译器能够推断出类型,那么类型可以被省略。TypeScript 不断改进类型推断。当数组中存在多种类型的元素时,它会尝试猜测最佳公共类型。变量animal的类型是Dog[],其中Sheepdog extends Dog

let animal = [new Dog(), new Sheepdog()];

下一个数组的最佳公共类型是(Dog | Fish)[],因为类Fish没有扩展到任何其他类:

class Fish {
  kind: string;
}

let animal = [new Dog(), new Sheepdog(), new Fish()];

类型推断也用于函数。在下一个示例中,编译器可以推断出函数的参数类型(string)和返回值类型(boolean):

let isEmpty: (param: string) => boolean;
isEmpty = function(x) {return x === null || x.length === 0};

装饰器

装饰器在 ECMAScript 2016 中提出(github.com/wycats/javascript-decorators)。它们类似于 Java 注解--它们还向类声明、方法、属性和函数的参数添加元数据,但它们更加强大。它们为它们的目标添加了新的行为。使用装饰器,我们可以在目标执行之前、之后或周围运行任意代码,就像面向方面的编程一样,甚至用新定义替换目标。在 TypeScript 中,您可以装饰构造函数、方法、属性和参数。每个装饰器都以@字符开头,后面跟着装饰器的名称。

它是如何在底层工作的,以其目标作为参数?让我们实现一个具有日志功能的经典示例。我们想要实现一个方法装饰器@log。方法装饰器接受三个参数:定义方法的类的实例,属性的键和属性描述符(developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)。

如果方法装饰器返回一个值,它将被用作此方法的新属性描述符:

const log = (target: Object, key: string | symbol, descriptor: PropertyDescriptor) => {
  // save a reference to the original method
  var originalMethod = descriptor.value;
  // replace the original function
 descriptor.value = function(...args: any[]) {
    console.log("Arguments: ", args.join(", "));
    const result = originalMethod.apply(target, args);
    console.log("Result: ", result);
    return result;
  }
 return descriptor;
}

class Rectangle {
  @log
  area(height: number, width: number) {
    return height * width;
  }
}

let rect = new Rectangle();
let area = rect.area(2, 3);

这个装饰器记录接收到的参数和返回值。装饰器也可以组合和定制参数。例如,您可以编写以下内容:

class Rectangle {
  @log("debug")
  @persist("localStorage")
  area(height: number, width: number) {
    return height * width;
  }
}

Angular 提供了不同类型的装饰器,用于依赖注入或在编译时添加元数据信息:

  • 类装饰器,如@NgModule@Component@Injectable

  • 属性装饰器,如@Input@Output

  • 方法装饰器,如@HostListener

  • 参数装饰器,如@Inject

TypeScript 编译器能够为装饰器发出一些设计时类型元数据。要访问这些信息,我们必须安装一个名为reflect-metadata的 Polyfill:

npm install reflect-metadata --save

现在我们可以访问,例如,在target对象上的属性(key)的类型如下:

let typeOfKey = Reflect.getMetadata("design:type", target, key);

请参阅官方 TypeScript 文档,了解有关装饰器和反射元数据 API 的更多信息(www.typescriptlang.org/docs/handbook/decorators.html)。在 TypeScript 中,Angular 应用程序,通过将编译器选项emitDecoratorMetadataexperimentalDecorators设置为true来启用装饰器(编译器选项稍后描述)。

类型定义文件

用原生 JavaScript 编写的 JavaScript 程序没有任何类型信息。如果您将 JavaScript 库(如 jQuery 或 Lodash)添加到基于 TypeScript 的应用程序中并尝试使用它,TypeScript 编译器可能找不到任何类型信息,并通过编译错误警告您。编译时安全性、类型检查和上下文感知的代码完成都会丢失。这就是类型定义文件发挥作用的地方。

类型定义文件为静态类型的 JavaScript 代码提供类型信息。类型定义文件以.d.ts结尾,只包含 TypeScript 未发出的定义。declare关键字用于向 JavaScript 代码添加类型,该代码存在于某个地方。让我们举个例子。TypeScript 附带了描述 ECMAScript API 的lib.d.ts库。这个类型定义文件会被 TypeScript 编译器自动使用。以下声明在此文件中定义,但没有实现细节:

declare function parseInt(s: string, radix?: number): number;

现在,当您在代码中使用parseInt函数时,TypeScript 编译器会确保您的代码使用正确的类型,并且在编写代码时,IDE 会显示上下文敏感的提示。类型定义文件可以通过输入以下命令作为依赖项安装在node_modules/@types目录下:

npm install @types/<library name> --save-dev

jQuery 库的一个具体例子是:

npm install @types/jquery --save-dev

在 Angular 中,所有类型定义文件都与 Angular npm 包捆绑在一起,位于node_modules/@angular目录下。无需像我们为 jQuery 那样单独安装这些文件。TypeScript 会自动找到它们。

大多数情况下,您的编译目标是 ES5(生成的 JavaScript 版本,得到广泛支持),但希望通过添加 Polyfills 来使用一些 ES6(ECMAScript 2015)功能。在这种情况下,您必须告诉编译器它应该在lib.es6.d.tslib.es2015.d.ts文件中查找扩展定义。这可以通过在编译器选项中设置以下内容来实现:

"lib": ["es2015", "dom"]

编译器选项

通常,在新的 TypeScript 项目中的第一步是添加一个 tsconfig.json 文件。该文件定义了项目和编译器的设置,例如要包含在编译中的文件和库,输出结构,模块代码生成等。tsconfig.json 中用于 Angular 2+ 项目的典型配置如下:

{
  "compilerOptions": {
    "target": "es5",
    "module": "es2015",
    "moduleResolution": "node",
    "noImplicitAny": true,
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "outDir": "dist",
    "lib": ["es2015", "dom"]
  },
  "types": ["node"],
  "exclude": ["node_modules", "dist"]
}

所列的编译器设置如下所述。所有选项的完整列表可在 TypeScript 文档页面上找到(www.typescriptlang.org/docs/handbook/compiler-options.html)。

选项 类型 默认 描述
target string ES3 这指定了 ECMAScript 的目标版本:ES3, ES5, ES2015, ES2016, 和 ES2017
module string ES6 if target is "ES6" and CommonJS otherwise 这指定了模块代码生成的格式:None, CommonJS, AMD, System, UMD, ES6, 或 ES2015
moduleResolution string Classic if module is "AMD," System, ES6, and Node otherwise 这确定了模块的解析方式。要么是 Node 用于 Node.js 风格的解析,要么是 Classic
noImplicitAny boolean false 这会在具有隐含的 any 类型的表达式和声明上引发错误。
sourceMap boolean false 这会生成相应的 .map 文件。如果你想要调试原始文件,这是很有用的。
emitDecoratorMetadata boolean false 这会为源代码中装饰的声明发出设计类型元数据。如果你想要开发带有 Angular 的 Web 应用程序,你必须将这个值设置为 true
experimentalDecorators boolean false 这启用了对 ECMAScript 装饰器的实验性支持。如果你想要开发带有 Angular 的 Web 应用程序,你必须将这个值设置为 true
outDir string - 这是编译文件的输出目录。
lib string[] 更多信息请参考文档。 这是要包含在编译中的库文件列表。更多信息请参考文档。
types string[] - 这是要包含的类型定义名称列表。
exclude string[] - 这是编译时排除的(子)目录列表。

你可以通过将 --noEmitOnError 选项设置为 true 来阻止编译器在出错时发出 JavaScript。

Angular 速查表 - 关键概念概述

Angular 2 引入了完全新的概念来构建 Web 应用程序。新的 Angular 平台是复杂的。不可能详细解释众多的 Angular 特性。相反,我们将集中讨论最重要的关键概念,如依赖注入、组件及其之间的通信、内置指令、服务、模板语法、表单和路由。

组件、服务和依赖注入

通常,您通过使用 Angular 特定的标记和组件类来组合 HTML 模板来编写 Angular 应用程序。组件只是一个使用 @Component 注释的 TypeScript 类。@Component 装饰器用于定义相关的元数据。它期望一个具有以下最常用属性的对象:

  • selector:这是表示该组件的 HTML 标签的名称

  • template:这是包含 HTML/Angular 标记的内联定义模板,用于视图

  • templateUrl:这是模板所在的外部文件的路径

  • styles:内联定义的样式,应用于该组件的视图

  • styleUrls:外部文件路径数组,其中包含要应用于该组件视图的样式

  • providers:可用于该组件及其子级的提供者数组

  • exportAs:这是组件实例在模板中导出的名称

  • changeDetection:这是该组件使用的变更检测策略

  • encapsulation:这是该组件使用的样式封装策略

组件类通过属性和方法的 API 与视图进行交互。组件类应该将复杂的任务委托给业务逻辑所在的服务。服务只是 Angular 实例化然后注入到组件中的类。如果在根组件级别注册服务,它们将作为单例并在多个组件之间共享数据。在下一节中,Angular 模块化和生命周期钩子,我们将看到如何注册服务。以下示例演示了如何使用组件和服务。我们将编写一个名为 ProductService 的服务类,然后在 ProductComponent 的构造函数中指定一个类型为 ProductService 的参数。Angular 将自动将该服务注入到组件中:

import {Injectable, Component} from '@angular/core';

@Injectable()
export class ProductService {
  products: Product[];

  getProducts(): Array<Product> {
    // retrieve products from somewhere...
    return products;
  }
}

@Component({
  selector: 'product-count',
  template: `<h2 class="count">Found {{products.length}} products</h2>`,
  styles: [`
    h2.count {
      height: 80px;
      width: 400px;
    }
  `]
})
export default class ProductComponent {
  products: Product[] = [];

  constructor(productService: ProductService) {
    this.products = productService.getProducts();
  }
}

请注意,我们将@Injectable()装饰器应用到了服务类上。这对于发出 Angular 需要将其他依赖项注入到此服务中的元数据是必要的。即使您不将其他服务注入到您的服务中,使用@Injectable也是一种良好的编程风格。

了解providers数组中的项是什么样子是很好的。一个项是一个带有provide属性(用于依赖注入的符号)和useClassuseFactoryuseValue中的一个的对象,提供实现细节:

{provide: MyService, useClass: MyMockService}
{provide: MyService, useFactory: () => {return new MyMockService()}}
{provide: MyValue, useValue: 50}

模板和绑定

模板告诉 Angular 如何渲染组件的视图。模板是具有特定 Angular 模板语法的 HTML 片段,例如插值、属性、属性和事件绑定、内置指令和管道等。我们将为您快速概述模板语法,从插值开始。插值用于在双大括号中评估表达式。然后将评估的表达式转换为字符串。表达式可以包含任何数学计算、组件的属性和方法等:

<p>Selected car is {{currentCar.model}}</p>

Angular 在每次变更检测周期之后评估模板表达式。变更检测周期由许多异步活动触发,例如 HTTP 响应、键盘和鼠标事件等。下一个基本模板语法与各种绑定相关。属性绑定将元素属性设置为组件属性值。元素属性在方括号中定义:

<img [src]="imageUrl">
<button [disabled]="formValid">Submit</button>

在这里,imageUrlformValid是组件的属性。请注意,这是单向绑定,因为数据流只在一个方向上,从组件的属性到目标元素属性。属性绑定允许我们设置属性。当没有元素属性可绑定时,使用这种绑定。属性绑定也使用方括号。属性名称本身以attr.为前缀,例如,考虑用于 Web 可访问性的 ARIA 属性:

<button [attr.aria-expanded]="expanded" [attr.aria-controls]="controls">
  Click me
</button>

用户交互导致元素到组件的数据流。在 Angular 中,我们可以通过事件绑定来监听特定的键盘、鼠标和触摸事件。事件绑定语法由左侧括号中的目标事件名称和右侧的带引号的模板语句组成。特别是,您可以调用组件的方法。在下一个代码片段中,onSave()方法在点击时被调用:

<button (click)="onSave()">Save</button>

该方法(通常是模板语句)接收一个参数--一个名为$event的事件对象。对于本机 HTML 元素和事件,$event是一个 DOM 事件对象:

<input [value]="name" (input)="name=$event.target.value">

双向绑定也是可能的。[(value)]语法将属性绑定的括号与事件绑定的括号结合在一起。Angular 的指令NgModel最适合用于本机或自定义输入元素的双向绑定。考虑以下示例:

<input [(ngModel)]="username">

等同于:

<input [value]="username" (input)="username=$event.target.value">

简而言之,双向绑定是指当用户进行更改时,属性同时显示和更新。模板引用变量是方便的模板语法的另一个例子。您可以在任何 DOM 元素上使用井号(#)声明一个变量,并在模板中的任何位置引用此变量。下一个示例显示了在input元素上声明的username变量。这个引用变量在按钮上被使用--它用于获取onclick处理程序的输入值:

<input #username>
<button (click)="submit(username.value)">Ok</button>

模板引用变量也可以设置为指令。一个典型的例子是NgForm指令,它提供了关于form元素的有用细节。例如,如果表单无效(必填字段未填写等),您可以禁用提交按钮:

<form #someForm="ngForm">
  <input name="name" required [(ngModel)]="name">
  ...
  <button type="submit" [disabled]="!someForm.form.valid">Ok</button>
</form>

最后,还有管道运算符(|)。它用于转换表达式的结果。管道运算符将左侧表达式的结果传递给右侧的管道函数。例如,管道date根据指定的格式格式化 JavaScript Date对象(angular.io/docs/ts/latest/api/common/index/DatePipe-pipe.html):

Release date: {{releaseDate | date: 'longDate'}}
// Output: "August 30, 2017"

也可以应用多个链接的管道。

内置指令

Angular 有很多内置指令:ngIfngForngSwitchngClassngStyle。前三个指令被称为结构指令,用于转换 DOM 的结构。结构指令以星号(*)开头。最后两个指令动态地操作 CSS 类和样式。让我们在示例中解释这些指令。

ngIf指令根据表达式的布尔结果在 DOM 中添加和删除元素。在下一个代码片段中,当show属性计算为false时,<h2>ngIf</h2>被移除,否则重新创建:

<div *ngIf="show">
  <h2>ngIf</h2>
</div>

Angular 4 引入了一个新的else子句,其引用名称为ng-template定义的模板。当ngIf条件求值为false时,ng-template中的内容将显示出来:

<div *ngIf="showAngular; else showWorld">
  Hello Angular
</div>
<ng-template #showWorld>
  Hello World
</ng-template>

ngFor通过对数组进行迭代来输出元素列表。在下一个代码片段中,我们对people数组进行迭代,并将每个项目存储在名为person的模板变量中。然后可以在模板中访问此变量:

<ui>
  <li *ngFor="let person of people">
    {{person.name}}
  </li>
</ui>

ngSwitch根据条件有条件地交换内容。在下一个代码片段中,ngSwitch绑定到choice属性。如果ngSwitchCase匹配此属性的值,则显示相应的 HTML 元素。如果没有匹配项,则显示与ngSwitchDefault关联的元素:

<div [ngSwitch]="choice">
  <h2 *ngSwitchCase="'one'">One</h3>
  <h2 *ngSwitchCase="'two'">Two</h3>
  <h2 *ngSwitchDefault>Many</h3>
</div>

ngClass在元素上添加和删除 CSS 类。指令应接收一个带有类名作为键和表达式作为值的对象,这些表达式求值为truefalse。如果值为true,则将关联的类添加到元素中。否则,如果为false,则从元素中删除类:

<div [ngClass]="{selected: isSelected, disabled: isDisabled}">

ngStyle在元素上添加和删除内联样式。指令应接收一个带有样式名称作为键和表达式作为值的对象,这些表达式求值为样式值。键可以有一个可选的.<unit>后缀(例如,top.px):

<div [ngStyle]="{'color': 'red', 'font-weight': 'bold', 'border-top': borderTop}">

为了能够在模板中使用内置指令,您必须从@angular/common导入CommonModule并将其添加到应用程序的根模块中。Angular 的模块将在下一章中进行解释。

组件之间的通信

组件可以以松散耦合的方式相互通信。Angular 组件可以共享数据的各种方式,包括以下方式:

  • 使用@Input()从父组件向子组件传递数据

  • 使用@Output()从子组件向父组件传递数据

  • 使用服务进行数据共享

  • 调用ViewChildViewChildrenContentChildContentChildren

  • 使用本地变量与子组件交互

我们只描述前三种方式。组件可以声明输入和输出属性。要将数据从父组件传递到子组件,父组件将值绑定到子组件的输入属性。子组件的输入属性应该用@Input()装饰。让我们创建TodoChildComponent

@Component({
  selector: 'todo-child',
  template: `<h2>{{todo.title}}</h2>`
})
export class TodoChildComponent {
  @Input() todo: Todo;
}

现在,父组件可以在其模板中使用todo-child并将父组件的todo对象绑定到子组件的todo属性。子组件的属性像往常一样用方括号暴露出来:

<todo-child [todo]="todo"></todo-child>

如果组件需要将数据传递给其父组件,它会通过输出属性发出自定义事件。父组件可以创建一个监听器来监听特定组件的事件。让我们看看它的实现。子组件ConfirmationChildComponent暴露了一个带有@Output()装饰的EventEmitter属性,以便在用户点击按钮时发出事件:

@Component({
  selector: 'confirmation-child',
  template: `
    <button (click)="accept(true)">Ok</button>
    <button (click)="accept(false)">Cancel</button>
  `
})
export class ConfirmationChildComponent {
  @Output() onAccept = new EventEmitter<boolean>();

  accept(accepted: boolean) {
    this.onAccept.emit(accepted);
  }
}

父组件订阅事件处理程序到该事件属性,并对发出的事件做出反应:

@Component({
  selector: 'confirmation-parent',
  template: `
    Accepted: {{accepted}}
    <confirmation-child (onAccept)="onAccept($event)"></confirmation-child>
  `
})
export class ConfirmationParentComponent {
  accepted: boolean = false;

  onAccept(accepted: boolean) {
    this.accepted = accepted;
  }
}

通过服务可以实现双向通信。Angular 利用 RxJS 库(github.com/Reactive-Extensions/RxJS)在应用程序的各个部分之间以及应用程序与远程后端之间进行异步和基于事件的通信。异步和基于事件的通信中的关键概念是“观察者”和“可观察对象”。它们提供了一种推送式通知的通用机制,也称为观察者设计模式。“可观察对象”表示发送通知的对象,“观察者”表示接收通知的对象。

Angular 在各处实现了这种设计模式。例如,Angular 的Http服务返回一个Observable对象:

constructor(private http: Http) {}

getCars(): Obvervable<Car[]> {
  return this.http.get("../data/cars.json")
    .map(response => response.json().data as Car[]);
}

在组件间通信的情况下,可以使用Subject类的一个实例。这个类同时继承了ObservableObserver。这意味着它充当了一个消息总线。让我们实现TodoService,它允许我们发出和接收Todo对象:

@Injectable()
export class TodoService {
  private subject = new Subject();

  toggle(todo: Todo) {
    this.subject.next(todo);
  }

  subscribe(onNext, onError, onComplete) {
    this.subject.subscribe(onNext, onError, onComplete);
  }
}

组件可以以以下方式使用此服务:

export class TodoComponent {
  constructor(private todosService: TodosService) {}

  toggle(todo: Todo) {
    this.todosService.toggle(todo);
  }
}

export class TodosComponent {
  constructor(private todosService: TodosService) {
    todosService.subscribe(
      function(todo: Todo) { // TodoComponent sent todo object },
      function(e: Error) { // error occurs },
      function() { // completed }
    );
  }
}

表格

表单是每个 Web 应用程序中的主要构建块。Angular 提供了两种构建表单的方法:模板驱动表单响应式表单。本节为您提供了模板驱动表单的简要概述。

当您需要在组件类中以编程方式创建动态表单时,响应式表单是合适的。请参考官方的 Angular 文档来学习响应式表单(angular.io/docs/ts/latest/guide/reactive-forms.html)。

我们已经提到了两个指令:NgFormNgModel。第一个指令创建一个FormGroup实例,并将其绑定到一个表单,以便跟踪聚合表单值和验证状态。第二个指令创建一个FormControl实例,并将其绑定到相应的form元素。FormControl实例跟踪form元素的值和状态。每个输入元素都应该有一个name属性,这是必需的,以便通过您分配给name属性的名称将FormControl注册到FormGroup下。如何处理这些跟踪的数据?您可以将NgFormNgModel指令导出到本地模板变量,例如#f="ngForm"#i="ngModel"。在这里,fi是本地模板变量,让您访问FormGroupFormControl的值和状态。这是可能的,因为FormGroupFormControl的属性在指令本身上被复制。有了这些知识,您现在可以检查整个表单或特定的form元素:

  • 是否有效(validinvalid属性)

  • 已被访问(toucheduntouched属性)

  • 有一些改变的值(dirtypristine属性)

下一个例子说明了基本概念:

<form #f="ngForm" (ngSubmit)="onSubmit(f)" novalidate>
  <label for="name">Name</label>
  <input type="text" id=name" name="name" required
         [(ngModel)]="name" #i="ngModel"> 
  <div [hidden]="i.valid || i.pristine">
    Name is required
  </div>
  <button>Submit</button>
</form>

// Output values and states
Input value: {{i.value}}
Is input valid? {{i.valid}}
Input visited? {{i.touched}}
Input value changed? {{i.dirty}}
Form input values: {{f.value | json}}
Is form valid? {{f.valid}}
Form visited? {{f.touched}}
Form input values changed? {{f.dirty}}

NgModel指令还会更新相应的form元素,使用特定的 CSS 类来反映元素的状态。根据当前状态,以下类将被添加/移除:

状态 如果为真的类 如果为假的类
元素已被访问 ng-touched ng-untouched
元素的值已更改 ng-dirty ng-pristine
元素的值是有效的 ng-valid ng-invalid

这对于样式很方便。例如,在验证错误的情况下,您可以在输入元素周围设置红色边框:

input.ng-dirty.ng-invalid {
  border: solid 1px red;
}

路由

Angular 的router模块允许您在单页应用程序中配置导航,而无需完整的页面重新加载。路由器可以在特殊标记<router-outlet>中显示不同的视图(已编译的组件模板)。在导航期间,一个视图将被另一个视图替换。简单的路由配置如下所示:

const router: Routes = [
  {path: '', redirectTo: 'home', pathMatch: 'full'},
  {path: 'home', component: HomeComponent},
  {path: 'books', component: BooksComponent}
];

当您导航到 Web 上下文根时,您将被重定向到/home。作为对此的反应,HomeComponent的视图将显示在<router-outlet>中。显然,直接导航到/home会显示相同的视图。导航到/books会显示BooksComponent的视图。此类路由器配置应转换为 Angular 模块,使用RouterModule.forRoot

const routes:  ModuleWithProviders  =  RouterModule.forRoot(router);

然后将其导入根模块类。除了根模块外,Angular 应用程序还可以包括许多特性或延迟加载的模块。这些单独的模块可以具有自己的路由器配置,应将其转换为使用RouterModule.forChild(router)的 Angular 模块。下一节“Angular 模块化和生命周期挂钩”将详细讨论模块。Angular 提供了两种实现客户端导航的策略:

  • HashLocationStrategy:此策略在基本 URL 后添加一个哈希标记(#)。此标记后的所有内容表示浏览器 URL 的哈希片段。哈希片段标识路由。例如,http://somehost.de:8080/#/books。更改路由不会导致服务器端请求。相反,Angular 应用程序会导航到新的路由和视图。此策略适用于所有浏览器。

  • PathLocationStrategy:此策略基于History API,仅在支持 HTML5 的浏览器中有效。这是默认的位置策略。

详细信息将在此处提及。如果要使用HashLocationStrategy,必须从'@angular/common'导入LocationStrategyHashLocationStrategy两个类,并按以下方式配置提供者:

providers: [{provide: LocationStrategy, useClass: HashLocationStrategy}]

提供者在下一节“Angular 模块化和生命周期挂钩”中进行了描述。PathLocationStrategy类需要对整个应用程序的基本 URL 进行配置。最佳做法是从'@angular/common'导入APP_BASE_HREF常量,并将其用作提供者以配置基本 URL:

providers: [{provide: APP_BASE_HREF, useValue: '/'}]

如何触发导航?有两种方法可以实现,一种是使用具有routerLink属性的链接,该属性指定由路由(路径)和可选参数组成的数组:

<a [routerLink]="['/']">Home</a>
<a [routerLink]="['/books']">Books</a>

<router-outlet></router-outlet>

或者通过在 Angular 的Router服务上调用navigate方法来以编程方式实现:

import {Router} from '@angular/router';

...

export class HomeComponent {

  constructor(private router: Router) { }

  gotoBooks() {
    this.router.navigate(['/books']);
  }
}

您还可以向路由传递参数。参数的占位符以冒号(:)开头:

const router: Routes = [
  ...
  {path: 'books/:id', component: BookComponent}
];

现在,当以真实参数导航到一本书,例如以编程方式this.router.navigate(['/books/2']),可以通过ActivatedRoute访问真实参数:

import {ActivatedRoute} from '@angular/router';

...

export class BooksComponent {
  book: string;

  constructor(private route: ActivatedRoute) {
    this.book = route.snapshot.params['id'];
  }
}

路由出口也可以被命名:

<router-outlet name="author"></router-outlet>

相关配置应包含具有路由出口名称的outlet属性:

{path: 'author', component: AuthorComponent, outlet: 'author'}

Angular 模块化和生命周期钩子

Angular 模块化与 NgModule 提供了一种很好的方式来组织 Web 应用程序中的代码。许多第三方库,如 PrimeNG、Angular Material、Ionic,都是作为 NgModule 分发的。生命周期钩子允许我们在组件级别在定义良好的时间执行自定义逻辑。本节详细介绍了这些主要概念。

模块和引导

Angular 模块使得将组件、指令、服务、管道等等整合成功能块成为可能。Angular 的代码是模块化的。每个模块都有自己的功能。有FormsModuleHttpModuleRouterModule以及许多其他模块。模块是什么样子?一个模块是一个用@NgModule装饰器注释的类(从@angular/core导入)。@NgModule接受一个配置对象,告诉 Angular 如何编译和运行模块代码。配置对象的最重要的属性是:

  • declarations:组件、指令和管道的数组,这些组件、指令和管道在该模块中实现并属于该模块。

  • imports:依赖项数组,以其他模块的形式需要在该模块中可用。

  • exports:要导出并允许被其他模块导入的组件、指令和管道的数组。其余部分是私有的。这是模块的公共 API,类似于 ECMAScript 模块中export关键字的工作原理。

  • providers:这是服务的数组(服务类、工厂或值),这些服务在该模块中可用。提供者是模块的一部分,可以被注入到组件(包括子组件)、指令和管道中。

  • bootstrap:每个 Angular 应用程序至少有一个模块--根模块。bootstrap属性仅在根模块中使用,并包含在启动应用程序时应首先实例化的组件。

  • entryComponents:这是 Angular 为其生成组件工厂的组件数组。通常,当组件打算在运行时动态创建时,您需要将组件注册为入口组件。这样的组件无法在模板编译时由 Angular 自动确定。

本书中任何单独示例的典型模块配置看起来像这样:

import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {FormsModule} from '@angular/forms';
import {APP_BASE_HREF} from '@angular/common';

// PrimeNG modules needed in this example
import {ButtonModule} from 'primeng/components/button/button';
import {InputTextModule} from 'primeng/components/inputtext/inputtext';

import {AppComponent} from './app.component';
import {SectionComponent} from './section/section.component';
import {routes} from './app-routing.module';

@NgModule({
  imports: [BrowserModule, BrowserAnimationsModule, FormsModule,
            routes, ButtonModule, InputTextModule],
  declarations: [AppComponent, SectionComponent],
  providers: [{provide: APP_BASE_HREF, useValue: '/'}],
  bootstrap: [AppComponent]
})
export class AppModule { }

需要BrowserModule才能访问特定于浏览器的渲染器和 Angular 标准指令,如ngIfngFor。除了根模块之外,不要在其他模块中导入BrowserModule。功能模块和延迟加载模块应该导入CommonModule

以下是如何在 JIT 模式(即时编译)中引导 Angular 应用程序的示例:

import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {AppModule} from './app';

platformBrowserDynamic().bootstrapModule(AppModule);

提前编译模式(AOT编译)中,您需要提供一个工厂类。要生成工厂类,您必须运行ngc编译器,而不是 TypeScript 的tsc编译器。在本章的最后两节中,您将看到如何在 Webpack 和 Angular CLI 中使用 AOT。AOT 模式中的引导代码如下:

import {platformBrowser} from '@angular/platform-browser';
import {AppModuleNgFactory} from './app.ngfactory';

platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);

使用 Angular 编写的绑定模板需要进行编译。使用 AOT,编译器在构建时只运行一次。使用 JIT,它在运行时每次都会运行。浏览器加载应用程序的预编译版本速度更快,如果应用程序已经编译,则无需下载 Angular 编译器。

模块也可以在请求时(按需)进行延迟加载。这种方法减少了初始页面显示时加载的 Web 资源的大小。页面显示更快。如果要启用延迟加载,您必须配置路由器以延迟加载模块。您只需要一个具有loadChildren属性的path对象,该属性指向延迟加载模块的路径和名称:

{path: "section", loadChildren: "app/section/section.module#SectionModule"}

请注意,loadChildren属性的值是一个字符串。此外,导入此路由器配置的模块不应在配置对象的imports属性中声明延迟加载模块作为依赖项。

生命周期钩子

Angular 组件具有生命周期钩子,在组件的生命周期中的特定时间执行。为此,Angular 提供了不同的接口。每个接口都有与接口名称相同的方法,前缀为ng。每个方法在对应的生命周期事件发生时执行。它们也被称为生命周期钩子方法。在构造函数被调用后,Angular 按以下顺序调用生命周期钩子方法:

生命周期钩子方法 目的和时机
ngOnChanges 每当一个或多个数据绑定的输入属性发生变化时都会调用此方法。此方法在初始更改(在ngOnInit之前)和任何后续更改时都会被调用。此方法有一个参数--一个具有string类型键和SimpleChange类型值的对象。键是组件的属性名称。SimpleChange对象包含当前值和先前值。下面展示了一个用法示例。
ngOnInit 在第一次ngOnChanges之后调用一次。请注意,组件的构造函数应该只用于依赖注入,因为在构造函数中尚未设置数据绑定的输入值。其他所有内容应该移动到ngOnInit钩子中。下面展示了一个用法示例。
ngDoCheck 在每次变更检测运行时调用此方法。这是一个很好的地方进行自定义逻辑,允许我们对对象的哪个属性进行细粒度的检查。
ngAfterContentInit 在 Angular 将外部内容放入组件视图之后调用一次。使用ngContent指令(ng-content标签)标记任何外部内容的占位符。之后演示了ngContent指令的用法示例。
ngAfterContentChecked 在 Angular 检查放入组件视图中的内容之后调用此方法。
ngAfterViewInit 在 Angular 初始化组件和子视图之后调用一次。
ngAfterViewChecked 在 Angular 检查组件的视图和子视图之后调用此方法。
ngOnDestroy 在 Angular 销毁组件实例之前调用此方法。当您使用内置结构指令(如ngIfngForngSwitch)删除组件或导航到另一个视图时会发生这种情况。这是一个很好的地方进行清理操作,比如取消订阅可观察对象、分离事件处理程序、取消间隔定时器等。

让我们看一个如何使用ngOnInitngOnChanges的例子:

import {Component, OnInit, OnChanges, SimpleChange} from '@angular/core';

@Component({
  selector: 'greeting-component',
  template: `<h1>Hello {{text}}</h1>`
})
export class GreetingComponent implements OnInit, OnChanges {
  @Input text: string;

  constructor() { }

  ngOnInit() {
    text = "Angular";
  }

  ngOnChanges(changes: {[propertyName: string]: SimpleChange}) {
    console.log(changes.text);
    // changes = {'text': {currentValue: 'World', previousValue: {}}}
    // changes = {'text': {currentValue: 'Angular', 
                  previousValue: 'World'}}
  }
}

在 HTML 中的使用:

<greeting-component [text]="World"></greeting-component>

现在让我们看看如何使用ngContent指令:

export @Component({
  selector: 'greeting-component',
  template: `<div><ng-content></ng-content> {{text}}</div>`
})
class GreetingComponent {
  @Input text: string;
}

在 HTML 中的使用:

<greeting-component [text]="World"><b>Hello</b></greeting-component>

在组件初始化之后,以下钩子方法始终在每次变更检测运行时执行:ngDoCheck -> ngAfterContentChecked -> ngAfterViewChecked -> ngOnChanges

使用 SystemJS 运行 PrimeNG

PrimeNG(www.primefaces.org/primeng)是一个丰富的 Angular 2+ UI 组件的开源库。PrimeNG 源自 PrimeFaces,是最受欢迎的 JavaServer Faces(JSF)组件套件。如果你了解 PrimeFaces,你会因为 API 相似而觉得 PrimeNG 很熟悉。目前,PrimeNG 拥有 80 多个外观华丽且易于使用的小部件。它们分为几个组,如输入和选择组件、按钮、数据迭代组件、面板、覆盖层、菜单、图表、消息、多媒体、拖放和其他。还有 22 个免费和高级主题。

PrimeNG 非常适合移动和桌面开发,因为它是一个响应式和触摸优化的框架。PrimeNG 展示是一个很好的地方,可以在其中使用组件,尝试它们的功能,学习文档和代码片段。无论如何,我们需要一个系统化的方法来开始使用 PrimeNG。这就是这本书试图传达的内容。在本章中,我们将使用 SystemJS(github.com/systemjs/systemjs)来设置和运行 PrimeNG--这是一个支持各种模块格式的通用模块加载器。如果你想尝试 TypeScript、Angular、PrimeNG 代码片段或在 Plunker(plnkr.co)中编写小型应用程序,SystemJS 是一个很好的选择,因为它可以动态加载你的文件,转译它们(如果需要)并解析模块依赖关系。在真实的应用程序中,你应该选择 Webpack 或基于 Angular CLI 的设置,它们具有更强大和高级的配置。它们还会打包你的应用程序,以减少 HTTP 请求的数量。这些设置将在接下来的两个部分中讨论。

Angular 的 SystemJS 配置

首先,你需要安装 Node.js 和 npm,我们已经在 你需要了解的 TypeScript 基础知识 部分提到过。为什么我们需要 npm?在 HTML 和 SystemJS 配置中,我们可以从 unpkg.com 引用所有依赖项。但是,我们更喜欢本地安装所有依赖项,这样 IDE 可以很好地支持自动完成。例如,要安装 SystemJS,你需要在你选择的控制台中运行以下命令:

npm install systemjs --save

对于读者,我们创建了一个完整的演示种子项目,其中所有依赖项都在 package.json 文件中列出。

完整的带有 PrimeNG 和 SystemJS 的种子项目可以在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter1/primeng-systemjs-setup

在种子项目中,所有依赖项都可以通过在项目根目录运行 npm install 来安装。如果你探索 index.html 文件,你会发现 SystemJS 库被包含在 <head> 标签中。之后,它作为全局 System 对象可用,它公开了两个静态方法:System.import()System.config()。第一个方法用于加载模块。它接受一个参数--模块名称,可以是文件路径,也可以是逻辑名称映射到文件路径。第二个方法用于设置配置。它接受一个配置对象作为参数。通常,配置放在 systemjs.config.js 文件中。要包含在 index.html 中的完整脚本包括 TypeScript 编译器、Polyfills 和 SystemJS 相关文件。引导过程是通过执行 System.import('app') 完成的:

<script src="../node_modules/typescript/lib/typescript.js"></script>
<script src="../node_modules/core-js/client/shim.min.js"></script>
<script src="../node_modules/zone.js/dist/zone.js"></script>
<script src="../node_modules/systemjs/dist/system.src.js"></script>
<script src="../systemjs.config.js"></script>

<script>
  System.import('app').catch(function (err) {
    console.error(err);
  });
</script>

以下是 Angular 项目的配置对象摘录:

System.config({
  transpiler: 'typescript',
  typescriptOptions: {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true
  },
  map: {
    '@angular/animations':
      'node_modules/@angular/animations/bundles/animations.umd.min.js',
    '@angular/common':
      'node_modules/@angular/common/bundles/common.umd.min.js',
    '@angular/compiler':
      'node_modules/@angular/compiler/bundles/compiler.umd.min.js',
    '@angular/core': 
      'node_modules/@angular/core/bundles/core.umd.min.js',
    '@angular/forms':
      'node_modules/@angular/forms/bundles/forms.umd.min.js',
    ...
    'rxjs': 'node_modules/rxjs',
    'app': 'src'
  },
  meta: {
    '@angular/*': {'format': 'cjs'}
  },
  packages: {
    'app': {
      main: 'main',
      defaultExtension: 'ts'
    },
    'rxjs': {main: 'Rx'}
});

简要说明提供了最重要的配置选项概述:

  • transpiler 选项指定了 TypeScript 文件的转译器。可能的值包括 typescriptbabeltraceur。转译发生在浏览器中,实时进行。

  • typescriptOptions 选项设置了 TypeScript 编译器选项。

  • map 选项为模块名称创建别名。当你导入一个模块时,根据映射,模块名称会被替换为关联的值。在配置中,所有 Angular 文件的入口点都以 UMD 格式存在。

  • packages选项为导入的模块设置了元信息。例如,您可以设置模块的主入口点。此外,您可以指定默认文件扩展名,以便在导入时能够省略它们。

添加 PrimeNG 依赖

每个使用 PrimeNG 的项目都需要本地安装库。您可以通过运行以下命令来实现这一点:

npm install primeng --save

因此,PrimeNG 被安装在项目根目录下的node_modules文件夹中,并在package.json中作为依赖项添加。在这里,如果您使用托管在 GitHub 上的种子项目,可以跳过这一步--只需运行npm install。下一步是向 SystemJS 配置文件添加两个新条目。为了更短的import语句,建议将primeng映射到node_modules/primeng。PrimeNG 组件以.js结尾的 CommonJS 模块形式分发。这意味着我们也应该设置默认扩展名:

System.config({
  ...
  map: {
    ...
    'primeng': 'node_modules/primeng'
  },
  packages: {
    'primeng': {
      defaultExtension: 'js'
    },
    ...
  }
});

现在,您可以从primeng/primeng导入 PrimeNG 模块。例如,写入以下行以导入AccordionModuleMenuItem

import  {AccordionModule, MenuItem} from 'primeng/primeng';

在生产中不推荐这种导入方式,因为所有其他可用的组件也将被加载。相反,只需使用特定的组件路径导入所需的内容:

import {AccordionModule} from 'primeng/components/accordion/accordion';
import {MenuItem} from 'primeng/components/common/api';

在演示应用程序中,我们只会使用ButtonModuleInputTextModule,因此需要按照以下方式导入它们:

import {ButtonModule} from 'primeng/components/button/button';
import {InputTextModule} from 'primeng/components/inputtext/inputtext';

我们想要创建的演示项目由应用程序代码和资产组成。对每个文件的详细描述将超出本书的范围。我们只会展示项目结构:

典型的 PrimeNG 应用程序需要一个主题。我们想要使用 Bootstrap主题。文件index.html必须在<head>标签内包含三个 CSS 依赖项--主题、PrimeNG 文件和用于 SVG 图标的 FontAwesome 文件(fontawesome.io):

<link rel="stylesheet" type="text/css"
      href="../node_modules/primeng/resources/themes/bootstrap/theme.css"/>
<link rel="stylesheet" type="text/css"
      href="../node_modules/primeng/resources/primeng.min.css"/>
<link rel="stylesheet" type="text/css"
      href="src/assets/icons/css/font-awesome.min.css"/>

所有 FontAwesome 文件都放在src/assets/icons下。大多数 PrimeNG 组件是原生的,但也有一些具有第三方依赖的组件。这些在下表中有解释:

组件 依赖
日程安排 FullCalendar 和 Moment.js
编辑器 Quill 编辑器
GMap 谷歌地图
图表 Charts.js
验证码 谷歌验证码

这些依赖的确切链接将在具体示例中显示。目前,我们已经完成了设置。让我们通过在项目根目录中运行npm start来启动我们的第一个应用程序。

应用程序在浏览器中启动,显示了两个 PrimeNG 组件,如下截图所示。正如您所看到的,浏览器中加载了许多单个网络资源(CSS 和 JS 文件):

使用 Webpack 设置 PrimeNG 项目

Webpack (webpack.js.org)是单页应用程序的事实标准捆绑器。它分析 JavaScript 模块、资产(样式、图标和图像)以及应用程序中的其他文件之间的依赖关系,并将所有内容捆绑在一起。在 Webpack 中,一切都是一个模块。例如,您可以像使用require('./myfile.css')import './myfile.css'一样导入 CSS 文件。

Webpack 可以通过文件扩展名和关联的加载程序找出导入文件的正确处理策略。构建一个大捆绑文件并不总是合理的。Webpack 有各种插件来分割您的代码并生成多个捆绑文件。它还可以在需要时异步加载应用程序的部分内容(延迟加载)。所有这些功能使它成为一个强大的工具。在本节中,我们将对 Webpack 2 的核心概念进行高级概述,并展示创建基于 Webpack 的 Angular、PrimeNG 应用程序的基本步骤。

PrimeNG 和 Webpack 的完整种子项目可在 GitHub 上找到github.com/ova2/angular-development-with-primeng/tree/master/chapter1/primeng-webpack-setup

项目结构与基于 SystemJS 的设置保持一致。

入口点和输出

JavaScript 和其他文件相互导入,紧密交织在一起。Webpack 创建了所有这些依赖关系的图形。这个图形的起点被称为入口点。入口点告诉 Webpack 从哪里开始解析所有依赖关系并创建一个捆绑包。入口点是在 Webpack 配置文件中使用entry属性创建的。在 GitHub 上的种子项目中,我们有两个配置文件,一个用于开发模式(webpack.dev.js),一个用于生产(webpack.prod.js)模式,每个都有两个入口点。

在开发模式中,我们使用 JIT 编译的主入口点。main.jit.ts文件包含相当正常的引导代码。第二个入口点组合了来自core-js(现代 ECMAScript 功能的 Polyfills)和zone.js(Angular 变更检测的基础)的文件:

entry: {
  'main': './main.jit.ts',
  'polyfill': './polyfill.ts'
}

在生产模式中,我们使用 AOT 编译的主入口点。JIT 和 AOT 在Angular 模块化和生命周期钩子部分提到过:

entry: {
  'main': './main.aot.ts',
  'polyfill': './polyfill.ts'
}

output属性告诉 Webpack 在哪里捆绑您的应用程序。您可以使用诸如[name][chunkhash]之类的占位符来定义输出文件的名称。[name]占位符将被entry属性中定义的名称替换。[chunkhash]占位符将在项目构建时被文件内容的哈希值替换。chunkFilename选项确定按需(延迟)加载的块的名称 - 由System.import()加载的文件。在开发模式中,我们不使用[chunkhash],因为哈希生成期间会出现性能问题:

output: {
  filename: '[name].js',
  chunkFilename: '[name].js'
}

[chunkhash] 占位符在生产模式中用于实现所谓的“长期缓存” - 每个文件都会在浏览器中被缓存,并在哈希值更改时自动失效和重新加载:

output: {
  filename: '[name].[chunkhash].js',
  chunkFilename: '[name].[chunkhash].js'
}

文件名中的哈希值在文件内容更改时会在每次编译时更改。这意味着,具有哈希值的文件名不能手动包含在 HTML 文件(index.html)中。HtmlWebpackPlugingithub.com/jantimon/html-webpack-plugin)帮助我们在 HTML 中包含使用<script><link>标签生成的捆绑包。种子项目利用了这个插件。

加载程序和插件

Webpack 只能将 JavaScript 文件视为模块。其他每个文件(.css.scss.json.jpg等)在导入时都可以转换为模块。加载程序转换这些文件并将它们添加到依赖图中。加载程序配置应该在module.rules下完成。加载程序配置中有两个主要选项:

  • 用于测试加载程序应用于的文件的正则表达式的test属性

  • 具体加载程序名称的loaderuse属性

module: {
  rules: [
    {test: /.json$/, loader: 'json-loader'},
    {test: /.html$/, loader: 'raw-loader'},
    ...
  ]
}

请注意,加载器应该在package.json中注册,以便它们可以在node_modules下安装。Webpack 主页有一份关于一些流行加载器的很好的概述(webpack.js.org/loaders)。对于 TypeScript 文件,在开发模式下,建议使用以下加载器顺序:

{test: /.ts$/, loaders: ['awesome-typescript-loader', 'angular2-template-loader']}

多个加载器从右到左应用。angular2-template-loader搜索templateUrlstyleUrls声明,并将 HTML 和样式内联到@Component装饰器中。awesome-typescript-loader主要用于加快编译过程。对于 AOT 编译(生产模式),需要另一种配置:

{test: /.ts$/, loader: '@ngtools/webpack'}

Webpack 不仅有加载器,还有插件,负责加载器之外的自定义任务。自定义任务可能包括压缩资产、将 CSS 提取到单独的文件中、生成源映射、在编译时定义常量等等。种子项目中使用的一个有用的插件是CommonsChunkPlugin。它生成共享模块的块,并将它们拆分成单独的包。这样可以优化页面速度,因为浏览器可以快速地从缓存中提供共享的代码。在种子项目中,我们将 Webpack 的运行时代码移动到一个单独的manifest文件中,以支持长期缓存。这样当只有应用程序文件发生变化时,就可以避免对供应商文件进行哈希重建:

plugins: [
  new CommonsChunkPlugin({
    name: 'manifest',
    minChunks: Infinity
  }),
  ...
]

如您所见,插件的配置是在plugins选项中完成的。还有两个生产配置中尚未提到的插件。AotPlugin启用 AOT 编译。它需要知道tsconfig.json的路径和用于引导的模块类的路径:

new AotPlugin({
  tsConfigPath: './tsconfig.json',
  entryModule: path.resolve(__dirname, '..') + 
               '/src/app/app.module#AppModule'
})

UglifyJsPlugin用于代码最小化:

new UglifyJsPlugin({
  compress: {
    dead_code: true,
    unused: true,
    warnings: false,
    screw_ie8: true
  },
  ...
})

添加 PrimeNG、CSS 和 SASS

是时候完成设置了。首先,确保package.json文件中有 PrimeNG 和 FontAwesome 的依赖项。例如:

"primeng": "~2.0.2",
"font-awesome": "~4.7.0"

其次,将所有 CSS 文件捆绑成一个文件。这个任务由ExtractTextPlugin完成,它需要加载器和插件配置:

{test: /.css$/, loader: ExtractTextPlugin.extract({
 fallback: "style-loader",
    use: "css-loader"
  })
},
{test: /.scss/, loader: ExtractTextPlugin.extract({
 fallback: "style-loader",
    use: ['css-loader', 'sass-loader']
  }),
  exclude: /^_.*.scss/ }
...
plugins: [
 new ExtractTextPlugin({
    filename: "[name].css"  // file name of the bundle
  }),
  ...
]

对于生产环境,应将文件名设置为"[name].[chunkhash].css"。捆绑的 CSS 文件会被HtmlWebpackPlugin自动包含到index.html中。

我们更喜欢在组件中不使用styleUrls。种子项目在一个地方导入了 CSS 和 SASS 文件——在src/assets/css目录下的main.scss文件中:

// vendor files (imported from node_modules)
@import "~primeng/resources/themes/bootstrap/theme.css";
@import "~primeng/resources/primeng.min.css";
@import "~font-awesome/css/font-awesome.min.css";

// base project stuff (common settings)
@import "global";

// specific styles for components
@import "../../app/app.component";
@import "../../app/section/section.component";

请注意,波浪号~指向node_modules。更准确地说,Sass 预处理器将其解释为node_modules文件夹。Sass 在第二章中有解释,主题概念和布局main.scss文件应该在入口点main.jit.tsmain.aot.ts中导入:

import './assets/css/main.scss';

Webpack 会处理剩下的事情。Webpack 还有更多好东西--一个带有实时重新加载的开发服务器webpack-dev-server (webpack.js.org/configuration/dev-server)。它会自动检测文件的更改并重新编译。您可以使用npm startnpm run start:prod来启动它。这些命令代表 npm 脚本:

"start": webpack-dev-server --config config/webpack.dev.js --inline --open
"start:prod": webpack-dev-server --config config/webpack.prod.js --inline --open

运行webpack-dev-server时,编译输出是从内存中提供的。这意味着提供的应用程序不位于dist文件夹中的磁盘上。

就这些。更多关于单元测试和端到端测试的配置选项将在第十章中添加,创建健壮的应用程序

使用 Angular CLI 设置 PrimeNG 项目

Angular CLI (cli.angular.io)是一个方便的工具,可以立即创建、运行和测试 Angular 应用程序。它可以在短时间内生成代码。我们将描述一些有用的命令,并向您展示如何将 PrimeNG 与 Angular CLI 集成。首先,应该全局安装该工具:

npm install -g @angular/cli

安装后,每个命令都可以在控制台中使用ng前缀执行。例如,要创建一个新项目,请运行ng new [projectname] [options]。让我们创建一个。转到一个将成为项目父目录的目录,并运行以下命令:

ng new primeng-angularcli-setup --style=scss

这个命令将在primeng-angularcli-setup文件夹中创建一个 Angular 4 项目。选项--style设置了 CSS 预处理器。在这里,我们想要使用 SASS 文件并需要一个 Sass 预处理器。预处理器在我们进行更改时编译 SASS 文件。如果只有 CSS 文件,则不需要设置预处理器。

完整的预配置种子项目与 PrimeNG 和 Angular CLI 可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter1/primeng-angularcli-setup

创建的项目具有以下顶级目录和文件:

目录/文件 简要描述
e2e 包含 e2e 测试(.e2e-spec.ts文件)和页面对象(.po.ts文件)的文件夹。
src 应该编写应用程序代码的源代码文件夹。
.angular-cli.json 设置配置文件。PrimeNG 依赖项可以在此处列出。
karma.conf.js 用于单元测试的 Karma 配置文件。
protractor.conf.js 用于端到端测试的 Protractor 配置文件。
package.json npm 项目包管理的标准文件。
tsconfig.json TypeScript 编译器的设置。
tslint.json TSLint 的设置。

现在,您可以通过输入以下内容启动应用程序:

ng serve

此命令将默认在http://localhost:4200上运行本地服务器。您将在浏览器中看到文本“app works!”。ng serve命令在内部使用webpack-dev-server。服务器以监视模式运行。当发生任何更改时,它会自动刷新页面。有很多配置选项。例如,您可以通过--port选项设置自定义端口。有关更多详细信息,请参阅官方文档github.com/angular/angular-cli/wiki。要将应用程序编译到输出目录,请运行以下命令:

ng build

构建产物将存储在dist目录中。

ng buildng serve中使用--prod选项将对文件进行缩小,并删除未使用的(死)代码。--aot选项将使用 AOT 编译,并生成更小更优化的产物。

要运行单元测试和端到端测试,请分别执行ng testng e2e命令。

生成脚手架

Angular CLI 允许我们使用ng generate生成组件、服务、指令、路由、管道等等。以下是如何生成一个组件:

ng generate component path/name

例如,如果我们运行以下命令:

ng generate component shared/message

将生成四个文件并更新一个文件。生成的输出将是:

installing component
 create src/app/shared/message/message.component.scss
 create src/app/shared/message/message.component.html
 create src/app/shared/message/message.component.spec.ts
 create src/app/shared/message/message.component.ts
 update src/app/app.module.ts 

新组件将自动注册在app.module.ts中。其他脚手架的生成方式相同。例如,要生成一个服务,请运行以下命令:

ng generate service path/name

有很多有用的选项。例如,您可以设置--spec=false以跳过测试文件生成。

添加 PrimeNG 依赖项

将 PrimeNG 与 Angular CLI 集成非常简单。首先,安装并保存依赖项:

npm install primeng --save
npm install font-awesome --save

其次,编辑.angular-cli.json文件,并将另外三个 CSS 文件添加到styles部分。这些文件与基于 SystemJS 和 Webpack 的设置相同:

"styles": [
  "styles.css",
  "../node_modules/primeng/resources/themes/bootstrap/theme.css",
  "../node_modules/primeng/resources/primeng.min.css",
  "../node_modules/font-awesome/css/font-awesome.min.css"
]

现在,您可以导入所需的 PrimeNG 模块。请参考使用 SystemJS 运行 PrimeNG部分,了解如何导入 PrimeNG 模块。在 GitHub 上的种子项目中,我们已经导入了MessagesModule并将一些演示代码放入了message.component.htmlmessage.component.ts中。

摘要

阅读完本章后,您对于即将到来的章节需要了解的 TypeScript 和 Angular 概念有了一个概述。TypeScript 引入了类型,有助于在开发时识别错误。有原始类型,从面向对象编程语言中知道的类型,自定义类型等等。默认情况下,TypeScript 编译器总是会在存在类型错误的情况下发出 JavaScript 代码。这样,您可以通过将.js文件重命名为.ts来快速将任何现有的 JavaScript 代码迁移到 TypeScript,而无需一次性修复所有编译错误。

典型的 Angular 应用程序是用 TypeScript 编写的。Angular 提供了基于组件的方法,将 UI 逻辑与应用程序(业务)逻辑解耦。它实现了一个强大的依赖注入系统,使得重用服务变得轻而易举。依赖注入还增加了代码的可测试性,因为您可以轻松地模拟您的业务逻辑。Angular 应用程序由分层组件组成,它们以各种方式进行通信,如@Input@Output属性,共享服务,本地变量等等。

Angular 是一个模块化的框架。带有@NgModule注解的模块类提供了一个很好的方式来保持代码的清晰和有组织性。Angular 是灵活的--生命周期钩子允许我们在组件的生命周期的几个阶段执行自定义逻辑。最重要的是,由于智能变更检测算法,它非常快速。Angular 并不提供任何丰富的 UI 组件。它只是一个用于开发单页面应用的平台。您需要第三方库来创建丰富的 UI 界面。

PrimeNG 是 Angular 2+的一组丰富 UI 组件。与竞争对手相比,PrimeNG 是为企业应用程序创建的,并提供了 80 多个组件。添加 PrimeNG 依赖很容易。您只需要将 PrimeNG 和 FontAwesome 依赖添加到package.json文件中,以及三个 CSS 文件:primeng.min.cssfont-awesome.min.css和您喜欢的任何主题的theme.css。下一章将详细介绍主题概念。

一个 Angular 和 PrimeNG 应用程序由 ES6(ECMAScript 2015)模块组成。模块可以被导出和导入。应用程序中的所有模块构成一个依赖图。因此,您需要一个特定的工具来解析这些模块,从某些入口点开始,并输出一个捆绑包。有一些工具可以做到这一点,还有其他任务,比如按需加载模块等。

在本章中,讨论了 SystemJS 和 Webpack 加载器。SystemJS 仅推荐用于演示应用程序以便学习目的。基于 Webpack 的构建更为复杂。Webpack 具有针对每种文件类型的加载器和插件的组合。插件将有用的行为包含到 Webpack 构建过程中,例如创建公共块、网页资源的最小化、复制文件和目录、创建 SVG 精灵等等。要快速开始使用 TypeScript 和 Angular 进行开发,请使用 Angular CLI 生成项目。这是一个脚手架工具,可以轻松创建一个开箱即用的应用程序。

第二章:主题概念和布局

本章的主要目标是介绍 PrimeNG 主题、布局和相关概念。PrimeNG 中使用的主题概念类似于 jQuery ThemeRoller CSS 框架(jqueryui.com/themeroller)。PrimeNG 组件旨在允许开发人员将它们无缝地集成到整个 Web 应用程序的外观和感觉中。在撰写本文时,有 17 个免费主题和 5 个高级主题和布局。免费主题包括 ThemeRoller 主题、Twitter Bootstrap 主题和一些由 PrimeFaces 和 PrimeNG 提供支持的自定义主题。这些主题与 PrimeNG 本身一起根据 Apache 许可证进行分发。

在第一章中,使用 Angular 和 PrimeNG 入门,我们展示了三种可能的设置和主题安装。您还可以在 PrimeNG 展示页面(www.primefaces.org/primeng)中玩转免费主题,通过在右上角切换主题--可以使用主题切换器。高级主题可以作为独立主题购买。您可以在 PrimeNG 主题库(primefaces.org/themes)中预览高级主题和布局。

精英或专业用户可以在不额外费用的情况下使用一些高级主题(目前是 Omega)。有关许可模型的更多信息,请访问许可页面(www.primefaces.org/licenses)。

在本章中,我们将涵盖以下主题:

  • 理解结构和皮肤 CSS

  • 使用 SASS 组织项目结构

  • 创建新主题的简单方法

  • PrimeNG 中的响应式网格系统

  • Bootstrap 的响应式布局符合 PrimeNG

理解结构和皮肤 CSS

每个组件都使用 CSS 进行样式设置,并包含两层样式信息:结构或组件特定样式和皮肤或组件独立样式。在本节中,您将了解这两种类型的 CSS 之间的区别,学习一些有用的选择器,并查看在生成的 HTML 中 Paginator 组件的示例样式。让我们开始吧。转到 Paginator 展示页面(www.primefaces.org/primeng/#/paginator)并探索 Paginator 组件的 HTML 代码。下一张截图显示了 Google Chrome DevTools 中的 HTML 和样式。

打开 DevTools 的快捷键:F12(Windows),command + option + I(Mac)。

在前面截图中突出显示的行代表了 Paginator 组件的容器元素,具有以下样式类:

  • ui-paginator

  • ui-unselectable-text

  • ui-widget

  • ui-widget-header

前两个样式类ui-paginatorui-unselectable-text是由 PrimeNG 生成的。这些是结构样式类。第一个为元素提供语义呈现,指示元素的角色。其他类似的样式类示例包括ui-datatable用于表格和ui-button用于按钮。

第二个样式类适用于希望避免意外复制粘贴无用内容(如图标或图像)的情况。一般来说,结构样式类定义了组件的骨架,并包括诸如边距、填充、显示类型、溢出行为、尺寸和定位等 CSS 属性。

PrimeNG 展示中几乎每个组件文档都包含一个带有组件结构样式类的样式部分。

正如已经提到的,PrimeNG 利用了 jQuery ThemeRoller CSS 框架。前面提到的ui-widgetui-widget-header类是由 ThemeRoller 定义的,影响了底层 HTML 元素和相关组件的外观和感觉。这些是皮肤样式类,定义了诸如文本颜色、边框颜色和背景图像等 CSS 属性。

选择器 应用
.ui-widget 这是应用于所有 PrimeNG 组件的类。例如,它应用了字体系列和字体大小。
.ui-widget-header 这是应用于组件的头部部分的类。
.ui-widget-content 这是应用于组件的内容部分的类。
.ui-state-default 这是应用于可点击的、类似按钮的组件或其元素的默认类。
.ui-state-hover 这是应用于可点击的、类似按钮的组件或其元素的mouseover事件的类。
.ui-state-active 这是应用于可点击的、类似按钮的组件或其元素的mousedown事件的类。
.ui-state-disabled 这是应用于组件或其元素被禁用时的类。
.ui-state-highlight 这是应用于组件或其元素被突出显示或选中时的类。
.ui-corner-all 这是将圆角半径应用于组件的四个角的类。
.ui-corner-top 这是将圆角半径应用于组件的顶部两个角的类。
.ui-corner-bottom 这是将圆角半径应用于组件的底部两个角的类。
.fa 这是应用于表示图标的元素的类。

这些样式一贯地应用于所有 PrimeNG 组件,因此可点击的按钮和手风琴标签都应用了相同的ui-state-default类来指示它们是可点击的。当用户将鼠标移动到这些元素之一上时,这个类会被更改为ui-state-hover,当这些元素被选中时,又会变成ui-state-active

这种方法可以确保所有具有类似交互状态的元素在所有组件中看起来都是相同的。所提供的 PrimeNG 选择器的主要优势是在主题设置上具有很大的灵活性,因为您不需要了解每个皮肤选择器来一致地更改 Web 应用程序中所有可用组件的样式。

在少数情况下,一些样式类并不是由 PrimeNG 明确生成的,也没有被 ThemeRoller 定义。日程安排组件(www.primefaces.org/primeng/#/schedule)就是这样的情况之一。它具有结构类fc-headfc-toolbarfc-view-container等,这些类由第三方插件FullCalendarfullcalendar.io)控制。

免费主题使用相对的em单位来定义具有.ui-widget类的小部件的字体大小。默认情况下为1em。例如,Omega 主题定义了以下内容:

.ui-widget {
  font-family: "Roboto", "Trebuchet MS", Arial, Helvetica, sans-serif;
  font-size: 1em;
}

由于em单位,字体大小很容易定制。建议在body元素上应用基本字体大小,以调整整个 Web 应用程序中组件的大小:

body {
  font-size: 0.9em;
}

使用 Sass 组织项目结构

每个大型前端应用程序都需要一个强大、可扩展的 CSS 架构。CSS 预处理器是必不可少的——它有助于编写更清晰、模块化的代码,具有可重用的部分,并维护大型和复杂的样式表。CSS 预处理器基本上是一种脚本语言,它扩展了 CSS 并将其编译成常规的 CSS。今天有三种主要的 CSS 预处理器:Sass、LESS 和 Stylus。根据 Google Trends 的数据,Sass 是今天使用最多的预处理器。Sass 模仿了 HTML 结构,并允许你嵌套 CSS 选择器,这些选择器遵循相同的视觉 HTML 层次结构。使用 CSS,你需要这样写:

.container {
  padding: 5px;
}

.container p {
  margin: 5px;
}

使用 Sass,你可以简单地写成这样:

.container {
  padding: 5px;
  p {
    margin: 5px;
  }
}

Sass 向后兼容 CSS,因此你可以通过将.css文件扩展名改为.scss来轻松转换现有的 CSS 文件。

在嵌套 CSS 选择器时,你可以使用方便的&符号。&符号连接 CSS 规则。例如,考虑以下 Sass 片段:

.some-class {
  &.another-class {
    color: red;
  }
}

这将被编译为以下内容:

.some-class.another-class {
  color: red;
}

&符号对于沙盒化的 UI 组件也很有用,当每个组件只使用以唯一命名空间为前缀的类名时。例如,以下虚构的头部模块使用了.mod-header命名空间进行沙盒化:

.mod-header {
  &-link {
    color: blue;
  }

  &-menu {
    border: 1px solid gray;
  }
}

输出结果有两个类:.mod-header-link.mod-header-menu。正如你所见,Sass 有助于避免 CSS 冲突。建议为每个 UI 组件编写单独的 Sass 文件,然后通过@import指令将它们组合在一起。使用这个指令,一个 Sass 文件可以被导入到另一个文件中。预处理器将获取你想要导入的文件,并将其与你导入的文件合并在一起。这与原生 CSS 的@import有点不同。CSS 的@import总是创建一个 HTTP 请求来获取导入的文件。Sass 的@import将文件合并在一起,以便将一个单一的 CSS 文件发送到浏览器。

另一个强大的 Sass 概念是局部文件。可以创建包含小片段的局部 Sass 文件,以便包含到其他 Sass 文件中。局部文件的两个典型例子是变量混合。变量有助于存储你想要在整个样式表中重用的信息。变量以美元符号开头。例如:

$brand-color-background: white;
$brand-color-content: black;

用法:

body {
  background-color: $brand-color-background;
  color: $brand-color-content;
}

混合器允许您创建要在样式表中重复使用的 CSS 声明组。它们的行为类似于带参数的函数。混合器以@mixin指令开头,后跟名称。让我们创建一个混合器来居中任何 HTML 内容:

@mixin center($axis: "both") {
  position: absolute;
  @if $axis == "y" {
    top: 50%;
    transform: translateY(-50%);
  }
  @if $axis == "x" {
    left: 50%;
    transform: translateX(-50%);
  }
  @if $axis == "both" {
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
  }
}

混合器名称是center,参数$axis具有默认值"both",如果您没有显式传递参数值。使用方法很简单--混合器必须使用@include指令包含:

.centered-box {
  @include center();
}

这导致以下结果:

.centered-box {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

部分文件以前导下划线命名,例如,_variables.scss_mixins.scss。下划线让 Sass 知道该文件不应编译为 CSS 文件。@import指令中的下划线和文件扩展名可以省略:

@import 'variables';
@import 'mixins';

Sass 还具有更强大的功能,如继承,运算符,内置函数和处理媒体查询。有关更多详细信息,请参阅官方 Sass 网站(sass-lang.com)。您可以在www.sassmeister.com上在线使用 Sass:

或者您可以在sass.js.org使用它:

现在是时候为组织您的 Sass 文件提供指南了。有很多 Sass 文件的良好 CSS 架构和项目结构是什么?在规划 CSS 架构时,您应该将目录和文件模块化为类别。有几个提议和建议。最后,这取决于您团队中的约定。其中一个流行的提议是 7-1 模式(sass-guidelin.es/#the-7-1-pattern)。这种架构提供了七个文件夹和一个主文件,用于导入所有文件并将它们编译成一个单一文件。它们如下:

  • base/: 此文件夹包含全局样式,如 CSS 重置,排版,颜色等。例如:

  • _reset.scss

  • _typography.scss

  • helpers/: 此文件夹包含 Sass 工具和辅助程序,如变量,混合器,函数等。该文件夹在单独编译时不应输出任何一行 CSS:

  • _variables.scss

  • _mixins.scss

  • components/: 此文件夹包含独立组件的样式。这些通常是小部件,其他组件可以由它们组合而成。例如:

  • _button.scss

  • _carousel.scss

  • layout/: 这个文件夹包含了更大组件的宏布局样式,比如 CSS 网格、页眉、页脚、侧边栏等等:

  • _header.scss

  • _footer.scss

  • pages/: 这是一个可选的文件夹,其中包含特定于页面的样式:

  • _home.scss

  • _about.scss

  • themes/: 这是一个可选的文件夹,其中包含不同主题的样式。对于具有多个主题的大型网站来说是有意义的:

  • _omega.scss

  • _ultima.scss

  • vendors/: 这个文件夹包含来自外部库和框架的文件,比如 Bootstrap、jQueryUI、Select2 等等:

  • bootstrap.scss

  • jquery-ui.scss

有些文件夹是特定于项目的,可能在许多项目中不存在。文件夹名称是任意的。例如,components/文件夹也可以根据您的喜好称为modules/。在 Angular 项目中,每个组件样式的 Sass 文件都驻留在与相应组件相同的文件夹中。没有专门的文件夹供它们使用。

对于本书来说,诞生了一个演示项目--一个想象的图形编辑器,演示了样式概念。这个 Web 应用是建立在 Angular 4 和 Bootstrap 3 之上的(getbootstrap.com)。它在左右两侧有各种面板以及一个工具栏。布局是响应式的--在小屏幕上,面板会堆叠。所有样式文件都被收集在main.scss文件中:

// 1\. Vendor files
@import "~font-awesome/css/font-awesome.min.css";
@import "vendor/bootstrap-custom";

// 2\. Helpers (variables, mixins, functions, ...)
@import "helpers/variables";
@import "helpers/mixins";

// 3\. Base stuff (common settings for all components and pages)
@import "common/viewport-workaround";
@import "common/global";
@import "common/components";

// 4\. Styles for components
@import "../../app/app.component";
@import "../../app/main/main.component";
@import "../../app/layout/layout.component";
@import "../../app/panel/panel.component";
@import "../../app/panel/toolbar/toolbar.component";

带有 Sass 文件的完整图形编辑器可以在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter2/graphic-editor-sass.

一旦main.scss文件被导入到引导 Angular 应用程序的文件中,Webpack 会自动在index.html中创建一个到main.css的链接(感谢HtmlWebpackPlugin):

// Webpack creates a link to the main.css and put it into the 
// index.html
import './assets/css/main.scss';

import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {AppModule} from './app/app.module';

platformBrowserDynamic().bootstrapModule(AppModule)
                        .catch(err => console.error(err));

Bootstrap flexbox 布局满足 PrimeNG部分将展示更灵活和现代的响应式布局。图形编辑器作为一个新的演示应用的基础。

创建新主题的简单方法

我们有时需要创建自己的主题,而不是使用预定义的主题。Web 应用程序通常应该具有公司特定的外观和感觉,这是由公司范围的样式指南固定和预设的。使用 PrimeNG 创建新主题很容易,因为它由 ThemeRoller CSS 框架(jqueryui.com/themeroller)提供支持。ThemeRoller 提供了一个功能强大且易于使用的在线可视工具。在本节中,我们将系统地展示创建新主题所需的所有步骤。有两种方法可以创建新主题,一种是通过 ThemeRoller,另一种是使用 Sass 从头开始。

主题滚动器方法

要第一手体验 ThemeRoller 在线可视工具,请转到 ThemeRoller 主页,浏览可用主题库,并调整 CSS 属性以查看页面上嵌入的小部件的变化。所有 CSS 更改都将实时应用。

我们必须选择现有主题(“画廊”选项卡)并编辑它(“自定义”选项卡)。单击“下载主题”按钮即可完成工作。

我们应该在下载生成器页面的组件选项下取消选择“全部切换”复选框,以便我们的新主题只包括皮肤样式。

接下来,我们需要将从 ThemeRoller 下载的主题文件迁移到 PrimeNG 主题基础设施。迁移步骤很简单:

  1. 我们下载的主题包将包含一个 CSS 文件jquery-ui.theme.css(以及缩小的变体)和images文件夹。解压包并将 CSS 文件重命名为theme.css

  2. 在您的 Web 应用程序中,创建一个名为新主题的文件夹,例如src/assets/themes/crazy

  3. theme.cssimages文件夹复制到src/assets/themes/crazy中。

完成这些步骤后,您可以在index.html文件中创建一个指向theme.css的链接:

<link rel="stylesheet" type="text/css"  
      href="src/assets/themes/crazy/theme.css"/>

这是创建自定义主题的最简单方法,无需了解 CSS 知识。

Sass 方法

第二种方法更灵活和准确。最好通过 Sass 手动创建新主题,因为主题更易维护。主要的 CSS 设置,如字体、颜色、边框半径等,可以通过 Sass 变量进行配置。您可以通过为这些变量设置自定义值来创建新主题。PrimeNG 正是采用了这种方法。大多数免费主题都是以这种方式创建的。

免费主题托管在 GitHub 上,网址为github.com/primefaces/primeng/tree/master/resources/themes

每个主题都有一个单独的文件夹,其中包含设置变量的 Sass 文件。这些变量本身在_theme.scss中使用--这是所有免费主题共享的文件。如果您将 PrimeNG 安装为依赖项,则可以在node_modules/primeng/resources/themes/下找到此文件。有时,您还需要为特定的 CSS 选择器设置自定义字体或特殊设置。您可以用自己的样式规则覆盖默认样式规则--只需在导入_theme.scss后编写它们。自定义主题文件的一般结构如下所示:

<predefined Sass variables>

@import "primeng/resources/themes/theme";

<your custom style rules>

让我们创建以下文件夹结构,其中包含三个用于新crazy主题的 Sass 文件:

- src
    - assets
        - themes
            - crazy
                - fonts
                    ...
                - _variables.scss
                - theme.scss

Sass 变量可以从任何其他主题(如 Omega)复制,并放置在_variables.scss中。其中一些变量会有自定义值,如下所示:

$fontFamily: "Quicksand", "Trebuchet MS", Arial, Helvetica, sans-serif;
...

// Header
$headerBorderWidth: 2px;
$headerBorderColor: #f0a9df;
...

// Content
$contentBorderWidth: 2px;
$contentBorderColor: #ffafaf;
...

// Forms
$invalidInputBorderColor: #ff0000;
...

如您所见,我们希望使用自定义字体Quicksand。您可以从这个免费资源以.otf格式(OpenType Font)下载这种字体:www.fontsquirrel.com/fonts/quicksand。为了跨浏览器支持,我们需要四种格式的字体:.ttf.eot.woff.svg。有许多转换工具,其中之一可以在www.font2web.com找到,它允许将任何.otf文件转换为上述四种格式。转换后,自定义字体应该被移动到fonts文件夹,并通过@font-face规则安装。

此外,我们希望小部件标题使用粉色渐变颜色,无效字段周围有红色边框。所有这些自定义规则都在主题文件theme.scss中完成。此文件的摘录说明了这个想法:

@import 'variables';
@import "primeng/resources/themes/theme";

@font-face {
  font-family: 'Quicksand';
  src: url('fonts/Quicksand-Regular.eot');
  url('fonts/Quicksand-Regular.woff') format('woff'),
  url('fonts/Quicksand-Regular.ttf') format('truetype'),
  url('fonts/Quicksand-Regular.svg') format('svg');
  font-weight: normal;
  font-style: normal;
}

.ui-widget-header {
  background: linear-gradient(to bottom, #fffcfc 0%, #f0a9df 100%);
}

.ui-inputtext.ng-dirty.ng-invalid,
p-dropdown.ng-dirty.ng-invalid > .ui-dropdown,
... {
  border-color: $invalidInputBorderColor;
}

crazy主题的完整项目可以在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter2/custom-theme.

建议的结构允许创建任意数量的主题。但是,如何将theme.scss编译成theme.css呢?有两种将 Sass 编译成 CSS 的方法:

  1. 从命令行安装 Sass。安装过程在 Sass 主页上有描述(sass-lang.com/install)。请注意,您需要预先安装 Ruby。一旦安装了 Sass,您就可以从终端运行 sass theme.scss theme.css

  2. 在 Node.js 下使用 node-sassgithub.com/sass/node-sass)。

在 GitHub 上的项目中,我们使用了 node-sass 以及 autoprefixergithub.com/postcss/autoprefixer)和 cssnanocssnano.co)。所有必需的依赖项都是本地安装的:

npm install node-sass autoprefixer cssnano postcss postcss-cli --save-dev

package.json 中的四个方便的 npm 脚本有助于创建主题文件:

"premakecss": "node-sass --include-path node_modules/ src/assets/themes/crazy/theme.scss -o src/assets/themes/crazy/",
"makecss": "postcss src/assets/themes/crazy/theme.css --use 
autoprefixer -d src/assets/themes/crazy/",
"prebuild:css": "npm run makecss",
"build:css": "postcss src/assets/themes/crazy/theme.css --use cssnano
             > src/assets/themes/crazy/theme.min.css"

@import "primeng/resources/themes/theme" 路径是通过 --include-path node_modules/ 选项找到的,该选项设置了查找导入文件的路径。这有助于避免所有与相对路径相关的混乱。

npm run build:css 命令将生成 theme.min.css,应该包含在页面中:

<link rel="stylesheet" type="text/css" href="src/assets/themes/crazy/theme.min.css"/>

新主题的外观和感觉令人惊叹:

PrimeNG 中的响应式网格系统

PrimeNG 有Grid CSS-- 一个针对移动设备、平板电脑和台式机进行优化的响应式和流体布局系统。PrimeNG 组件内部使用 Grid CSS,但这个轻量级实用程序也可以作为独立使用。CSS Grid 基于 12 列布局,就像许多其他网格系统一样。所有列的总宽度为 100%。在本节中,我们将详细解释 PrimeNG 网格系统的所有功能。

基本原则

布局容器应该有 ui-g 样式类。当布局容器的子元素以 ui-g-* 为前缀时,它们就变成了列,其中 * 是从 1 到 12 的任意数字。数字表示了 12 个可用单位中占据的空间。当列的数量超过 12 时,列会换行到下一行:

<div class="ui-g">
  <div class="ui-g-2">2</div>
  <div class="ui-g-4">4</div>
  <div class="ui-g-6">6</div>
  <div class="ui-g-8">8</div>
  <div class="ui-g-4">4</div>
</div>

以下布局有两行(行):

两个 ui-g 容器也可以实现相同的两行布局:

<div class="ui-g">
  <div class="ui-g-2">2</div>
  <div class="ui-g-4">4</div>
  <div class="ui-g-6">6</div>
</div>
<div class="ui-g">
  <div class="ui-g-8">8</div>
  <div class="ui-g-4">4</div>
</div>

通常,带有 ui-g 样式类的 n 个容器创建 n 行。

嵌套列

列可以嵌套在更复杂的布局中。要实现这一点,只需使用带有 ui-g-* 样式类的元素进行嵌套:

<div class="ui-g">
  <div class="ui-g-8 ui-g-nopad">
    <div class="ui-g-6">6</div>
    <div class="ui-g-6">6</div>
    <div class="ui-g-12">12</div>
  </div>
  <div class="ui-g-4">4</div>
</div>

有了这个结构,具有不同内容的列将不会具有相等的高度。有一个更健壮的解决方案可以强制使具有不同内容的列具有相等的高度。只需将内部的div元素包装在另一个具有ui-g样式类的div中,或者更简单地,将ui-g分配给具有嵌套列的列:

<div class="ui-g">
  <div class="ui-g ui-g-8 ui-g-nopad">
    <div class="ui-g-6">6<br/>6<br/>6<br/>6<br/>6<br/>6<br/></div>
    <div class="ui-g-6">6</div>
    <div class="ui-g-12">12</div>
  </div>
  <div class="ui-g-4">4</div>
</div>

结果如下所示:

列具有默认填充0.5em。要删除它,您需要应用ui-g-nopad样式类。这在之前的示例中已经演示过。

响应式和流体布局

通过向列应用额外的类,可以实现响应式布局。支持四种屏幕尺寸,具有不同的断点。

前缀 设备 尺寸
ui-sm-* 手机等小型设备 最大宽度:640px
ui-md-* 平板等中等尺寸设备 最小宽度:641px
ui-lg-* 大尺寸设备,如台式机 最小宽度:1025px
ui-xl-* 大屏幕监视器 最小宽度:1441px

当一个元素具有表中列出的多个样式类时,它们从下到上应用。让我们举个例子:

<div class="ui-g">
  <div class="ui-g-12 ui-md-6 ui-lg-2">ui-g-12 ui-md-6 ui-lg-2</div>
  <div class="ui-g-12 ui-md-6 ui-lg-2">ui-g-12 ui-md-6 ui-lg-2</div>
  <div class="ui-g-12 ui-md-4 ui-lg-8">ui-g-12 ui-md-4 ui-lg-8</div>
</div>

这里发生了什么?

  • 在大屏幕上,三列按比例显示为 2:12、2:12 和 8:12。

  • 在中等屏幕上,显示两行。第一行有相等的列,第二行有 4:12 的列。

  • 在小屏幕(移动设备)上,列会堆叠--每列显示在自己的行中。

屏幕截图显示了中等尺寸设备上列的排列方式:

PrimeNG 组件具有内置的响应模式。它们理解特殊的ui-fluid样式类。Grid CSS 和任何其他网格系统都可以与此样式类一起使用,该样式类为组件提供 100%的宽度。这种行为有助于有效利用屏幕空间。一个示例演示了流体布局中的各种组件:

<div  class="ui-fluid ui-corner-all">
 <div  class="ui-g">
    <div  class="ui-g ui-g-12 ui-md-6 ui-g-nopad">
      <div  class="ui-g-12 ui-md-3 ui-label">
        Passenger
      </div>
      <div  class="ui-g-12 ui-md-9">
        <input  pInputText type="text"/>
      </div>
    </div>
    <div class="ui-g ui-g-12 ui-md-6 ui-g-nopad">
      <div class="ui-g-12 ui-md-3 ui-label">
        Flight day
      </div>
      <div class="ui-g-12 ui-md-9">
        <p-calendar [(ngModel)]="date" [showIcon]="true">
        </p-calendar>
      </div>
    </div>
  </div>
  <div  class="ui-g">
    <div  class="ui-g ui-g-12 ui-md-6 ui-g-nopad">
      <div  class="ui-g-12 ui-md-3 ui-label">
        Notice
      </div>
      <div  class="ui-g-12 ui-md-9">
        <textarea  pInputTextarea type="text"></textarea>
      </div>
    </div>
    <div  class="ui-g ui-g-12 ui-md-6 ui-g-nopad">
      <div  class="ui-g-12 ui-md-3 ui-label">
        Destination
      </div>
      <div  class="ui-g-12 ui-md-9">
        <p-listbox  [options]="cities" [(ngModel)]="selectedCity">
        </p-listbox>
      </div>
    </div>
  </div>
</div>

从中等屏幕到大屏幕的布局如下:

小屏幕上的布局为堆叠列:

如您所见,所有右对齐的标签都变成了左对齐。您可以通过媒体查询实现此行为:

.ui-fluid .ui-g .ui-label {
  text-align: right;
  white-space: nowrap;
}

@media screen and (max-width: 640px) {
  .ui-fluid .ui-g .ui-label {
    text-align: left;
  }
}

完整的演示应用程序和说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter2/primeng-grid-css.

Bootstrap flexbox 布局符合 PrimeNG

在本节中,我们将使用 Bootstrap 4(v4-alpha.getbootstrap.com)和 PrimeNG 组件重新实现在Sass 组织项目结构部分介绍的图形编辑器。从版本 v4.0.0-alpha.6 开始,默认情况下 Bootstrap 只有基于 flexbox 的布局,没有回退。

Flexbox是一种新的布局模型,在所有现代浏览器中得到广泛支持(caniuse.com/#search=flexbox)。互联网上有许多教程。例如,您可以阅读css-tricks.com/snippets/css/a-guide-to-flexbox中关于 CSS flexbox 布局的全面指南。Flexbox 解决了许多布局问题。Flexbox 的主要优势之一是能够填充额外的空间。Flexbox 布局中的所有列都具有相同的高度,而不考虑它们的内容。让我们展示两种设备分辨率的图形编辑器的最终屏幕。

对于桌面:

对于移动设备:

除了 PrimeNG,我们还需要安装最新的 Bootstrap 4。在撰写本文时,这是 4.0.0-alpha.6:

npm install bootstrap@4.0.0-alpha.6 --save

安装完成后,您需要将带有 flexbox 布局规则的 CSS 文件导入到main.scss文件中:

@import "~bootstrap/dist/css/bootstrap-grid.css";

在以前的 Bootstrap 版本中,您必须显式启用 flexbox 布局:

$enable-flex: true;
@import "~bootstrap/scss/bootstrap-grid.scss";

如果您打算使用样式进行额外的灵活对齐选项,您必须导入bootstrap-grid.scss_flex.scss

@import "~bootstrap/scss/bootstrap-grid";
@import "~bootstrap/scss/utilities/flex";

_flex.scss是一组用于垂直和水平对齐列的实用程序,用于控制内容的视觉顺序等。该文件包含各种 CSS 规则,如justify-content-startalign-items-endalign-self-autoflex-firstflex-last等。这里解释了一些规则。请参考官方的 Bootstrap 文档以了解更多细节(v4-alpha.getbootstrap.com/layout/grid)。

整个应用程序的骨架驻留在两个文件中:app.component.htmllayout.component.html。第一个文件包含了一个 PrimeNG 的带有两个菜单项的选项卡菜单:

<div class="container-fluid">
  <div class="row">
    <div class="col">
      <p-tabMenu [model]="items"></p-tabMenu>
    </div>
  </div>
</div>

<router-outlet></router-outlet>

每个项目都定义了routerLink

items: MenuItem[];
...
this.items = [
  {label: 'SVG Graphic-Engine', icon: 'fa-paint-brush',
    routerLink: '/svg'},
  {label: 'Canvas Graphic-Engine', icon: 'fa-paint-brush',
    routerLink: '/canvas'}
];

在选项卡菜单中点击选项卡会将layout.component.html加载到router-outlet中:

<div class="container-fluid">
  <div class="row align-items-center ge-toolbar">
    <div class="col">
      <ge-toolbar></ge-toolbar>
    </div>
  </div>
  <div class="row no-gutters">
    <div class="col-md-8 flex-md-unordered col-drawing-area">
      <div class="drawing-area">
        <ng-content select=".ge-drawing-area"></ng-content>
      </div>
    </div>
    <div class="col-md-2 flex-md-last">
      <div class="flex-column no-gutters">
        <div class="col ge-palette">
          <ge-palette></ge-palette>
        </div>
        <div class="col ge-shapes">
          <ge-shapes></ge-shapes>
        </div>
      </div>
    </div>
    <div class="col-md-2 flex-md-first">
      <ge-properties></ge-properties>
    </div>
  </div>
</div>

ng-content区域被 SVG 或 Canvas 表面替换,用户可以在其中绘制形状。ge-toolbar组件包含 PrimeNG 的<p-toolbar>。其他ge-*组件包含面板,例如<p-panel header="Palette">

最有趣的部分是样式类。在前面的代码片段中使用的样式类的简要描述如下:

样式类 描述
row 这充当放置在行内的列的容器。每列可以占用 1 到 12 个空间。
align-items-* 这定义了行内的 flex 列在垂直方向上的位置。align-items-center类将列定位在中间。
no-gutters 这会从行中移除边距和从列中移除填充。
col 这设置了auto-layout模式--Bootstrap 4 的一个新功能,用于等宽列。列将自动分配行中的空间。
col-<prefix>-<number> 这表示您想在每行中使用的列数,最多为 12 列。前缀定义了断点。例如,col-md-8类表示,在中等和更大的屏幕上,该列将占 12 列中的 8 列,在小于中等大小的屏幕上将占 12 列(默认)。
flex-column 这会改变项目的flex-direction(列)。项目可以水平或垂直布局。flex-column类将方向从左到右改为从上到下。
flex-<prefix>-first 这将列重新排序为布局中的第一列。前缀定义了应该从哪个断点应用重新排序。
flex-<prefix>-last 这将列重新排序为布局中的最后一列。前缀如前所述。
flex-<prefix>-unordered 这在第一个和最后一个之间显示列。前缀如前所述。

请注意,在小型设备上,我们已经减小了字体大小。这可以通过 Bootstrap 提供的断点混合来实现:

@import "~bootstrap/scss/mixins/breakpoints";

@include media-breakpoint-down(md) {
  body {
    font-size: 0.9em;
  }
}

有各种断点混合,它们期望以下参数之一:

  • xs:小型屏幕< 576px

  • sm:中型屏幕>= 576px

  • md:中型屏幕>= 768px

  • lg:大型屏幕>= 992px

  • xl:超大屏幕>= 1200px

例如,具有ge-palette样式类的元素在超过 768px 的屏幕上得到margin-top: 0,在小于 768px 的屏幕上得到margin-top: 0.5em

.ge-palette {
  margin-top: 0.5em;
}

@include media-breakpoint-up(md) {
  .ge-palette {
    margin-top: 0;
  }
}

使用 Bootstrap 4 和 PrimeNG 的完整图形编辑器可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter2/graphic-editor-bootstrap4.

摘要

阅读完本章后,您可以区分结构和皮肤样式类。简而言之,结构样式类定义了组件的骨架,而皮肤样式类用于主题。我们已经看到如何设置任何 PrimeNG 主题并创建新主题。新主题可以通过 ThemeRoller 或通过设置现有主题的 Sass 变量和 CSS 属性的自定义值,并随后编译为 CSS 文件来创建。我们鼓励使用 CSS 预处理器进行模块化 CSS 架构。Sass 预处理器有助于编写更好的样式。还提供了组织 Sass 文件的指南。

阅读完本章后,您也可以使用响应式网格系统之一,无论是 PrimeNG 自己的还是 Bootstrap 的。PrimeNG 提供了一个轻量级的响应式和流体布局系统。此外,当在顶层容器元素上使用.ui-fluid样式类时,PrimeNG 组件具有内置的响应模式。基于 flexbox 的布局是 HTML5 Web 应用程序的新标准和优势。flexbox 的主要优势之一是能够填充额外的空间 - 所有列具有相同的高度。Bootstrap 4 增加了对 flexbox 模型的支持,并允许您开发令人惊叹的布局。

从下一章开始,我们将深入研究每个组件。我们对令人兴奋的 PrimeNG 世界的旅程始于输入和选择组件。

第三章:增强输入和选择

本章介绍了增强功能的常用输入和选择组件,适用于任何类型的应用程序或网站。这些组件是每个 Web 应用程序的主要部分。每个组件的所有功能将涵盖您在开发项目时可能遇到的许多实时用例。在创建登录表单、注册表单或任何类型的表单填写应用程序时,输入和选择组件是首要考虑的因素。由于 Web 使用的快速革命和技术改进,需要各种增强的输入和选择组件,使 Web 更加强大。PrimeNG 提供了超过 20 个用于数据输入和选择的组件,这些组件通过皮肤能力和有用功能(如用户友好界面、验证等)扩展了标准或原生 HTML 组件。

在本章中,我们将涵盖以下主题:

  • 使用 InputMask 进行格式化输入

  • 自动完成的自动建议

  • 使用芯片输入多个值

  • 发现复选框-布尔、多个和三态

  • 使用单选和多选组件选择项目

  • 基本和高级日历场景

  • 微调器和滑块-提供输入的不同方式

  • 使用丰富和强大的编辑器进行文本编辑

  • 密码和基于星级的评分输入

  • 使用输入和选择组件进行验证

使用 InputMask 进行格式化输入

InputMask 是一种特殊类型的输入组件,可以最大程度地减少用户输入不正确数据的机会。它应用了提供的掩码模板的灵活验证。这对以特定格式输入数据特别有用,例如数字、字母数字、日期、货币、电子邮件和电话。电话号码输入的 InputMask 组件的基本示例如下:

<p-inputMask id="basic" name="basic" mask="99-999999"    
  [(ngModel)]="simple" placeholder="99-999999"/>

根据前面的示例,掩码值(999) 999-9999表示只能输入数字,括号和破折号结构。由于使用了相同掩码值的占位符,它建议提供的输入格式。输入的初始显示如下:

一旦输入获得焦点,口罩格式中的数字将被空格替换,而其他字符将保持在初始阶段。口罩的默认占位符字符是下划线(_),因此它将为每个数字显示下划线字符。每次keyPress事件发生后,口罩字符(即9)将被实际字符填充。如果提供的输入不完整或模糊,则整个输入将自动清除(默认情况下,autoCleartrue)。

在组件的 DOM 树中发生事件时,有些情况需要执行某些功能。inputMask组件支持onComplete回调,在用户完成口罩模式时调用。例如,当用户完成口罩输入时,用户将收到通知,如下所示:

Growl 消息出现在页面顶部,带有关闭图标,这样我们可以随时删除粘性通知。

口罩格式选项

mask属性是使用输入口罩的必需属性。该组件不仅允许数字类型,还支持字母和字母数字字符,因此口罩格式可以是以下内置定义的组合:

  • a:字母字符(A-Z,a-z

  • 9:数字字符(0-9

  • *:字母数字字符(A-Z,a-z,0-9

让我们举个例子,我们可以根据单选按钮的选择显示具有不同口罩选项的输入口罩,如下所示:

<div>
 <div id="phoneformat" *ngIf="format == 'Option1'">
    <span>Phone:</span>
    <p-inputMask mask="(999) 999-9999" [(ngModel)]="phone" 
      placeholder="(999) 999-9999" name="phone">
    </p-inputMask>
  </div>
  <div id="dateformat" *ngIf="format == 'Option2'">
    <span>Date:</span>
    <p-inputMask mask="99/99/9999" [(ngModel)]="date" 
      placeholder="99/99/9999" name="date">
    </p-inputMask>
  </div>
  <div id="serialformat" *ngIf="format == 'Option3'">
    <span>Serial Number:</span>
    <p-inputMask mask="a*-999-a999" [(ngModel)]="serial" 
      placeholder="a*-999-a999" name="serial">
    </p-inputMask>
 </div>
</div>

根据前面的示例,只会显示一个带有定义口罩的输入元素。以下屏幕截图显示了日期口罩格式的快照结果:

unmask属性可用于控制值的掩码或未掩码输出。例如,如果ngModel将原始未掩码值或格式化的掩码值设置为组件的绑定值,则它非常有用。

使用占位符字符

如前所述,下划线(_)是口罩中默认的活动占位符。但是可以使用slotChar属性进行自定义,如下所示:

<p-inputMask mask="99/99/9999" [(ngModel)]="slot" placeholder="99/99/9999"
  slotChar="mm/dd/yyyy" name="slotchar"></p-inputMask> 

slotChar选项可以是单个字符或表达式。

将口罩的一部分设为可选项

到目前为止,所有输入掩码的示例都表明掩码中的所有字符都是必需的。也可以通过使用问号(?)字符使掩码的一部分变为可选。在掩码定义中问号后面列出的任何内容都将被视为可选输入。一个常见的用例是显示带有可选分机号码的电话号码,如下所示:

<span>Phone Ext</span>
<p-inputMask mask="(999) 999-9999? x99999" [(ngModel)]="optional"     
  name="optionalmask" placeholder="(999) 999-9999? x99999">
</p-inputMask>

一旦用户通过到达问号字符完成输入并模糊组件,其余的验证将被跳过。也就是说,直到那部分的输入不会被擦除。例如,电话号码输入,如(666) 234-5678(666) 234-5678? x1230 将是掩码的可选情况的有效输入。

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter3/inputmask.

AutoComplete 的自动建议

AutoComplete 是一个输入组件,它在用户输入到输入框时提供实时建议。这使用户能够在输入时快速查找并从查找的值列表中进行选择,从而利用了搜索和过滤功能。

AutoComplete 组件的基本用法包括suggestions属性,以提供所有结果项的列表,以及completeMethod,以根据输入的查询过滤项目。例如,以下 AutoComplete 组件根据用户查询显示国家列表:

<p-autoComplete [(ngModel)]="country" name="basic"
 [suggestions]="filteredCountries"
  (completeMethod)="filterCountries($event)"
  field="name" [size]="30"
  placeholder="Type your favourite Country" [minLength]="1">
</p-autoComplete>

在上面的示例中,minLength="1" 用作输入查询结果的最小字符数。这将呈现如下快照中所示的输出:

当用户在输入框中输入时,complete 方法将按需过滤项目。该方法必须在组件类中定义,如下所示:

filterCountries(event: any) {
 let query = event.query;
  this.countryService.getCountries().
 subscribe((countries: Country[]) => {
    this.filteredCountries = this.filterCountry(query, countries);
  });
}

上述方法允许根据用户查询对国家列表进行过滤。在这种情况下,它将过滤所有以 query 字符开头的国家。

为了改善用户体验,AutoComplete 通过dropdown属性提供了一个下拉选项。单击下拉图标,它将立即在向下弹出窗口中填充所有可能的项目。

多重选择

使用 AutoComplete,还可以通过将multiple属性设置为true来选择多个值。借助多选,可以将选定的文本作为数组(例如,countries属性)检索出来。在这种情况下,ngModel应该引用一个数组。

使用对象

到目前为止,AutoComplete 已经展示了它在原始类型上的强大功能,但它也可以处理对象类型。传递给模型的值将是一个对象实例,但field属性定义了要显示为建议的标签。也就是说,在这种情况下,field属性用于将对象的任何属性显示为标签。以下示例展示了对象使用的功能:

<p-autoComplete id="instance" [(ngModel)]="countryInstance" name="instance"
 [suggestions]="filteredCountryInstances"
 (completeMethod)="filterCountryInstances($event)" field="name">
</p-autoComplete>

在上面的例子中,Country对象被用作模型对象实例,显示的建议来自使用name字段属性的国家。

高级功能 - 定制内容显示

在许多情况下,普通字段填充是不够的;为了获得更好的体验,定制内容会更有力量。AutoComplete 使用ng-template提供了这个功能,它在建议面板内显示定制内容。传递给ng-template的本地template变量是suggestions数组中的一个对象。具有国家名称和国旗的 AutoComplete 的定制示例如下:

<p-autoComplete [(ngModel)]="customCountry" name="template"
 [suggestions]="filteredCustomCountries"
  field="name" (completeMethod)="filterCustomCountries($event)" 
  [size]="30" [minLength]="1" placeholder="Start your search">
  <ng-template let-country pTemplate="item">
    <div class="ui-helper-clearfix" class="template-border">
      <img src="/assets/data/images/country/
        {{country.code.toLowerCase()}}.png" class="country-image"/>
      <div class="country-text">{{country.name}}</div>
     </div>
 </ng-template>
</p-autoComplete>

对显示的数据类型没有限制。以下截图显示了定制国家信息的快照结果:

item模板用于定制建议面板内的内容,其中selectedItem用于定制多选中的选定项。

AutoComplete 组件支持许多事件,如下所述:

名称 参数 描述
completeMethod
  • event.originalEvent: 浏览器事件

  • event.query: 用于搜索的值

调用以搜索建议的回调函数。
onFocus
onBlur
onSelect
onUnselect
onDropdownClick
  • event.originalEvent: 浏览器事件

  • event.query: 输入字段的当前值

当下拉按钮被点击时调用的回调函数。
onClear

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter3/autocomplete.

使用芯片输入多个值

芯片组件用于在输入字段中表示多个复杂实体,如联系信息,以小块的形式。芯片可以包含实体,如照片、标题、文本、规则、图标,甚至联系人。这对以紧凑的方式表示信息很有用。芯片组件的以下基本示例表示联系人姓名的顺序。默认情况下,每个实体都可以通过叉号图标或退格键删除:

<p-chips [(ngModel)]="contactnames" name="basic"></p-chips>

以下屏幕截图显示了公司联系人姓名作为芯片示例的快照结果:

芯片组件支持两个名为onAddonRemove的事件回调。这些事件回调将在向输入框添加和移除芯片时被调用。

使用模板显示复杂信息

使用ng-template元素自定义芯片,其中值作为隐式变量传递。ng-template的内容包括普通文本、图标、图片和任何其他组件。请记住,自定义芯片组件没有叉号图标,也就是说,我们只能通过退格键删除芯片条目。带有图标的芯片组件的自定义示例如下:

<p-chips [(ngModel)]="complexcontacts" name="template">
 <ng-template let-item pTemplate="item">
    <i class="fa fa-address-card"></i>-{{item}}
  </ng-template>
</p-chips>

在上面的示例中,使用公司标志和联系人姓名显示了自定义内容。以下屏幕截图显示了自定义芯片示例的快照结果:

使用maxdisabled属性来控制芯片的用户输入操作。可以使用max属性限制最大条目数。例如,如果我们设置max="5",则不允许在输入中添加第六个条目。而disabled="true"会使输入框被禁用,从而限制芯片的输入。

PrimeNG 4.1 版本引入了用于自定义输入的inputStyleinputStyleClass属性,以及用于控制重复输入的allowDuplicate属性。

完整的演示应用程序及说明可在 GitHub 上找到

请点击以下链接查看章节 3 中的 chips 示例代码:github.com/ova2/angular-development-with-primeng/tree/master/chapter3/chips.

发现复选框 - 布尔值,多个和三态

复选框是具有皮肤功能的标准复选框元素的扩展。复选框可以作为单个复选框来提供布尔值,也可以作为具有相同组名的多个复选框的多个选择。

布尔复选框 - 单选

默认情况下,复选框启用了多选功能,我们可以通过启用binary属性来进行单选。单选复选框的基本示例如下:

<p-checkbox name="single" [(ngModel)]="checked" binary="true">
</p-checkbox>

在上面的示例中,布尔复选框用于了解对 Angular 框架的兴趣。组件将显示如下截图所示:

通过在模型中启用布尔属性,也可以实现复选框的预选。

复选框多选

如前所述,默认情况下启用了多选功能,多个复选框控件具有相同的组名。在这种情况下,model属性绑定到一个数组以保存所选值。通过将单个复选框的值分配给所选值,复选框组将显示预选项。选择不同的喜爱的 Angular 版本的多个复选框选择如下:

<div class="ui-g" class="multicheckbox-width">
 <div class="ui-g-12"><p-checkbox name="angulargroup"  
    value="AngularJS1.0" label="AngularJS V1.0" [(ngModel)]="selectedVersions"></p-checkbox>
  </div>
  <div class="ui-g-12"><p-checkbox name="angulargroup" 
    value="AngularV2.0" label="Angular V2.0"
 [(ngModel)]="selectedVersions"></p-checkbox>
  </div>
  <div class="ui-g-12"><p-checkbox name="angulargroup" 
    value="AngularV4.0" label="Angular V4.0"
 [(ngModel)]="selectedVersions"></p-checkbox>
  </div>
</div>

复选框组将显示默认选择,如下截图所示:

为了通知复选框选择,有一个名为onChange的事件回调,将在用户操作时被调用。同时,用户操作通过disabled属性被禁用。

多状态表示 - TriStateCheckbox

PrimeNG 超越了 Web 上“真/假”选择的普通复选框行为。在某些情况下,特别是表示任何实体的状态时,需要“真/假/空”组合。请记住,model属性分配给任何类型而不是boolean类型。用于输入对 Angular 4 的反馈的 TriStateCheckbox 的基本示例如下:

<p-triStateCheckbox name="tristate" [(ngModel)]="status">
</p-triStateCheckbox>

TriStateCheckbox 将显示三种不同的状态(优秀,良好和不好),如下截图所示:

此增强复选框还为任何用户交互提供了onChange事件回调。用户操作通过disabled属性禁用,就像普通的布尔复选框一样。

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter3/checkbox.

使用单选和多选组件选择项目

下拉提供了一种从可用选项集合中选择项目的方法。要列出所有可能的选项,我们应该使用定义标签值属性的SelectItem接口,并将此列表绑定到options属性。选定项目的双向绑定通过model属性进行定义。让我们为用户输入在下拉框中显示一个国家列表。下拉框的基本示例将如下所示:

<p-dropdown [options]="countries" [(ngModel)]="selectedCountry"
  [styleClass]="dropdown-width" placeholder="Select a Country">
</p-dropdown>

下拉框将显示如下所示的选项:

下拉组件提供了三个事件回调,如onChangeonFocusonBlur。当下拉值发生变化时,分别获得焦点和失去焦点。有一个属性editable(即editable="true")可以直接编辑输入,就像其他输入组件一样。

下拉视口的宽度和高度将通过autoWidthscrollHeight属性进行控制。默认情况下,下拉框的宽度是根据选项的宽度计算的。而滚动高度通过scrollHeight选项以像素为单位进行控制,如果列表的高度超过此值,则定义滚动条。

自定义下拉框

下拉组件通过自定义内容比默认标签文本更强大。filter属性用于通过覆盖中的输入筛选所有可能的选项。下拉框的自定义示例,显示了代表国家名称和国旗图像的选项列表,将如下所示:

<p-dropdown [options]="countries" [(ngModel)]="selectedCountry"  
  [styleClass]="dropdown-width" filter="filter">
 <ng-template let-country pTemplate="item">
    <div class="ui-helper-clearfix" class="template-border">
      <img src="/assets/data/images/country/
        {{country.code.toLowerCase()}}.png" class="country-image"/>
      <div class="country-text">{{country.name}}</div>
    </div>
  </ng-template>
</p-dropdown>

下拉框将显示自定义内容和过滤,如下面的屏幕截图所示:

不必向下滚动查看所有国家的列表,顶部有一个过滤输入选项,可以按其起始字母过滤国家名称。它还支持逗号分隔值的多属性过滤(例如,filterBy="label, value.name")。默认情况下,过滤是针对SelectItem API 的标签进行的。

多选下拉框

多选组件用于从集合中选择多个项目,而不是提供单个项目选择的下拉组件。具有国家列表的多选组件的基本示例如下:

<p-multiSelect [options]="countries" [(ngModel)]="selectedCountries">
</p-multiSelect>

选项列表通过SelectItem接口的集合可用,该接口采用标签值对。选项列表通过多选组件的options属性绑定。多选将显示国家列表,如下面的屏幕截图所示:

在这种情况下,用户可以使用复选框选项选择多个国家,该选项适用于每个项目,并且可以过滤输入以选择特定选项。

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter3/select.

基本和高级日历场景

日历是一种输入组件,以不同的定制方式选择日期输入,例如内联、本地化、限制特定日期和面向时间。在这种情况下,日历模型由日期类型属性支持。基本日期选择的最简单组件声明如下:

<p-calendar [(ngModel)]="basicDateInput" name="basic"></p-calendar>

这显示一个输入文本框,点击后会打开一个弹出式日期选择对话框,如下所示:

除了基本的日期选择外,还可以通过顶部的左右箭头控件在每年的每个月之间进行导航。这将在高级功能部分进行解释。

日期选择很简单,可以通过点击弹出对话框中的特定日期来完成。默认情况下,日历显示为弹出式,但可以通过inline属性更改此行为。日历显示的内联版本如下:

为了更好的用户体验,组件还提供了通过showIcon属性显示日历弹出窗口的另一个选项。使用图标按钮的日历输入示例如下:

日历组件的可视显示,带有icon属性的将改变输入框旁边显示的默认图标。

本地化

不同语言和格式的本地化是通过将本地设置对象绑定到locale属性来定义的。默认的本地值是英语。要表示不同的区域设置,我们应该提供相应的语言文本标签。例如,德语区域应该为德语日历提供以下标签:

this.de = {
 firstDayOfWeek: 1,
  dayNames: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag',  
 'Freitag', 'Samstag'],
  dayNamesShort: ['Son', 'Mon', 'Die', 'Mit', 'Don', 'Fre', 'Sam'],
  dayNamesMin: ['S', 'M', 'D', 'M ', 'D', 'F ', 'S'],
  monthNames: [
    'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli',
    'August', 'September', 'Oktober', 'November', 'Dezember'
  ],
  monthNamesShort: ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul',
                    'Aug', 'Sep', 'Okt', 'Nov', 'Dez']
};

带有德语区域标签的日历将显示如下:

如前所示,区域特定的标签需要在后台组件中格式化为 JSON 以显示区域特定的日历。

时间选择器选项

除了标准的日历日期选择,我们还可以使用showTimehourFormat来显示时间。这可以进一步限制为仅使用timeOnly属性来显示时间,这只是一个时间选择器。例如,timeOnly选项将显示时间选择器如下:

两种时间格式(12 小时制和 24 小时制)将使用分割按钮分别显示。请注意,此时启用了showTime属性。

高级功能

日历组件的高级功能,如日期格式(使用dateFormat属性)、受限日期(使用minmax日期)、月份和年份导航器以便轻松访问(使用monthNavigatoryearNavigatoryearRange属性)、只读输入(使用readOnlyInput属性)以及有用的事件,如onSelectonFocusonClearonBlur

上述快照描述了可以与其特性的任何可能组合一起使用的日历。

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter3/calendar.

旋转器和滑块-提供输入的不同方式

输入组件 Spinner 通过控件或按钮提供数字输入的增量和减量。但仍然有选项可以将其用作普通的InputText。Spinner 的基本示例如下:

<p-spinner  name="basic" size="30" [(ngModel)]="basicinput"></p-spinner>

如下截图所示,Spinner 将显示带有按钮控件:

如快照所示,可以使用 Spinner 控件连续修改值。与任何其他输入组件一样,Spinner 支持onChange事件回调,该回调将在值更改时被调用。可以通过maxlength属性控制允许的最大字符数。用户交互将通过readonlydisabled属性受限。

高级功能-超越基本用法

Spinner 组件提供的功能不仅仅是具有增量和减量控件。它还可以提供诸如使用minmax属性的值边界,使用step属性自定义步进因子(默认步进因子为1)以及数字分隔符,例如decimalSeparatorthousandSeparator。Spinner 的自定义示例如下:

<p-spinner name="minmax" size="40" [(ngModel)]="customizedinput" [min]="0" [max]="100" [step]="0.50"
  placeholder="Enter your input or use spinner controls"></p-spinner>

如下截图所示,Spinner 将显示带有按钮控件:

一旦用户输入达到minmax限制,值将无法通过控件或输入更改。

可以使用formatInput属性自定义输入的格式。

滑块

滑块组件提供了使用滑块条或拖动手柄输入值的能力。model属性绑定到一个数字类型,它保存输入值。可以通过为两者提供相同的模型值将输入附加到滑块。滑块的基本示例如下:

<p-slider [(ngModel)]="basicinput" name="basicinput"  
  styleClass="slider-width">
</p-slider>

如下截图所示,滑块将显示带有拖动手柄:

每次拖动手柄穿过条时,输出值将更新。

高级功能-超越基本用法

滑块组件可以通过类似于具有输入边界的微调器的方式进行进一步定制,使用minmax属性或range属性同时提及两个边界,使用step属性定制步进因子(默认步进因子为1),以及使用animate属性在单击滑块时提供动画效果。

滑块输入的默认方向是水平的。可以使用orientation属性将滑块的方向或方向更改为垂直。

有时,除了滑块手柄之外,还可以使用常规输入,因为这样可以直接输入并且还可以通过拖动滑块手柄来显示输出。滑块的定制示例如下:

<input type="text" pInputText name="customizedinput"   
  [(ngModel)]="customizedinput"
 styleClass="input-width"/>
<p-slider [(ngModel)]="customizedinput" name="customizedinput"   
  styleClass="slider-width" [step]="20"
 [animate]="true" (onChange)="onChange()" (onSlideEnd)="onSlideEnd()">
</p-slider>

滑块将显示为以下截图所示的定制特性:

滑块输入和滑块手柄值是相互依赖的。例如,更改一个值将反映另一个值。

完整的演示应用程序及说明可在 GitHub 上找到:

使用富文本编辑器进行文本编辑

编辑器是基于 Quill 编辑器的富文本编辑器(所见即所得)。它包含一个带有常见选项的默认工具栏,其控件可以使用标题元素进行定制。此处使用的是 Quill 1.0 的最新版本作为依赖项。具有默认工具栏的基本文本编辑器可以表示如下:

<p-editor name="basic" [(ngModel)]="basictext" 
  styleClass="editor-dimensions">
</p-editor>

具有常见选项的文本编辑器将如下所示:

1. 在package.json中添加 Quill 1.0 依赖项并安装它,或者使用 CLI 工具安装它(npm install quill --save)。

2. 还要在入口页面中添加 Quill 脚本和样式 URL:

`<script src="https://cdn.quilljs.com/

1.0.0-beta.3/quill.min.js">`

`<link rel="stylesheet" type="text/css" href="https://cdn.quilljs.com/1.0.0-

beta.3/quill.snow.css">`

编辑器支持onTextChangeonSelectionChange事件,当编辑器的文本发生变化时,将调用onTextChange事件,当编辑器的选定文本发生变化时,将调用onSelectionChange事件。

自定义编辑器

如前所述,编辑器提供了一个带有常用选项的默认工具栏。可以通过在头部元素内定义元素来自定义工具栏。例如,使用文本样式控件创建的自定义工具栏如下所示:

<p-editor name="custom" [(ngModel)]="customtext" 
  styleClass="editor-dimensions">
 <p-header>
 <span class="ql-formats">
      <button class="ql-bold"></button>
      <button class="ql-italic"></button>
      <button class="ql-underline"></button>
      <button class="ql-clean"></button>
 </span>
 </p-header>
</p-editor>

带有自定义工具栏的文本编辑器将显示如下:

工具栏可以以不同的方式使用任意数量的工具栏控件进行自定义。请参考 Quill 文档以获取所有可用的控件。

完整的演示应用程序及说明可在 GitHub 上找到。

github.com/ova2/angular-development-with-primeng/tree/master/chapter3/editor.

密码和基于星级的评分输入

密码是一个增强型输入,具有字符的安全输入,就像网页上的其他密码字段一样,但它提供了强度指示器(弱、中、强),表示用户输入的安全强度。用户密码的基本示例可以写成如下形式:

<input pPassword name="basic" type="password" />

以下截图显示了基本密码示例的快照结果:

通过附加pPassword指令,密码应用于输入字段。ngModel属性用于绑定密码值。

默认情况下,密码将显示提示和强度指示标签。有一个选项可以使用诸如promptLabelweakLabelmediumLabelstrongLabel等属性来自定义所有强度指示标签。这将有助于根据需要本地化密码输入。默认情况下,feedback属性为true。一旦输入获得焦点或按键,指示标签就会出现。但是通过将反馈设置为false来改变这种行为,可以抑制输入的指示器。

评分输入

评分组件提供了基于星级的评分,具有选择和取消的功能。组件的基本声明如下:

<p-rating name="basic" [(ngModel)]="angular" ></p-rating>

在这里,评分限定值应该是一个数字类型。Angular 评分的默认视觉效果如下截图所示:

star属性帮助提供评分中的星星数量。星星的默认值为5

选择和取消评分的行为可以更加交互,您可以通过onRateonCancel回调来得到通知。在上面的快照中,评分值可以通过左侧的取消图标清除。这是因为,默认情况下cancel属性将被启用。如果该属性被禁用,则一旦选择评分就无法取消。通过禁用cancel属性,评分快照将显示为没有图标,如下所示:

由于这个特性,取消按钮不会出现来取消给定的评分。一次只能取消一个星级。

目前,评分组件不支持半个或四分之一的值。

通过在评分组件上启用readonlydisabled属性,无法选择或取消评分。这对于仅用于显示目的很有用。

完整的演示应用程序及说明可在 GitHub 上找到:

使用输入和选择组件进行验证

Angular 提供了三种不同的构建应用程序中表单的方式:

  • 基于模板的方法:这种方法允许我们构建表单,几乎不需要或根本不需要应用程序代码

  • 基于模型驱动(或响应式)的低级 API 方法:在这种方法中,我们创建的表单可以进行测试,而无需 DOM

  • 使用更高级 API 的基于模型驱动的方法:这种方法使用一个称为FormBuilder的更高级 API。

PrimeNG 创建了大多数输入和选择组件,并支持基于模型驱动的表单。因此,所有输入和选择组件都可以进行验证。

让我们以一个带有firstnamelastnamepasswordaddressphonegender字段的带有验证支持的注册表单为例。PrimeNG 组件由一个模型驱动的 API 支持,使用FormBuilder将所有表单控件分组以创建一个注册表单,如下所示:

this.registrationform = this.formBuilder.group({
    'firstname': new FormControl('', Validators.required),
    'lastname': new FormControl('', Validators.required),
    'password': new FormControl('',   
      Validators.compose([Validators.required, 
      Validators.minLength(8)])),
    'address': new FormControl(''),
    'phone': new FormControl(''),
    'gender': new FormControl('', Validators.required)
});

然而,HTML 中包含了与注册表单绑定的form元素和formGroup。表单将包含一系列控件和验证条件以显示消息:

<form [formGroup]="registrationform" (ngSubmit)="onSubmit(registrationform.value)">
  ... </form>

具有无效输入的注册表单将导致错误消息,如下快照所示:

PrimeNG 组件通过模板驱动表单和模型驱动表单提供验证。用户可以灵活选择需要提供的验证类型。

完整的演示应用程序及说明可在 GitHub 上找到。

github.com/ova2/angular-development-with-primeng/tree/master/chapter3/validation.

总结

在本章的结尾,您将能够无缝地为任何给定的用例使用所有可用的输入和选择组件。最初,我们涵盖了各种输入组件。起初,我们从使用 InputMask 进行格式化输入,使用 AutoComplete 进行自动建议,以及使用 Chips 组件输入多个值开始。

之后,我们讨论了各种复选框组件,如布尔复选框、多选框和三态复选框变体。之后,我们讨论了常用的选择组件,如单选和多选组件。我们解释了特定用例的输入组件,如日历日期输入、滑块、微调器、密码、星号和使用丰富编辑器进行文本编辑,以及所有可能的功能。最后,我们通过查看输入和选择组件的验证来结束了本章。所有这些组件和所有可能的功能都是通过逐步方法进行解释的。

在下一章中,您将看到各种按钮和面板组件将如何使您的生活更轻松。

第四章:按钮和面板组件

在本章中,我们将首先涵盖各种按钮组件,如单选按钮、分割按钮、切换按钮和选择按钮,然后转向各种面板组件,如工具栏、基本面板、字段集、手风琴和选项卡视图。用户输入将以多种方式进行,其中按钮输入是最佳选项之一;另一方面,面板组件充当容器组件,允许对其他原生 HTML 或 PrimeNG 组件进行分组。PrimeNG 的每个功能——增强按钮和面板组件都涵盖了许多实时用例需求。本章详细介绍了配置按钮和面板组件的各种设置。

在本章中,我们将涵盖以下主题:

  • 增强按钮、单选按钮和分割按钮

  • 通过切换按钮和选择按钮选择值

  • 使用工具栏对按钮进行分组

  • 使用面板和字段集排列您的视图

  • 垂直堆叠的手风琴面板

  • 在 TabView 中使用选项卡对内容进行分组

增强按钮、单选按钮和分割按钮

按钮是任何网页设计中经常使用的元素。PrimeNG 通过出色的功能扩展了普通按钮的行为。

按钮

按钮组件是用于用户与图标和主题进行交互的标准输入元素的扩展。pButton指令将普通的 HTML 按钮变成 PrimeNG 增强按钮。具有定义的标签文本的按钮组件的基本示例将如下所示:

<button name="basic" pButton type="button" label="ClickMe"></button>

按钮的类型应为button类型。以下屏幕截图显示了基本按钮示例的快照结果:

按钮组件支持一个名为click的事件回调,该事件将在单击按钮元素时被调用。请记住,按钮的点击事件基本上是来自 Angular 而不是特定于 PrimeNG 的。

图标和严重性

按钮组件在图标和严重性属性方面更有用。icon属性用于在按钮上方表示字体 awesome 图标。默认图标位置是左侧位置。可以使用iconPos属性自定义此位置,有效值为leftright。为了仅显示一个图标,将标签留空。按钮组件的示例,包括各种图标和标签的组合,将如下所示:

<button pButton type="button" icon="fa-close"></button>
<button pButton type="button" icon="fa-check" label="Yes"></button>
<button pButton type="button" icon="fa-check" iconPos="right" label="Yes"></button>

在上面的示例中,按钮被定义为没有标签,有标签,并且带有标签的右侧定位图标,依次排列。以下屏幕截图显示了带有图标的按钮的快照结果:

为了区分用户操作的不同严重级别,PrimeNG 提供了五种不同的类,即这些样式类与常规主题颜色不同:

  • ui-button-secondary

  • ui-button-success

  • ui-button-info

  • ui-button-warning

  • ui-button-danger

以下屏幕截图显示了带有所有严重情况的按钮的快照结果(与常规主题类进行比较):

用户交互使用常规的disabled属性来阻止。

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter4/button.

RadioButton

RadioButton 是标准单选按钮元素的扩展,具有选择能力以一次只选择一个值。双向绑定通过ngModel指令提供,该指令使默认值可以作为已选或未选(预选)。以下是一个具有定义标签文本的 RadioButton 组件的基本示例:

<div class="ui-g">
 <div class="ui-g-12">
    <p-radioButton name="group1" value="Angular" label="Angular"
 [(ngModel)]="basic"></p-radioButton>
  </div>
  <div class="ui-g-12">
    <p-radioButton name="group1" value="React" label="React"
 [(ngModel)]="basic"></p-radioButton>
  </div>
  <div class="ui-g-12">
    <p-radioButton name="group1" value="Polymer" label="Polymer"
 [(ngModel)]="basic"></p-radioButton>
  </div>
</div>

在上面的示例中,所有单选按钮都映射到同一组(name="group1"),以便作为互斥的单选按钮组工作。以下屏幕截图显示了单选按钮示例的快照结果:

单选按钮组件支持一个名为onClick的事件回调,该事件将在单选按钮元素被点击时被调用。label属性为单选按钮提供了标签文本。标签也是可点击的,并且选择值。与单选按钮相关的标签组件需要在点击时触发输入的焦点,这可以通过inputId属性实现。

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter4/radio-button.

SplitButton

SplitButton 将一组菜单项与默认命令按钮组合在一个覆盖中。此按钮使用常见的菜单模型 API 来定义其项目。因此,分割按钮是按钮和菜单组件的组合。使用定义的标签文本的 SplitButton 组件的基本示例将如下所示:

<p-splitButton label="Create" (onClick)="create()" [model]="items">
</p-splitButton>

标签仅适用于默认命令按钮。以下屏幕截图显示了分割按钮示例的快照结果:

分割按钮组件支持一个名为onClick的事件回调,该回调将在单击默认按钮元素时被调用。

PrimeNG 4.1 提供了appendTo选项,可以自定义覆盖的附加位置。

图标和主题

有许多选项可以自定义分割按钮的行为。图标可以分别应用于关联的默认命令按钮和菜单项,使用icon属性。默认情况下,图标对齐到左侧,但也可以使用iconPos属性应用到右侧,而组件和覆盖的皮肤行为可以通过stylestyleClassmenuStylemenuStyleClass类属性进行修改。使用定义的标签文本的 SplitButton 组件的基本示例将如下所示:

<p-splitButton label="Create" icon="fa-check" iconPos="right"  
  menuStyleClass="customized-menu" [model]="itemsIcons">
</p-splitButton>

在上面的示例中,通过menuStyleClass属性改变了覆盖菜单的默认样式。例如,在这种情况下,通过设置menuStyleClass类名来改变覆盖的默认宽度,如下所示:

.customized-menu {
  width: 140%;
}

以下屏幕截图显示了分割按钮示例的快照结果:

在上面的快照中,分割按钮定制了图标,创建命令按钮图标对齐到右侧,并且覆盖的宽度增加以容纳图标和文本。

完整的演示应用程序和说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter4/split-button

使用 ToggleButton 和 SelectButton 选择一个值

ToggleButton 提供了一种使用按钮选择布尔值的方法。ngModel指令用于将双向数据绑定到布尔属性。也就是说,通过启用布尔属性来实现切换按钮的预选。ToggleButton 使用的基本示例如下:

<p-toggleButton [(ngModel)]="basic" name="basic"></p-toggleButton>

以下屏幕截图显示了基本示例的快照结果:

ToggleButton 还提供了自定义选项,如onLabeloffLabelonIconoffIcon,用于覆盖默认标签和图标。与切换按钮相关的标签组件需要在单击标签时触发按钮的焦点,这可以通过inputId属性实现。具有标签、图标和事件的自定义切换按钮如下:

<p-toggleButton [(ngModel)]="customized" name="custom" onLabel="I 
  confirm" offLabel="I reject" onIcon="fa-check-square" 
  offIcon="fa-window-close">
</p-toggleButton>

在上面的例子中,可以为icon属性使用各种 font-awesome 图标。以下屏幕截图显示了自定义示例的快照结果:

用户使用onChange事件来通知用户操作。同时,使用disabled属性来阻止用户交互。

SelectButton

SelectButton 组件用于从按钮形式的列表中选择单个或多个项目。选项列表中的每个项目都被定义为具有标签值对属性的SelectItem接口。选项通过ngModel属性进行绑定,实现双向绑定,这将根据后端组件数据进行默认选择。选择按钮使用的基本示例如下:

<p-selectButton [options]="types" [(ngModel)]="selectedType"   
  name="basic">
</p-selectButton>  

在上面的例子中,所有 Prime 库都被收集为options属性的数组。以下屏幕截图显示了选择按钮示例的快照结果:

在上面的例子中,一次只能选择一个项目(单选),但也可以使用multiple属性选择多个项目(即multiple="true")。在这种情况下,所选的数组列表不应指向空值或未定义的值。

选择按钮组件支持一个名为onChange的事件回调,该事件将在单击默认按钮元素时被调用。

完整的演示应用程序及说明可在 GitHub 上找到:

使用 Toolbar 分组按钮

Toolbar 是按钮和其他 Web 资源的分组或容器组件。Toolbar 内容包装在两个 div 元素中,一个用于使用 .ui-toolbar-group-left 类在左侧对齐内容,另一个用于使用 .ui-toolbar-group-right 类在右侧对齐内容。Toolbar 组件的示例,包括不同的按钮、输入控件和文本内容,如下所示:

<p-toolbar name="toolbar">
 <div class="ui-toolbar-group-left">
    <button pButton type="button" name="open" label="Open" 
      icon="fa-folder-open"></button>
    <button pButton type="button" name="new" label="New folder" 
      icon="fa-plus"></button>
    <p-splitButton name="organize" label="Organize" 
      icon="fa-check" name="organize"
 [model]="items"></p-splitButton>
  </div>

  <div class="ui-toolbar-group-right">
    <input name="search" type="text" size="30" pInputText 
    [(ngModel)]="search"
 placeholder="Search files here"/>
      <i class="fa fa-bars"></i>
      <button name="refresh" pButton type="button" 
      icon="fa-refresh"></button>
      <button name="help" pButton type="button" 
      icon="fa-question-circle"></button>
  </div>
</p-toolbar>

以下屏幕截图显示了 Toolbar 的快照结果:

在上述快照中,常用的 Toolbar 按钮放在左侧,次要(或附加信息)放在右侧。通过 stylestyleClass 属性提供了皮肤特性。

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter4/toolbar.

使用 Panel 和 FieldSet 排列您的视图

大多数网站和仪表板需要分组或容器组件来突出标题和描述。PrimeNG 提供了许多容器组件的变体。

Panel

作为 Web 内容的通用分组组件,Panel 具有切换和自定义内容等功能。Panel 的基本定义如下:

<p-panel header="PrimeNG">
  PrimeNG is a collection of rich UI components for Angular.
  PrimeNG is a sibling of the popular JavaServer Faces Component Suite,  
  PrimeFaces.
  All widgets are open source and free to use under MIT License.
  PrimeNG is developed by PrimeTek Informatics, a company with years of 
  expertise in developing open source UI components.
</p-panel>

Panel 的上述定义将在容器内显示 PrimeNG 详细信息,如下屏幕截图所示:

Panel 将更加用户友好,具有可切换(toggleable="true")和自定义标题内容功能。可切换功能将内容定义为展开或折叠。面板内容的初始状态(展开或折叠)由collapsed属性定义;默认情况下,内容部分将展开,而自定义的标题和页脚通过p-headerp-footer标签定义,可以接受文本、图像、图标等。例如,以下是以下拉列表形式显示 PrimeNG 资源列表的自定义标题的示例:

我们可以使用onBeforeToggleonAfterToggle回调在切换之前和之后捕获用户操作。

FieldSet

FieldSet 是一个带有内容切换功能的分组组件。在顶部,图例定义了标题并在主体内容周围绘制了一个框。具有toggleable功能的 FieldSet 示例如下:

 <p-fieldset legend="PrimeNG" [toggleable]="true" [collapsed]="true">
   PrimeNG is a collection of rich UI components for Angular.
   PrimeNG is a sibling of the popular JavaServer Faces Component  
   Suite, PrimeFaces.
   All widgets are open source and free to use under MIT License.
   PrimeNG is developed by PrimeTek Informatics, a company with years 
   of expertise in developing open source UI components.
</p-fieldset>

如下所示,前面的 FieldSet 的定义将显示为以下截图:

与 Panel 组件类似,FieldSet 通过p-header属性(即自定义的标题内容)提供了自定义的图例。

FieldSet 的标题文本由legend属性管理,而切换功能由toggleablecollapsed属性控制。有两个名为onBeforeToggleonAfterToggle的事件回调可用于任何自定义逻辑实现。

完整的演示应用程序及说明可在 GitHub 上找到:

带有手风琴的垂直堆叠面板

手风琴是一个容器组件,可以以多个选项卡的形式对内容进行分组。内容可以是文本、图像或任何其他组件。所有选项卡内容都以垂直顺序堆叠。带有不同版本 Angular 细节的手风琴组件的基本定义如下:

<p-accordion>
 <p-accordionTab header="AngularJS">
    AngularJS (commonly referred to as "Angular.js" or "AngularJS 1.X")  
    is a JavaScript-based open-source front-end web application 
    framework mainly maintained by Google and by a community of  
    individuals and corporations to address many of the
    challenges encountered in developing single-page applications.
  </p-accordionTab>
  <p-accordionTab header="AngularV2.0">
    The successor to the older AngularJS web framework, now simply 
    known as "Angular". Angular takes a web component-based 
    approach to build powerful applications for the web. It is used  
    along with TypeScript which provides support for both older
    and new versions of JavaScript.
  </p-accordionTab>
  <p-accordionTab header="AngularV4.0">
    Angular version 4.0.0 is a major release following announced 
    adoption of Semantic Versioning, and is backwards compatible with   
    2.x.x for most applications.
  </p-accordionTab>
</p-accordion>

如下所示,前面的手风琴将显示为垂直面板:

在上面的简单示例中,Accordion 将一次显示一个选项卡内容。组件中有一个选项可以通过启用multiple属性来显示多个选项卡内容。Accordion 可以通过强大的功能进行自定义,如自定义标题、选项卡事件、默认选定的选项卡和禁用行为。

自定义的 Accordion 组件定义如下:

<p-accordion>
 <p-accordionTab>
    <p-header>
      <img src="/assets/data/images/angularjs.png" 
        alt="Smiley face" width="42" height="42">
      AngularJS
    </p-header>
    AngularJS (commonly referred to as "Angular.js" or "AngularJS 1.X") 
    is a JavaScript-based open-source front-end web application 
    framework mainly maintained by Google and by a community
    of individuals and corporations to address many of the challenges  
    encountered in developing single-page applications.
  </p-accordionTab>
  <p-accordionTab header="AngularV2.0">
    <p-header>
      <img src="/assets/data/images/angular2.svg" 
        alt="Smiley face" width="42" height="42">
      AngularV2.0
    </p-header>
    The successor to the older AngularJS web framework, 
    now simply known as "Angular". Angular takes a web 
    component-based approach to build powerful 
    applications for the web. It is used along with TypeScript 
    which provides support for both older and new versions of  
    JavaScript.
  </p-accordionTab>
  <p-accordionTab header="AngularV4.0">
    <p-header>
      <img src="/assets/data/images/angular4.png" 
        alt="Smiley face" width="42" height="42">
      AngularV4.0
    </p-header>
    Angular version 4.0.0 is a major release 
    following announced adoption of Semantic Versioning,
    and is backwards compatible with 2.x.x for most applications.
 </p-accordionTab>
</p-accordion>

在上面的示例中,使用p-header标签创建了自定义标题,其中包含 Angular 标志和文本内容。Accordion 将显示带有自定义高级功能的内容,如下图所示:

Accordion 组件支持两个名为onOpenonClose的事件回调,分别在打开和关闭选项卡时调用。

PrimeNG 4.1 版本引入了activeIndex属性,用于定义活动选项卡或要以编程方式更改的索引数组。例如,[activeIndex]="0,1"。完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter4/accordion.

在 TabView 中使用选项卡对内容进行分组

TabView 是一个选项卡面板组件,用于以垂直和水平选项卡的形式对内容进行分组。默认的 TabView 将以水平方向显示选项卡,并且一次只能选择一个选项卡来查看内容。TabView 组件的基本定义如下:

<p-tabView name="basic">
 <p-tabPanel header="AngularJS">
    AngularJS (commonly referred to as "Angular.js" or
    "AngularJS 1.X") is a JavaScript-based open-source front-end web  
    application framework mainly maintained by Google and by a  
    community of individuals and corporations to address many of 
    the challenges encountered in developing single-page applications.
  </p-tabPanel>
  <p-tabPanel header="AngularV2.0">
    The successor to the older AngularJS web framework, 
    now simply known as "Angular". Angular takes a
    web component-based approach to build powerful 
    applications for the web. It is used along with
    TypeScript which provides support for both older 
    and new versions of JavaScript.
  </p-tabPanel>
  <p-tabPanel header="AngularV4.0">
    Angular version 4.0.0 is a major release following announced  
    adoption of Semantic Versioning, and is backwards compatible 
    with 2.x.x for most applications.
  </p-tabPanel>
</p-tabView>

前面的 TabView 将显示为水平面板,如下面的截图所示:

每个选项卡都用p-tabPanel表示。可以使用orientation属性改变选项卡的方向。它支持四种不同的方向:topbottomleftrighttop是默认方向。

该组件还支持各种其他高级功能,如closable选项卡(closable="true")、事件(onChange在选项卡更改时调用,onClose在选项卡关闭时调用)、使用selection属性进行默认选择,以及使用disabled属性禁用选项卡。

onChange事件对象公开了两个在组件类中可访问的属性:

onChange
  • event.originalEvent: 原生点击事件

  • event.index: 选定选项卡的索引

|

onTabChange(event:any) {
 this.msgs = [];
 this.msgs.push({severity:'info', summary:'Tab Expanded', 
 detail: 'Target: '+ event.originalEvent.target+'Index: ' + event.index});

onClose事件对象公开了三个属性,在组件类中可以访问:

onClose
  • event.originalEvent: 原生点击事件

  • event.index: 关闭选项卡的索引

  • event.close: 回调以实际关闭选项卡,仅在启用controlClose时可用

|

onTabClose(event:any) {
 this.msgs = [];
  this.msgs.push({severity:'info', summary:'Tab closed', 
 detail: 'Target: ' + event.originalEvent.target+'Index: ' + event.index});
}

TabView 组件的自定义定义如下:

<p-tabView (onChange)="onTabChange($event)"  
  (onClose)="onTabClose($event)">
 <p-tabPanel header="AngularJS" [closable]="true" [selected]="true">
    AngularJS (commonly referred to as "Angular.js" or "AngularJS 1.X") 
    is a JavaScript-based open-source front-end web application 
    framework mainly maintained by Google and by a community of
    individuals and corporations to address many of the challenges 
    encountered in developing single-page applications.
    </p-tabPanel>
 <p-tabPanel header="AngularV2.0" [closable]="true" 
   leftIcon="fa-bell-o" rightIcon="fa-bookmark-o">
    The successor to the older AngularJS web framework, 
    now simply known as "Angular". Angular takes a
    web component-based approach to build powerful applications 
    for the web. It is used along with TypeScript which provides  
    support for both older and new versions of JavaScript.
  </p-tabPanel>
  <p-tabPanel header="AngularV4.0" [disabled]="true">
    Angular version 4.0.0 is a major release following announced  
    adoption of Semantic Versioning, and is backwards compatible 
    with 2.x.x for most applications.
  </p-tabPanel>
</p-tabView>

前面的 TabView 将显示如下截图所示:

请记住,TabView 元素只应用orientationactiveIndexstylestyleClass属性,而所有其他属性都需要为选项卡面板元素定义。

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter4/tabview.

摘要

在本章结束时,您将了解如何根据给定的用例处理各种按钮和面板组件。最初,我们涵盖了各种按钮组件。首先,我们从点击按钮变体开始,如 Button,RadioButton 和 SplitButton 组件;之后,我们转向选择值按钮变体,如 ToggleButton 和 SelectButton 组件,然后解释如何使用 Toolbar 组件将多个按钮分组。稍后,我们转向 PrimeNG 套件中提供的各种面板组件。面板组件之旅从有效地安排视图开始,使用 Panels 和 FieldSets,然后介绍如何使用垂直堆叠的 Accordion 组件,以及如何在 TabView 组件内部使用多个选项卡对内容进行分组。

下一章将详细介绍数据迭代组件,如 DataTable,导出 CSV 数据,DataList,OrderList,PickList,Schedule,以及树形分层组件,如 Tree 和 TreeTable 组件。所有这些组件都将以逐步的方式解释其所有可能的特性。

第五章:数据迭代组件

在本章中,我们将涵盖使用 PrimeNG 提供的数据迭代组件来可视化数据的基本和高级功能,其中包括 DataTable、DataList、PickList、OrderList、DataGrid、DataScroller、Tree 和 TreeTable。我们将从提供了诸多功能的 DataTable 组件开始,如过滤、排序、分页、选择、重新排序、列调整大小、切换等。然后我们将专注于其他各种组件,如 DataList,以列表格式呈现数据,并通过 PickList 和 OrderList 等列出的集合提供数据选择。

之后,我们还将看到两个更多的数据变化组件,如 DataGrid,它以网格导向布局排列大型数据集,以及 DataScroller,它根据用户滚动页面来懒加载数据。Tree 和 TreeTable 组件以树形式列出数据,并且它们大多基于相同的数据模型。在本章末尾,我们将讨论一个名为 Schedule 的复杂组件,用于可视化日历数据,并演示其懒加载功能的使用。

在本章中,我们将涵盖以下主题:

  • 多功能 DataTable

  • 在 DataTable 中选择行

  • 在 DataTable 中对数据进行排序、过滤和分页

  • 使用模板自定义单元格内容

  • 在 DataTable 中调整、重新排序和切换列

  • 使用 DataTable 进行单元格编辑

  • 使 DataTable 响应式

  • 使用列和行分组

  • 使用懒加载 DataTable 处理大量数据

  • 通过提供行模板进行行展开

  • 以 CSV 格式导出数据

  • DataTable 事件和方法

  • 使用 DataList 列出数据

  • 使用 PickList 列出数据

  • 使用 OrderList 列出数据

  • 使用 DataGrid 进行网格化数据

  • 使用 DataScroller 进行按需数据加载

  • 使用 Tree 可视化数据

  • 使用 TreeTable 可视化数据

  • 使用 Schedule 管理事件

多功能 DataTable

DataTable 以表格格式显示数据。表格是数据按行和列排列,或者可能是更复杂的结构。它需要一个作为对象数组的值,通过value属性绑定,并且使用p-column组件定义列。一个基本的组件示例,用于显示在列表格式中的浏览器详情,将被写成如下形式:

<p-dataTable [value]="browsers">
 <p-column field="engine" header="Engine"></p-column>
  <p-column field="browser" header="Browser"></p-column>
  <p-column field="platform" header="Platform"></p-column>
  <p-column field="grade" header="Grade"></p-column>
</p-dataTable>

browsers数组由具有enginebrowserplatformgrade属性的对象组成。field属性将映射模型对象属性,而header属性用于显示列的标题。在实时应用程序中,我们使用服务从远程数据源获取数据。在这种情况下,服务被创建为可注入的服务,并且它使用 HTTP 模块来获取数据。浏览器服务将被定义为可观察对象,如下所示:

@Injectable()
export class BrowserService {

constructor(private http: Http) { }

getBrowsers(): Observable<Browser[]> {
 return this.http.get('/assets/data/browsers.json')
    .map(response => response.json().data as Browser[]);
  }
}

组件类必须为value属性定义一个browser对象(或项目)的数组。项目是从远程服务调用中检索的,如下所示:

browsers: Browser[];

constructor(private browserService: BrowserService) { }

ngOnInit() {
  this.browserService.getBrowsers().subscribe((browsers: any) 
    => this.browsers =  browsers);
}

以下屏幕截图显示了以表格格式呈现的快照结果:

在前面的快照中,我们可以观察到行的替代颜色。这是一个特定于主题的行为。

PrimeNG 4.1 以更灵活的方式处理变更检测功能。

变更检测

DataTable 使用基于 setter 的检查或ngDoCheck来判断基础数据是否发生变化以更新用户界面UI)。这是使用immutable属性进行配置的。如果启用(默认),则会使用基于 setter 的检测,因此数据更改(例如添加或删除记录)应始终创建一个新的数组引用,而不是操作现有数组。这个约束是由于 Angular,并且如果引用没有改变,就不会触发 setter。在这种情况下,删除项目时使用 slice 而不是 splice,或者在添加项目时使用扩展运算符而不是push方法。

另一方面,将immutable属性设置为false会移除使用 ngDoCheck 的限制,使用 IterableDiffers 来监听变化,而无需创建数据的新引用。基于 setter 的方法更快;然而,根据您的偏好,两种方法都可以使用。

动态列

在前面的用例中,列是使用p-column标签以静态表示定义的。还有另一种方法可以通过动态列在数据表中表示列。表列需要被实例化为一个数组。该数组将使用ngFor指令进行迭代,如下所示:

<p-dataTable [value]="basicBrowsers">
 <p-header>
    <div class="algin-left">
      <p-multiSelect [options]="columnOptions" [(ngModel)]="cols">
      </p-multiSelect>
    </div>
  </p-header>
  <p-column *ngFor="let col of cols" [field]="col.field" [header]="col.header"></p-column>
</p-dataTable>

cols属性描述了组件类中给定的列选项:

this.cols = [
  {field: 'engine', header: 'Engine'},
  {field: 'browser', header: 'Browser'},
  {field: 'platform', header: 'Platform'},
  {field: 'grade', header: 'Grade'}
];

以下屏幕截图显示了动态列在表格格式中的快照结果作为示例:

在上面的快照中,列是使用多选下拉菜单动态添加或删除的。为了演示目的,我们从表格中删除了版本列字段。

在 DataTable 中选择行

为了在组件上执行 CRUD 操作,需要对表格行进行选择。PrimeNG 支持各种选择,如单选、多选、单选按钮和复选框,并带有不同的事件回调。

单选

在单选中,通过单击特定行上的单击事件来选择行。通过将selectionMode设置为single并将selection属性设置为所选行来启用此选择。默认情况下,可以通过 Meta 键(Windows 的 Ctrl 键或 macOS 的 Command 键)取消选择行。通过禁用metaKeySelection属性,可以在不按下 Meta 键的情况下取消选择行。

具有单选功能的组件,用于选择特定的浏览器记录,将如下所示编写:

<p-dataTable [value]="basicBrowsers" selectionMode="single"  
  [(selection)]="selectedBrowser">
  // Content goes here
</p-dataTable>

组件类必须定义selectedBrower对象来存储所选项目。以下屏幕截图显示了单选结果的快照:

为了通知单选是否起作用,我们在页脚部分显示了所选记录的信息。页脚数据应始终与所选记录同步。

多选

在多选中,通过单击特定行上的单击事件来选择行,并且可以使用 Meta 键或Shift键选择多行。通过将selectionMode设置为multiple并将selection属性设置为以数组形式保存所选行来启用此选择。默认情况下,可以通过 Meta 键(Windows 的 Ctrl 键或 macOS 的 Command 键)取消选择行。通过禁用metaKeySelection属性,可以在不使用 Meta 键的情况下取消选择行。

具有多选功能的组件,用于选择多个浏览器记录,将如下所示编写:

<p-dataTable [value]="basicBrowsers" selectionMode="multiple" 
  [(selection)]="selectedBrowsers">
  // Content goes here
</p-dataTable>

组件类必须定义selectedBrowers数组对象来存储所选记录。以下屏幕截图显示了多选结果的快照:

为了通知多选是否起作用,我们在页脚部分显示了选定记录的信息。页脚数据应始终与选定的记录同步。

单选和多选都支持四个事件回调,onRowClickonRowDblClickonRowSelectonRowUnselect,它们在事件对象中携带选定的数据信息。有关更多详细信息,请参阅事件部分。

单选按钮选择

单选可以通过单选按钮实现,每行都有单选按钮,而不是在特定行上使用单击事件。通过在列级别设置selectionModesingle(请记住,前面提到的普通选择是在表级别上工作的),并将selection属性设置为保存选定行的对象来启用选择。

具有单选按钮选择功能的组件,用于选择特定的浏览器记录,将如下编写:

<p-dataTable [value]="basicBrowsers" [(selection)]="selectedBrowser">
 <p-header> RadioButton selection (Single Selection)</p-header>
  <p-column [style]="{'width':'38px'}" selectionMode="single">
  </p-column>
  //Content goes here
</p-dataTable>

以下屏幕截图显示了单选按钮选择的快照结果:

截至目前,单选按钮选择没有未选择的功能(也就是说,一旦选择了另一行,该行就会被取消选择)。

复选框选择

多选可以通过复选框实现,每行都有复选框,而不是在特定行上使用单击事件。通过在列级别设置selectionModemultiple(请记住,普通选择在表级别提供此功能),并将selection属性设置为保存选定行的对象数组来启用选择。

具有复选框选择功能的组件,用于选择多个浏览器记录,将如下编写:

<p-dataTable [value]="basicBrowsers" [(selection)]="selectedBrowser">
 <p-header> Multiple Selection </p-header>
  <p-column [style]="{'width':'38px'}" selectionMode="multiple">
  </p-column>
  //Content goes here
</p-dataTable>

以下屏幕截图显示了复选框选择的快照结果:

在这种选择中,选定的记录可以通过取消复选框来取消选择。复选框选择支持onHeaderCheckboxToggle事件,用于切换标题复选框。有关更多详细信息,请参阅事件部分。

启用选择时,请使用dataKey属性避免在比较对象时进行深度检查。如果无法提供dataKey,请使用compareSelectionBy属性设置为"equals",它使用引用进行比较,而不是默认的"deepEquals"比较。深度比较不是一个好主意(特别是对于大量数据),因为它会检查所有属性。

例如,可以选择browserId属性的值作为dataKey,如下所示:

`

...

`

在 DataTable 中对数据进行排序、过滤和分页

排序、过滤和分页功能对于任何类型的数据迭代组件来说都是非常重要的功能。在处理大型数据集时,这些功能将非常有帮助。

排序

通过在每一列上启用sortable属性来提供排序功能。默认情况下,组件支持单一排序(sortMode="single")。我们可以通过设置sortMode="multiple"来实现多重排序。具有排序功能的 DataTable 组件,以按升序或降序对浏览器记录进行排序,将如下所示:

<p-dataTable [value]="browsers" (onSort)="onSort($event)">
  <p-column field="engine" header="Engine" [sortable]="true">
  </p-column>
  <p-column field="browser" header="Browser" [sortable]="true">
  </p-column>
  <p-column field="platform" header="Platform" [sortable]="true">
  </p-column>
  <p-column field="grade" header="Grade" [sortable]="true">
  </p-column>
</p-dataTable>

以下屏幕截图显示了对有限数量记录进行单一排序的快照结果:

我们需要使用 Meta 键(Windows 为 Ctrl,macOS 为 Command 键)来使用多列排序功能。还支持使用sortFunction函数进行自定义排序,而不是在field属性上进行常规排序。排序功能还提供了onSort事件回调,将在对列进行排序时调用。有关更多信息,请参阅事件详细信息部分。

过滤

通过在每一列上启用filter属性来提供过滤功能。过滤可以应用于列级别和整个表级别。表级别的过滤也称为全局过滤。要启用全局过滤,需要在globalFilter属性中引用输入的本地模板变量。全局过滤输入的keyup事件将被监听以进行过滤。

过滤功能支持可选的过滤属性,例如filterMatchMode,以提供不同类型的文本搜索。它有五种过滤匹配模式,如startsWithcontainsendsWithequalsin,默认匹配模式是startsWith,而filterPlaceholder属性用于显示辅助占位文本。具有表列过滤功能的 DataTable 组件将如下所示:

<div class="ui-widget-header align-globalfilter">
 <i class="fa fa-search search-globalfilter"></i>
  <input #gb type="text" pInputText size="50" 
  placeholder="Global Filter">
</div>
<p-dataTable [value]="browsers" [rows]="10" [paginator]="true"   
  [globalFilter]="gb" #datatable (onFilter)="onFilter($event)">
  <p-header>List of Browsers</p-header>
  <p-column field="browser" header="Browser (contains)" [filter]="true" 
    [filterMatchMode]="contains"  filterPlaceholder="Search"></p-column>
  <p-column field="platform" header="Platform (startsWith)" 
    [filter]="true"
  filterPlaceholder="Search"></p-column>
  <p-column field="rating" header="Rating ({{browserFilter||'No 
    Filter'}}" 
    [filter]="true"  filterMatchMode="equals" [style]="
    {'overflow':'visible'}">
    <ng-template pTemplate="filter" let-col>
      <i class="fa fa-close"
  (click)="ratingFilter=null; 
        datatable.filter(null,col.field,col.filterMatchMode)"></i>
      <p-slider [styleClass]="'slider-layout'"
 [(ngModel)]="ratingFilter" [min]="1" [max]="10"
  (onSlideEnd)="datatable.filter
        ($event.value,col.field,col.filterMatchMode)">
      </p-slider>
    </ng-template>
  </p-column>
  <p-column field="engine" header="Engine (Custom)" [filter]="true"
 filterMatchMode="equals" [style]="{'overflow':'visible'}">
    <ng-template pTemplate="filter" let-col>
      <p-dropdown [options]="engines" [style]="{'width':'100%'}"
  (onChange)="datatable.filter($event.value,col.field,
        col.filterMatchMode)"  styleClass="ui-column-filter">
      </p-dropdown>
    </ng-template>
  </p-column>
  <p-column field="grade" header="Grade (Custom)" [filter]="true"
 filterMatchMode="in" [style]="{'overflow':'visible'}">
    <ng-template pTemplate="filter" let-col>
      <p-multiSelect [options]="grades" defaultLabel="All grades"
  (onChange)="datatable.filter($event.value,col.field,
        col.filterMatchMode)"  styleClass="ui-column-filter">
      </p-multiSelect>
    </ng-template>
  </p-column>
</p-dataTable>

过滤功能通常应用于普通输入组件,但也可以通过在各种其他输入上提供过滤器来自定义此行为,例如 Spinner、Slider、DropDown 和 MultiSelect 组件。自定义输入过滤器调用带有三个参数的filter函数。filter函数的签名将如下所示:

datatable.filter($event.value, col.field, col.filterMatchMode)

以下屏幕截图显示了一个具有过滤功能的快照结果,作为示例,记录数量有限:

在前面的快照中,我们可以观察到数据是通过评分滑块和多选等级字段进行过滤的。过滤功能还提供了onFilter事件回调,该回调将在过滤输入时被调用。有关更多信息,请参阅事件详细信息部分。

分页

如果表格支持大型数据集,那么在单个页面上显示所有数据看起来很尴尬,当滚动数百万条记录时,对用户来说将是一场噩梦。DataTable 组件通过启用paginator属性和rows选项来支持分页功能,仅需显示页面中的记录数量。

除了上述必需的功能,它还支持各种可选功能,例如:

  • pageLinks属性显示了一次显示的页面链接数量。

  • rowsPerPageOptions属性允许更改在单个页面中显示的行数(作为数组的逗号分隔值)。

  • totalRecords属性显示了对于延迟加载功能有用的逻辑记录。

  • paginatorPosition属性显示分页器的可能值为topbottomboth。分页器的默认位置是bottom

用于显示大量浏览器信息的分页示例将如下所示:

<p-dataTable [value]="browsers" [rows]="10" [paginator]="true" 
  [pageLinks]="3" [rowsPerPageOptions]="[10,15,20]" 
  paginatorPosition="both"(onPage)="onPage($event)">
  <p-column field="engine" header="Engine"></p-column>
  <p-column field="browser" header="Browser"></p-column>
  <p-column field="platform" header="Platform"></p-column>
  <p-column field="grade" header="Grade"></p-column>
</p-dataTable>

以下屏幕截图显示了一个具有分页功能的快照结果,作为示例:

除了内置于 DataTable 中的分页器之外,我们还可以使用外部分页器来使用 Paginator 组件。分页功能还提供了onPage事件回调(而外部分页器提供了onPageChange回调),该回调将在分页时被调用。有关更多信息,请参阅事件详细信息部分。

使用模板自定义单元格内容

默认情况下,每列的field属性值用于显示表格内容。可以通过ng-template模板标签以各种可能的方式自定义内容,该模板标签可以应用于头部、主体和底部级别。传递给ng-template模板的template变量用于列定义,行数据由rowData属性使用。还可以通过rowIndex变量获得可选的行索引。

ng-template模板将具有pTemplate指令,其中包含了可能的值为headerbodyfooter的自定义类型。自定义的浏览器内容以各种文本颜色和行数据信息显示,并带有按钮选择,如下所示:

<p-dataTable [value]="basicBrowsers">
 <p-column field="engine" header="Engine"></p-column>
  <p-column field="browser" header="Browser"></p-column>
  <p-column field="platform" header="Platform"></p-column>
  <p-column field="grade" header="Grade">
    <ng-template let-col let-browser="rowData" pTemplate="body">
      <span [style.color]="'Green'" *ngIf="browser[col.field]=='A'"> 
        {{browser[col.field]}}</span>
      <span [style.color]="'Blue'" *ngIf="browser[col.field]=='B'"> 
        {{browser[col.field]}}</span>
      <span [style.color]="'Red'" *ngIf="browser[col.field]=='C'">
        {{browser[col.field]}}</span>
    </ng-template>
  </p-column>
  <p-column styleClass="col-button">
    <ng-template pTemplate="header">
      <button type="button" pButton icon="fa-refresh"></button>
    </ng-template>
    <ng-template let-browser="rowData" pTemplate="body">
      <button type="button" pButton (click)="selectBrowser(browser)" 
        icon="fa-search"></button>
    </ng-template>
  </p-column>
</p-dataTable>

在上面的例子中,我们自定义了表格内容,根据成绩显示不同的颜色,使用 body 模板每行带有按钮选择,使用 header 模板在表头处有一个按钮。以下截图显示了自定义内容显示的快照结果:

根据上面的快照,ng-template模板标签用于不同类型,以提供完全灵活的自定义。

在 DataTable 中调整大小、重新排序和切换列

默认情况下,组件的所有列都是静态表示,没有交互。该组件为列提供了调整大小、重新排序和切换功能。

调整大小

可以通过将resizableColumns属性设置为true来使用拖放行为调整列的大小。有两种调整大小模式可用。一种是fit模式,另一种是expand模式。默认模式是fit模式。在此模式下,调整列时,表格的总宽度不会改变;而在expand模式下,表格的总宽度将会改变。

使用expand模式的调整功能将被编写如下:

<p-dataTable [value]="basicBrowsers" resizableColumns="true"  
  columnResizeMode="expand">
 <p-column field="engine" header="Engine"></p-column>
  <p-column field="browser" header="Browser"></p-column>
  <p-column field="platform" header="Platform"></p-column>
  <p-column field="grade" header="Grade"></p-column>
</p-dataTable>

以下截图显示了使用expand调整大小模式的快照结果:

在前面的快照中,我们可以观察到引擎和等级列都根据其内容大小调整大小,以优化屏幕区域。由于expand模式,表的总宽度也会改变。当列调整大小时,它还可以提供onColumnResize事件回调,该事件在列调整大小时传递调整大小的列标题信息。有关更多信息,请参阅事件详细信息部分。

重新排序

通常,表列的顺序将完全按照组件中定义的顺序显示。只需将reorderableColumns属性设置为true,即可使用拖放功能重新排序列。

重新排序功能将写成如下形式:

<p-dataTable [value]="browsers" reorderableColumns="true">
 <p-column field="engine" header="Engine"></p-column>
  <p-column field="browser" header="Browser"></p-column>
  <p-column field="platform" header="Platform"></p-column>
  <p-column field="grade" header="Grade"></p-column>
</p-dataTable>

以下截图显示了重新排序功能的快照结果示例:

根据前面的快照,平台和浏览器列字段是相互重新排序的(即,初始列顺序为enginebrowserplatformgrade。重新排序后,列的顺序将变为engineplatformbrowsergrade)。每当列重新排序时,它还提供onColReorder事件回调。有关更多详细信息,请参阅事件部分。

切换

大多数情况下,屏幕空间不足以显示所有列。在这种情况下,切换表列将非常有助于节省可用的屏幕空间。由于此功能,只能显示必需或主要列。可以通过在动态列表上定义 MultiSelect 组件来实现此功能,以切换列。请参阅本章开头提到的动态列示例。

使用 DataTable 进行单元格内编辑

默认情况下,组件的内容将处于只读模式(即,我们无法编辑内容)。使用单元格编辑功能,UI 将更具交互性。只需在表和列级别上设置editable属性,即可启用单元格编辑功能。单击单元格时,将激活编辑模式。在单元格外部单击或按下“Enter”键后,将切换回查看模式并更新值。单元格编辑功能将写成如下形式:

<p-dataTable [value]="browsers" [editable]="true">
 <p-column field="browser" header="Browser" [editable]="true">
  </p-column>
  <p-column field="platfrom" header="Platform" [editable]="false">
  </p-column>
  <p-column field="engine" header="Engine" [editable]="true">
    <ng-template let-col let-browser="rowData" pTemplate="editor">
      <p-dropdown [(ngModel)]="browser[col.field]" [options]="engines"  
        [autoWidth]="false"  required="true"></p-dropdown>
    </ng-template>
  </p-column>
  <p-column field="grade" header="Grade" [editable]="true">
  </p-column>
</p-dataTable>

以下截图显示了在engine字段上使用单元格编辑功能的快照结果示例:

默认情况下,可编辑模式在单击特定单元格时启用输入组件。我们还可以使用其他输入组件,如 DropDown、MultiSelect、Calendar 等,进行自定义输入编辑。在前面的示例中,我们可以使用 Input 和 Dropdown 组件编辑单元格。

使 DataTable 响应式

响应功能对于 Web 和移动应用程序都非常有用。如果屏幕尺寸小于某个断点值,则组件列将以响应模式堆叠显示。通过将responsive属性设置为true来启用此功能。此堆叠行为也可以通过手动实现(不考虑屏幕尺寸)来实现,方法是启用stacked属性(即stacked="true")。

Table 组件的响应模式功能将被编写如下:

<button pButton type="button" (click)="toggle()" 
  [class]="responsive-toggle"
 label="Toggle" icon="fa-list">
</button>
<p-dataTable [value]="browsers" [rows]="5" [paginator]="true" 
  [pageLinks]="3" [responsive]="true" [stacked]="stacked">
  <p-header>Responsive</p-header>
  <p-column field="engine" header="Engine"></p-column>
  <p-column field="browser" header="Browser"></p-column>
  <p-column field="platform" header="Platform"></p-column>
  <p-column field="grade" header="Grade"></p-column>
</p-dataTable>

组件类定义了toggle方法,用于切换响应行为,如下所示:

toggle() {
 this.stacked = !this.stacked;
}

以下屏幕截图显示了 DataTable 组件具有堆叠列的快照结果:

在这种用例中,列通过手动切换按钮以堆叠的方式显示,该按钮放置在表格外部。响应模式或堆叠行为也可以通过减小或最小化屏幕尺寸来实现。

使用列和行分组

DataTable 组件在列级和行级都提供了分组功能。

列分组

可以使用p-headerColumnGroupp-footerColumnGroup标签在表头和表尾区域对列进行分组,这些标签使用colspanrowspan属性定义列的数组。表行使用p-row标签定义,其中包含列组件。具有列分组的组件将被编写如下:

<p-dataTable [value]="basicBrowsers">
 <p-headerColumnGroup>
    <p-row>
      <p-column header="Browser" rowspan="3"></p-column>
      <p-column header="Details" colspan="4"></p-column>
    </p-row>
    <p-row>
      <p-column header="Environment" colspan="2"></p-column>
      <p-column header="Performance" colspan="2"></p-column>
    </p-row>
    <p-row>
      <p-column header="Engine"></p-column>
      <p-column header="Platform"></p-column>
      <p-column header="Rating"></p-column>
      <p-column header="Grade"></p-column>
    </p-row>
  </p-headerColumnGroup>

  <p-column field="browser"></p-column>
  <p-column field="engine"></p-column>
  <p-column field="platform"></p-column>
  <p-column field="rating"></p-column>
  <p-column field="grade"></p-column>

  <p-footerColumnGroup>
    <p-row>
      <p-column footer="*Please note that Chrome browser 
        details not included"
 colspan="5"></p-column>
    </p-row>
  </p-footerColumnGroup>
</p-dataTable>

以下屏幕截图显示了列分组功能的快照结果:

在前面的快照中,我们可以观察到特定于浏览器的信息是通过列分组进行分类的。

行分组

默认情况下,表行是单独的,并逐个显示以表示唯一记录。在许多情况下,需要将多个行分组为一行。

可展开的行分组

行可以根据特定字段进行分组,以便使用行展开器功能展开和折叠行。通过设置rowGroupMode="subheader"expandableRowGroups="true"groupField="browser"来启用此功能。groupField设置为特定的分类列。

具有可展开行组选项的行分组功能将被编写如下:

<p-dataTable [value]="browsers" sortField="browser"  
  rowGroupMode="subheader" groupField="browser"  
  expandableRowGroups="true" [sortableRowGroup]="false">
  <p-header>Toggleable Row Groups with Footers</p-header>
  <ng-template pTemplate="rowgroupheader" let-rowData> 
    {{rowData['browser']}}
 </ng-template>
  <p-column field="engine" header="Engine"></p-column>
  <p-column field="platform" header="Platform"></p-column>
  <p-column field="rating" header="rating">
    <ng-template let-col let-browser="rowData" pTemplate="body">
      <span>{{browser[col.field]}} 'Stars'</span>
    </ng-template>
  </p-column>
  <ng-template pTemplate="rowgroupfooter" let-browser>
    <td colspan="3" style="text-align:right">Chrome browsers are not 
      included</td>
  </ng-template>
</p-dataTable>

以下截图显示了可展开的行分组功能的快照结果,作为示例:

在这种情况下,我们展开了 Firefox 版本 3 组,以查看随时间变化的所有浏览器细节。

子标题

所有相关项目可以使用子标题功能分组在一个子组下。这个用例类似于展开的行组,但这些子标题不能被折叠。通过设置rowGroupMode="subheader"groupField="engine"来启用此行为。groupField属性设置为特定的分类列。

具有子标题选项的行分组功能将被编写如下:

<p-dataTable [value]="browsers" sortField="engine"  
  rowGroupMode="subheader"
 groupField="engine" [styleClass]="'rowgroup-padding'">
  <p-header>Subheader</p-header>
  <ng-template pTemplate="rowgroupheader" let-rowData>
    {{rowData['engine']}}
 </ng-template>
  <p-column field="browser" header="Browser" sortable="true">
  </p-column>
  <p-column field="platform" header="Platform" sortable="true">
  </p-column>
  <p-column field="grade" header="Grade" sortable="true">
  </p-column>
</p-dataTable>

以下截图显示了具有子标题分组功能的表格的快照结果,作为示例:

子标题分组功能

在前面的用例中,所有浏览器细节都基于唯一的浏览器引擎进行分组,作为子标题。

RowSpan 组

行可以根据sortField属性进行分组。通过将rowGroupMode属性值设置为rowspan(即rowGroupMode="rowspan")来启用此功能。具有行跨度的行分组示例将被编写如下:

<p-dataTable [value]="browsers" sortField="engine"   
  rowGroupMode="rowspan"
 [styleClass]="'rowgroup-padding'">
  <p-header>RowSpan</p-header>
  <p-column field="engine" header="Engine" sortable="true"></p-column>
  <p-column field="platform" header="Platform" sortable="true">
  </p-column>
  <p-column field="browser" header="Browser" sortable="true">
  </p-column>
  <p-column field="grade" header="Grade" sortable="true"></p-column>
</p-dataTable>

以下截图显示了具有行跨度分组功能的组件的快照结果,作为示例:

行跨度分组功能

在这个版本的行分组中,浏览器的“引擎”字段用于跨越其所有相关项目的行分组。

使用延迟加载 DataTable 处理大量数据

延迟加载是处理大型数据集的非常关键的功能。此功能通过分页、排序和过滤操作加载数据块,而不是一次性加载所有数据。通过设置lazy模式(lazy="true")并使用onLazyLoad`回调来进行用户操作,事件对象作为参数。事件对象保存了分页、排序和过滤数据。

还需要使用投影查询显示要用于分页配置的逻辑记录数量。这是因为在延迟加载中我们只能检索当前页的数据。没有关于剩余记录的信息可用。因此,需要根据数据源中的实际记录显示分页链接。这可以通过表组件上的totalRecords属性实现。

具有延迟加载功能的组件将被编写如下:

<p-dataTable [value]="browsers" [lazy]="true" [rows]="10" 
  [paginator]="true" [totalRecords]="totalRecords" 
  (onLazyLoad)="loadBrowsersLazy($event)">
  <p-header>List of browsers</p-header>
  <p-column field="engine" header="Engine" [sortable]="true" 
  [filter]="true">
  </p-column>
  <p-column field="browser" header="Browser" [sortable]="true" 
  [filter]="true">  
  </p-column>
  <p-column field="platform" header="Platform" [sortable]="true" 
  [filter]="true">
  </p-column>
 <p-column field="grade" header="Grade" [sortable]="true" 
  [filter]="true">
  </p-column>
</p-dataTable>

组件类定义了延迟加载回调,以根据需要检索数据,如下所示:

loadBrowsersLazy(event: LazyLoadEvent) {
 //event.first = First row offset //event.rows = Number of rows per page //event.sortField = Field name to sort with //event.sortOrder = Sort order as number, 1 for asc and -1 for dec //filters: FilterMetadata object having field as 
  //key and filter value, 
  //filter matchMode as value    this.browserService.getBrowsers().subscribe((browsers: any) =>
    this.browsers = browsers.slice(event.first, 
    (event.first + event.rows)));
}

作为延迟加载的演示,我们使用分页操作来检索数据。我们还可以使用排序和过滤功能。以下截图显示了一个快照结果,以便作为示例进行说明:

在上面的快照中,我们可以清楚地观察到第 4 页的信息是动态从远程数据源检索的。有关延迟加载事件回调的更多详细信息,请参考事件部分。

总是更喜欢对大型数据集使用延迟加载以提高性能。

通过提供行模板进行行展开

在许多情况下,不可能容纳表中的所有数据。表数据的次要或附加信息需要以不同的表示形式填充。行展开功能允许为特定行显示详细内容(即,在请求时显示在单独的块中显示数据)。要使用此功能,请启用expandableRows属性,并使用expander属性作为单独列添加扩展列,以及常规列以切换行。要声明扩展内容,请使用pTemplate指令,并将rowexpansion作为值。从ng-template中使用本地模板引用变量来访问表数据。

具有行展开功能以显示浏览器的完整详细信息的组件将被编写如下:

<p-dataTable [value]="basicBrowsers" expandableRows="true"   
  [expandedRows]="expandedRows">
  <p-column expander="true" styleClass="col-icon" header="Toggle">
  </p-column>
  <p-column field="engine" header="Engine"></p-column>
  <p-column field="browser" header="Browser"></p-column>
  <p-column field="platform" header="Platform"></p-column>
  <p-column field="grade" header="Grade"></p-column>
  <ng-template let-browser pTemplate="rowexpansion">
    <div class="ui-grid ui-grid-responsive ui-fluid 
      rowexpansion-layout">
      <div class="ui-grid-row">
        <div class="ui-grid-col-9">
          <div class="ui-grid ui-grid-responsive ui-grid-pad">
            <div class="ui-grid-row">
              <div class="ui-grid-col-2 label">Engine:</div>
              <div class="ui-grid-col-10">{{browser.engine}}</div>
            </div>
            <div class="ui-grid-row">
              <div class="ui-grid-col-2 label">Browser:</div>
              <div class="ui-grid-col-10">{{browser.browser}}</div>
            </div>
            <div class="ui-grid-row">
              <div class="ui-grid-col-2 label">Platform:</div>
              <div class="ui-grid-col-10">{{browser.platform}}</div>
            </div>
            <div class="ui-grid-row">
              <div class="ui-grid-col-2 label">Version:</div>
              <div class="ui-grid-col-10">{{browser.version}}</div>
            </div>
            <div class="ui-grid-row">
              <div class="ui-grid-col-2 label">Rating:</div>
              <div class="ui-grid-col-10">{{browser.rating}}</div>
            </div>
            <div class="ui-grid-row">
              <div class="ui-grid-col-2 label">Grade:</div>
              <div class="ui-grid-col-10">{{browser.grade}}</div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </ng-template>
</p-dataTable>

如果需要,可以使用expandedRows属性将展开的行存储在组件类内的数组变量中。以下截图显示了具有行展开功能的组件的快照结果作为示例:

默认情况下,可以一次展开多行。我们可以通过将rowExpandMode属性设置为single来进行严格的单行展开。

我们也可以为分组表格应用行展开行为:

  • 该组件提供了一个expandableRowGroups布尔属性,用于创建切换行组的图标。

  • 默认情况下,所有行都将被展开。expandedRowGroups属性用于保存要默认展开特定行组的行数据实例。

提供了名为toggleRow的方法,用于切换表格行与行数据。

以 CSV 格式导出数据

数据可以在在线模式下随时以表格格式查看。但是,也需要离线模式下的数据。此外,在许多情况下,我们需要从网站获取大量数据报告。PrimeNG DataTable 可以使用exportCSV()API 方法以 CSV 格式导出。放置在表格内部或外部的按钮组件可以触发此方法,以便以 CSV 格式下载数据。具有导出 API 方法调用的组件将被编写如下:

<p-dataTable #dt [value]="basicBrowsers" exportFilename="browsers"   
  csvSeparator=";">
 <p-header>
    <div class="ui-helper-clearfix">
    <button type="button" pButton icon="fa-file-o" iconPos="left" label="CSV" (click)="dt.exportCSV()" style="float:left"></button>
    </div>
  </p-header>
  <p-column field="engine" header="Engine"></p-column>
  <p-column field="browser" header="Browser"></p-column>
  <p-column field="platform" header="Platform"></p-column>
  <p-column field="grade" header="Grade"></p-column>
</p-dataTable>

默认情况下,导出的 CSV 使用逗号(,)作为分隔符。但是,可以使用 DataTable 组件上的csvSeparator属性更改此行为。

DataTable 事件和方法

DataTable 组件针对每个功能提供了许多事件回调和方法。以下表格列出了所有表格事件回调的名称、参数详情和描述:

名称 参数 描述
onRowClick
  • event.originalEvent: 浏览器事件

  • event.data: 选定的数据

当点击行时调用的回调函数。
onRowSelect
  • event.originalEvent: 浏览器事件

  • event.data: 选定的数据

  • event.type: 选择类型,有效值为rowradiobuttoncheckbox

当选择行时调用的回调函数。
onRowUnselect
  • event.originalEvent: 浏览器事件

  • event.data: 未选择的数据

  • event.type: 取消选择类型,有效值为rowcheckbox

当使用 Meta 键取消选择行时调用的回调函数。
onRowDblclick
  • event.originalEvent: 浏览器事件

  • event.data: 选定的数据

当双击选择行时调用的回调函数。
onHeaderCheckboxToggle
  • event.originalEvent: 浏览器事件

  • event.checked: 头部复选框的状态

当头部复选框状态改变时调用的回调函数。
onContextMenuSelect
  • event.originalEvent: 浏览器事件

  • event.data: 选定的数据

当右键选择行时调用的回调函数。
onColResize
  • event.element: 调整列标题大小

  • event.delta:宽度变化的像素数

当列调整大小时调用的回调函数。
onColReorder
  • event.dragIndex:拖动列的索引

  • event.dropIndex:放置列的索引

  • event.columns:重新排序后的列数组

当列重新排序时调用的回调函数。
onLazyLoad
  • event.first:第一行偏移

  • event.rows:每页的行数

  • event.sortField:用于排序的字段名称

  • event.sortOrder:排序顺序作为数字,升序为1,降序为-1

  • 过滤器:具有字段作为键和过滤器值、过滤器matchMode作为值的FilterMetadata对象

当在延迟模式下进行分页、排序或过滤时调用的回调函数。
onEditInit
  • event.column:单元格的列对象

  • event.data:行数据

当单元格切换到编辑模式时调用的回调函数。
onEdit
  • event.originalEvent:浏览器事件

  • event.column:单元格的列对象

  • event.data:行数据

  • event.index:行索引

当编辑单元格数据时调用的回调函数。
onEditComplete
  • event.column:单元格的列对象

  • event.data:行数据

  • event.index:行索引

当单元格编辑完成时调用的回调函数(仅支持Enter键)。
onEditCancel
  • event.column:单元格的列对象

  • event.data:行数据

  • event.index:行索引

当使用Esc键取消单元格编辑时调用的回调函数。
onPage
  • event.first:页面中第一条记录的索引

  • event.rows:页面上的行数

当分页发生时调用的回调函数。
onSort
  • event.field:已排序列的字段名称

  • event.order:排序顺序为 1 或-1

  • event.multisortmeta:多重排序模式中的排序元数据。有关此对象结构的多重排序部分,请参见多重排序部分。

当列排序时调用的回调函数。
onFilter
onRowExpand
  • event.originalEvent:浏览器事件

  • data:要展开的行数据

当行展开时调用的回调函数。
onRowCollapse
  • event.originalEvent:浏览器事件

  • data:要折叠的行数据

当行折叠时调用的回调函数。
onRowGroupExpand
  • event.originalEvent:浏览器事件

  • group:分组的值

当行组展开时调用的回调函数。
onRowGroupCollapse
  • event.originalEvent:浏览器事件

  • group:组的值

折叠行组时调用的回调。

以下表格列出了常用的表格方法及其名称、参数和描述:

名称 参数 描述
reset - 重置排序、过滤和分页器状态
exportCSV - 以 CSV 格式导出数据
toggleRow data 切换给定行数据的行扩展

PrimeNG 版本 4.0.1 重新引入了rowTrackBy选项,用于迭代组件,如 DataTable、DataGrid 和 DataList,以改善 DOM 优化。也就是说,通过将决策委托给ngForTrackBy指令来优化每一行的 DOM 插入和更新。在 PrimeNG 中,这将通过rowTrackBy属性实现。如果未定义该属性,默认情况下算法会检查对象标识。例如,浏览器行通过 ID 属性标识为

trackById(index, browser) { return browser.id; }

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter5/datatable

使用 DataList 列出数据

DataList 组件用于以列表布局显示数据。它需要一个项目集合作为其值,并使用ng-template显示内容,其中每个项目都可以使用本地模板变量访问。该模板还使用let-i表达式表示每个项目的索引。将所有浏览器详细信息显示为列表格式的 DataList 组件的基本示例将如下所示:

<p-dataList [value]="basicBrowsers">
 <ng-template let-browser pTemplate="item">
    <div class="ui-grid ui-grid-responsive ui-fluid" 
      class="content-layout">
      <div class="ui-grid-row">
        <div class="ui-grid-col-3">
          <img src="/assets/data/images/{{browser.code}}.png" 
            width="100" height="80"/>
        </div>
        <div class="ui-grid-col-9">
          <div class="ui-grid ui-grid-responsive ui-fluid">
            <div class="ui-grid-row">
              <div class="ui-grid-col-2">Engine: </div>
              <div class="ui-grid-col-10">{{browser.engine}}</div>
            </div>
            <div class="ui-grid-row">
              <div class="ui-grid-col-2">Browser: </div>
              <div class="ui-grid-col-10">{{browser.browser}}</div>
            </div>
            <div class="ui-grid-row">
              <div class="ui-grid-col-2">Platform: </div>
              <div class="ui-grid-col-10">{{browser.platform}}</div>
            </div>
            <div class="ui-grid-row">
              <div class="ui-grid-col-2">Version: </div>
              <div class="ui-grid-col-10">{{browser.version}}</div>
            </div>
            <div class="ui-grid-row">
              <div class="ui-grid-col-2">Grade: </div>
              <div class="ui-grid-col-10">{{browser.grade}}</div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </ng-template>
</p-dataList>

需要从外部服务中检索浏览器详细信息的列表。在这种情况下,BrowserService服务将被注入到组件类中,以检索浏览器信息。我们使用可观察对象使用 HTTP 模块获取数据。列表数据将在页面加载时检索如下:

basicBrowsers: Browser[];

constructor(private browserService: BrowserService) { }

ngOnInit() {
  this.browserService.getBrowsers().subscribe(
    (browsers:any) => this.basicBrowsers = browsers.slice(0,4));
}

出于演示目的,我们将记录数限制为五条。以下屏幕截图显示了 DataList 组件以列表格式的快照结果作为示例:

前面的快照只是以表格格式显示数据。在下一节中,您可以找到更多功能,使数据列表成为一个强大的组件。

Facets 和分页

数据列表组件支持诸如标题和页脚之类的面板,使用 p-headerp-footer 标签。为了改善大型数据集上的用户体验,它支持分页功能。通过将 paginator 属性设置为 true 来启用此功能,并使用 rows 属性设置要显示的行数。除了这些必需的设置之外,分页还有一些可选的自定义设置。在所有这些可选属性中,paginatorPosition 用于在 topbottomboth 位置显示分页器;rowsPerPageOptions 用于显示一个下拉菜单,其中包含要在一页中显示的可能行数;emptyMessage 用于在没有记录存在时显示数据列表主体。分页还支持 onPage 事件回调,该事件将在页面导航时被调用。有关更多详细信息,请参阅事件部分。

具有面板和分页功能的数据列表组件以显示浏览器信息如下:

<p-dataList [value]="advancedBrowsers" [paginator]="true" [rows]="5"
 (onPage)="onPagination($event)" [rowsPerPageOptions]="[5,10,15]"
 [paginatorPosition]="both" [emptyMessage]="'No records found'">
  <p-header>
    List of Browsers
  </p-header>
    .... // Content
 <p-footer>
    Note: Grades are 3 types.A,B and C.
 </p-footer> </p-dataList>

以下屏幕截图显示了带有分页的快照结果:

数据列表组件提供了可自定义的所有分页控件选项。

懒加载

懒加载是处理大型数据集的非常有用的功能。它不会一次加载所有数据,而是根据用户需求逐步加载。DataList 支持分页交互上的懒加载。通过启用 lazy 属性(即 lazy="true")并调用 onLazyLoad 回调来实现此功能,从远程数据源检索数据。有关签名和更多详细信息,请参阅事件部分。

懒加载事件对象提供了页面中的第一条记录和当前页面中的行数,以获取下一组数据。此外,您应该通过投影查询提供总记录以进行分页配置。即使在页面加载时没有那么多记录可用(即,仅在懒惰模式下存在当前页面记录),这对基于可用记录总数显示分页链接非常有用。

让我们以此处显示的基本原型为例,来介绍数据列表组件的懒加载功能:

<p-dataList [value]="lazyloadingBrowsers" [paginator]="true" [rows]="5"   
  [lazy]="true"
 (onLazyLoad)="loadData($event)" [totalRecords]="totalRecords">
  ... // Content
</p-dataList>

组件类必须定义懒加载事件回调,以根据用户请求(在本例中,将是分页)检索记录,如下所示:

loadData(event:any) {
 let start = event.first;//event.first = First row offset
  let end = start + event.rows;//event.rows = Number of rows per page
  this.browserService.getBrowsers().subscribe((browsers: any) =>
 this.lazyloadingBrowsers = browsers.slice(start,end));
}

在上述代码片段中,您可以观察到事件的firstrows属性对于检索下一批记录非常有帮助。根据rows属性,它尝试在每个实例上获取下一个rows数量的记录。

事件

该组件提供两个事件回调,一个用于分页,另一个用于懒加载。两个事件都提供两个参数,以获取页面上第一条记录和行数。懒加载事件在启用懒加载模式的情况下,通过分页、过滤和排序功能来调用。

名称 参数 描述
onLazyLoad
  • event.first:第一行偏移

  • event.rows:每页的行数

当分页、排序或过滤以懒加载模式发生时调用的回调函数。
onPage
  • event.first:页面中第一条记录的索引

  • event.rows:页面上的行数

当分页发生时调用的回调函数。

它还提供许多其他功能,例如用于页眉和页脚显示的 facets(p-headerp-footer),用于在多个页面之间导航的分页,以及用于根据需要检索数据的懒加载功能。

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter5/datalist

使用 PickList 列出数据

PickList 组件用于在两个不同的列表之间移动项目。您还可以在每个列表内重新排序项目。这提供了所选项目的整体状态。项目可以使用默认按钮控件或拖放行为进行移动/重新排序。PickList 需要两个数组,一个用于源列表,另一个用于目标列表。使用ng-template模板标签来显示项目的内容,其中数组中的每个项目都可以使用本地ng-template变量访问。

使用国家信息的 PickList 组件的基本示例将如下所示:

<p-pickList [source]="sourceCountries" [target]="targetCountries"
 [sourceStyle]="{'height':'350px'}" [targetStyle]="{'height':'350px'}">
  <ng-template let-country pTemplate="item">
    <div class="ui-helper-clearfix">
      <img src="/assets/data/images/country/
        {{country.code.toLowerCase()}}.png" />
      <span>{{country.flag}} - {{country.name}}({{country.dial_code}})
 </span>
    </div>
 </ng-template>
</p-pickList>

在组件类中,让我们定义一个用于可用数据的源列表,以及一个用于表示尚未进行选择的空列表。需要注入国家服务以从外部资源访问国家信息:

sourceCountries: Country[];
targetCountries: Country[];

constructor(private countryService: CountryService) { }

ngOnInit() {
 this.countryService.getCountries().subscribe(
    (countries: Country[]) => 
  {
    this.sourceCountries = countries;
  });
  this.targetCountries = [];
}

默认情况下,源和目标面板都具有默认的widthheight属性。但是可以使用sourceStyletargetStyle属性来自定义此默认行为。以下屏幕截图显示了初始 PickList 的快照结果。

PickList 组件提供了六个事件回调,用于在两个列表之间移动项目和对源和目标区域中的项目进行排序。在这六个回调中,有四个用于移动项目,onMoveToTargetonMoveToSourceonMoveAllToSourceonMoveAllToSource,而排序项目则由onSourceReorderonTargetReorder执行。

该组件可以通过不同的方式进行自定义,如下所述:

  • 可以使用sourceHeadertargetHeader作为属性来自定义标题。

  • 网页将使用responsive属性(responsive="true")变得响应式,根据屏幕大小调整按钮控件。

  • 默认情况下,通过禁用metaKeySelection属性(metaKeySelection="false")来防止默认的多重选择(借助 Meta 键)。

  • 按钮控件的可见性通过showSourceControlsshowTargetControls属性进行控制。例如,showSourceControls="false"showTargetControls="false"

PrimeNG 4.1 支持使用filterBy属性对项目字段进行过滤,这是一个新的功能。可以通过在filterBy属性中放置逗号分隔的字段来过滤多个字段:

<p-pickList [source]="sourceCountries"  [target]="targetCountries"   filterBy="name, code">
  ...
</p-pickList>

新的 4.1 版本还支持启用dragdrop属性来实现拖放功能(在同一列表内或跨列表)。它还提供了dragdropScope属性,用于保存唯一键以避免与其他拖放事件发生冲突。拖放功能示例如下:

<p-pickList [source]="sourceCountries" [target]="targetCountries"   
  sourceHeader="Available" targetHeader="Selected" [dragdrop]="true" 
  dragdropScope="name">
   ...
</p-pickList>

完整的演示应用程序及说明可在 GitHub 上找到。

github.com/ova2/angular-development-with-primeng/tree/master/chapter5/picklist

使用 OrderList 列出数据。

OrderList 组件用于按不同方向(上下)对项目集合进行排序。该组件需要一个数组类型变量来存储其值,并使用ng-template来显示项目数组的内容。每个项目将在ng-template模板中使用本地ng-template变量进行访问。当项目位置发生变化时,后端数组也会更新以存储最新的项目顺序。

使用国家信息的 OrderList 组件的基本示例将如下编写:

<p-orderList [value]="countries" header="Favourite countries" >
 <ng-template let-country pTemplate="item">
    <div class="ui-helper-clearfix">
      <img src="/assets/data/images/country/
        {{country.code.toLowerCase()}}.png" />
      <span class="content-format">
        {{country.flag}} {{country.name}}({{country.dial_code}})
 </span>
    </div>
  </ng-template>
</p-orderList>

在组件类中,让我们定义一个国家列表来显示项目的集合。如下所示,需要注入国家服务以从外部资源或数据源访问国家信息:

countries: Country[];

constructor(private countryService: CountryService) { }

ngOnInit() {
 this.countryService.getCountries().subscribe((countries: Country[]) =>
  {
    this.countries = countries;
  });
}

默认情况下,列表面板具有默认的widthheight属性。但是可以使用listStyle属性进行自定义。以下截图显示了初始顺序列表的快照结果作为示例:

OrderList 组件提供了三种不同的事件回调,如下所示:

名称 参数 描述
onReorder event:浏览器事件 重新排序列表时要调用的回调函数。
onSelectionChange
  • originalEvent:浏览器事件

  • value:当前选择

选择更改时要调用的回调函数。
onFilterEvent
  • originalEvent:浏览器事件

  • value:当前过滤值

发生过滤时要调用的回调函数。

可以按以下方式以不同方式自定义组件的默认行为:

  • 可以使用header属性自定义标题

  • responsive属性(responsive="true")用于应用响应式行为,根据屏幕大小调整按钮控件

  • 通过禁用metaKeySelection属性(metaKeySelection="false")来防止默认的多重选择(借助于 Meta 键)。

以下截图显示了具有前面提到的自定义的国家列表的快照结果:

在前面的快照中,您可以观察到由于其responsive特性(responsive="true"),控件出现在顶部。我们还可以观察到面板宽度已根据视口大小进行了调整(使用listStyle属性)。

PrimeNG 4.1 版本支持过滤和拖放功能作为新的添加。过滤功能可以使用filterBy属性应用于单个字段和多个字段,类似于 DataTable 组件。例如,对国家数据进行多重过滤的功能如下:

<p-orderList [value]="countries" filterBy="name, code">
 ...
</p-orderList>

新的 4.1 版本还支持通过启用dragdrop属性重新排序项目的拖放功能。它还提供了dragdropScope属性,用于保存唯一键以避免与其他拖放事件发生冲突。拖放功能示例如下:

<p-orderList [value]="countries" [dragdrop]="true" dragdropScope="name">
  ...
</p-orderList>

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter5/orderlist

以 DataGrid 为例的网格化数据

DataGrid 以网格导向布局显示数据。数据以多个单元格以规律的方式排列的布局形式表示。它需要一个作为value属性的数组的项目集合和ng-template模板标签来显示其内容,其中每个项目都可以使用本地模板变量进行访问。模板内容需要包装在一个div元素中,以便使用任何网格 CSS 样式以网格布局格式化数据。

数据网格组件的基本示例与浏览器信息将如下所示:

<p-dataGrid [value]="basicBrowsers">
 <ng-template let-browser pTemplate="item">
    <div style="padding:3px" class="ui-g-12 ui-md-3">
      <p-panel [header]="browser.browser" [style]="{'text-
        align':'center'}">
        <img src="/assets/data/images/{{browser.code}}.png" 
          width="50"height="50"> 
        <div class="car-detail">{{browser.engine}} - 
          {{browser.version}}
 </div>
        <hr class="ui-widget-content" style="border-top:0">
        <i class="fa fa-search" (click)="selectBrowser(browser)"
 style="cursor:pointer"></i>
      </p-panel>
    </div>
  </ng-template>
</p-dataGrid>

组件类必须定义一个浏览器对象数组,这些对象是使用服务从远程数据源检索的。页面加载时访问的服务将如下所示:

basicBrowsers: Browser[];

constructor(private browserService: BrowserService) { }

ngOnInit() {
 this.browserService.getBrowsers().subscribe((browsers: any) =>
 this.basicBrowsers = browsers.slice(0, 12));
}

以下屏幕截图显示了数据网格组件在网格布局中的快照结果:

在上面的快照中,任何两个单元格之间的填充将保持一致。这可以通过该组件的皮肤类进行自定义。

基本用法之外 - 高级功能

在上面的快照中,浏览器数据以网格布局显示。但是,您可以观察到没有标题或页脚来总结上下文。标题和页脚方面可使用 p-headerp-footer 标签。

为了提高大型数据集的可用性,DataGrid 提供了分页功能,通过页面导航显示下一块数据。通过启用paginator属性并设置rows属性来提供此功能。与任何其他数据组件一样,分页功能如pageLinksrowsPerPageOptionspaginatorPositiontotalRecords都可用于自定义。

为了处理大量数据,DataGrid 支持懒加载功能,以便以块的方式访问大量数据。通过启用lazy属性来提供此功能。同时,应该在分页操作中使用onLazyLoad事件调用懒加载方法。

以下是定义了懒加载事件回调的组件类,其中event对象作为参数显示在这里:

loadData(event: any) {
 let start = event.first; //event.first = First row offset
  let end = start + event.rows; //event.rows = Number of rows per page
  this.browserService.getBrowsers().subscribe((browsers: any) =>
 this.lazyloadingBrowsers = browsers.slice(start,end));
}

以下屏幕截图显示了懒加载功能的快照结果示例:

在上述快照中,它显示了外观(头部和页脚)、自定义分页选项,并在用户需求时延迟加载数据。关于浏览器的附加信息将通过单击每个单元格中可用的搜索图标在对话框中显示。默认情况下,DataGrid 组件在各种屏幕尺寸或设备上都是响应式的布局显示。

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter5/datagrid

使用 DataScroller 进行按需数据加载

DataScroller 使用滚动功能按需显示数据。它需要一个项目集合作为其值,要加载的行数,以及ng-template模板标签来显示内容,其中每个项目都可以使用隐式变量访问。使用各种浏览器信息的 DataScroller 组件的基本示例将如下所示(请记住,这里使用了流体网格来格式化浏览器记录的内容):

<p-dataScroller [value]="basicBrowsers" [rows]="5">
 <ng-template let-browser pTemplate="item">
    <div class="ui-grid ui-grid-responsive ui-fluid" 
      class="content-layout">
      <div class="ui-grid-row">
        <div class="ui-grid-col-3">
          <img src="/assets/data/images/{{browser.code}}.png" 
            width="100" height="80"/>
        </div>
        <div class="ui-grid-col-9">
          <div class="ui-grid ui-grid-responsive ui-fluid">
            <div class="ui-grid-row">
              <div class="ui-grid-col-2">Engine: </div>
              <div class="ui-grid-col-10">{{browser.engine}}</div>
            </div>
            // Other content goes here
          </div>
        </div>
      </div>
    </div>
  </ng-template>
</p-dataScroller>

与任何其他数据组件一样,数据列表的组件类应该定义一个浏览器对象的数组。数据是通过对数据源进行远程调用来填充的。以下屏幕截图显示了一个示例的快照结果:

如前面的快照所示,数据根据窗口滚动作为目标按需显示。为了使 DataScroller 元素更易读,它支持使用p-headerp-footer标签的头部和页脚等方面。默认情况下,DataScroller 组件侦听窗口的滚动事件。还有另一种选项,可以使用内联模式将组件的容器定义为事件目标。为此,我们应该将inline属性启用为true(即inline="true")。

除了基于滚动的数据加载外,还可以使用显式按钮操作加载更多数据。组件应该定义一个引用 Button 组件的loader属性。带有加载器按钮的 DataScroller 组件将如下所示:

<p-dataScroller [value]="advancedBrowsers" [rows]="5" [loader]="loadButton">
  // Content goes here </p-dataScroller>
<p-dataScroller [value]="advancedBrowsers" [rows]="5" [loader]="loadButton">

以下屏幕截图显示了一个带有加载器显示的快照结果:

在上面的快照中,一旦用户在左侧点击搜索按钮,就会以对话框格式显示附加的浏览器信息。这可以展示如何在 DataScroller 组件中选择特定记录的能力。

惰性加载

为了处理大型数据集,该组件还支持惰性加载功能。它不是加载整个数据,而是在每次滚动操作时加载数据块。需要lazyonLazyLoad属性来启用此行为。DataScroller 的惰性加载示例将如下所示:

<p-dataScroller [value]="lazyloadingBrowsers" [rows]="5"
 [lazy]="true" (onLazyLoad)="loadData($event)">
  //Content goes here </p-dataScroller>

组件类定义了惰性加载事件回调,以按块检索数据,如下所示:

loadData(event: any) {
 let start = event.first; //event.first = First row offset
  let end = start + event.rows; //event.rows = Number of rows per page
  this.browserService.getBrowsers().subscribe((browsers: any) =>
 this.lazyloadingBrowsers = browsers.slice(start, end));
}

在上面的代码片段中,您可以观察到event对象的firstrows属性对于检索下一批记录非常有用。根据rows属性,它尝试在每次获取时获取下一个rows数量的记录。

API 方法reset用于重置 DataScroller 组件的内容或数据。也就是说,组件将重置为其默认状态。

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter5/datascroller

使用树形结构可视化数据

Tree 组件用于以图形格式显示数据的分层表示。它提供了TreeNode对象数组作为其值。TreeNode API 提供了许多属性来创建树节点对象。树结构基本上有三个主要组件,如下所示:

  • 树元素称为节点

  • 连接元素的线称为分支

  • 没有子节点的节点称为叶节点或叶子节点

具有节点的 Tree 组件的基本示例将如下所示(节点将表示旅游景点):

<p-tree [value]="basicTree"></p-tree>

Tree 组件的数据应以嵌套的父子层次结构提供。每个树节点都使用一组属性创建,例如labeldataexpandIconcollapsedIconchildren等等。TreeNode属性的完整列表如下所示:

名称 类型 默认 描述
label string null 节点的标签。
data any null 节点表示的数据。
icon string null 节点旁边显示的图标。
expandedIcon string null 展开状态下使用的图标。
collapsedIcon string null 折叠状态下使用的图标。
children TreeNode[] null 作为子节点的树节点数组。
leaf boolean null 指定节点是否有子节点。用于延迟加载。
style string null 节点的内联样式。
styleClass string null 节点的样式类。
expanded boolean null 节点是否处于展开或折叠状态。
type string null ng-template类型匹配的节点类型。
parent TreeNode null 节点的父节点。
styleClass string null 节点元素的样式类名称。
draggable boolean null 是否禁用特定节点的拖动,即使启用了draggableNodes
droppable boolean null 是否禁用特定节点的放置,即使启用了droppableNodes
selectable boolean null 用于禁用特定节点的选择。

TreeNode的所有属性都是可选的。

旅游景点示例的树节点结构如下:

"data":
[
  {
    "label": "Asia",
    "data": "Documents Folder",
    "expandedIcon": "fa-folder-open",
    "collapsedIcon": "fa-folder",
    "children": [{
      "label": "India",
      "data": "Work Folder",
      "expandedIcon": "fa-folder-open",
      "collapsedIcon": "fa-folder",
      "children": [{
 "label": "Goa", "icon": "fa-file-word-o",
 "data": "Beaches& Old Goa colonial architecture"},
          {"label": "Mumbai", "icon": "fa-file-word-o", "data": 
 "Shopping,Bollywood"},
          {"label": "Hyderabad", "icon": "fa-file-word-o", 
 "data": "Golconda Fort"}
      ]
    },
      {
        "label": "Singapore",
        "data": "Home Folder",
        "expandedIcon": "fa-folder-open",
        "collapsedIcon": "fa-folder",
        "children": [{
 "label": "Woodlands", "icon": "fa-file-word-o", 
 "data": "Parks,Sea food"}]
      },
    ]
  }
...
]

在实时应用程序中,位于远程数据源中的数据是通过服务检索的。以下服务将被注入到组件类中:

@Injectable()
export class TreeNodeService {

  constructor(private http: Http) { }

  getTouristPlaces(): Observable<any[]> {
    return this.http.get('/assets/data/cities.json')
      .map(response => response.json().data);
  }
}

组件类在页面加载时使用服务调用加载数据,如下所示:

basicTree: TreeNode[];

constructor(private nodeService: TreeNodeService) { }

ngOnInit() {
 this.nodeService.getTouristPlaces().subscribe(
    (places: any) => this.basicTree = places);
}

以下截图显示了分层树组件表示的快照结果,以示例为例:

在前面的用例中,我们展开了印度和德国的国家树节点,以查看它们表示为旅游地点的子节点。

选择功能 - 单选、多选和复选框

树组件支持三种选择方式,包括单选、多选和复选框。单选是通过启用selectionMode属性和selection属性来实现的,后者保存了一个选定的树节点。

具有单选功能的树组件,以选择一个喜爱的旅游地点,将如下所示:

<p-tree [value]="singleSelectionTree" selectionMode="single" [(selection)]="selectedPlace"  (onNodeSelect)="nodeSelect($event)" (onNodeUnselect)="nodeUnselect($event)"></p-tree>
<div>Selected Node: {{selectedPlace ? selectedPlace.label : 'none'}}</div>

以下截图显示了树组件的快照结果,以单选为例:

在这里,通过将selectionMode设置为multipleselectionMode="multiple")来启用多重选择。在这种情况下,selection属性保存一个作为选定节点的对象数组。多重选择也可以通过复选框选择来实现,只需将selectionMode="checkbox"

具有多个复选框选择功能的树组件,以选择多个旅游地点,将如下所示:

<p-tree [value]="checkboxSelectionTree" selectionMode="checkbox"
 [(selection)]="selectMultiplePlaces"></p-tree>
<div>Selected Nodes: <span *ngFor="let place of selectMultiplePlaces">{{place.label}} </span></div>

以下截图显示了树组件的快照结果,以复选框选择为例:

选择功能支持两个事件回调,如onRowSelectonRowUnselect,提供了选定和取消选定的树节点。有关更多详细信息,请参阅事件部分。

选择节点的传播(向上和向下方向)通过propagateSelectionUppropagateSelectionDown属性来控制,默认情况下是启用的。

超出基本用法 - 高级功能

树组件还支持许多高级功能:

  • 自定义内容可以使用模板标签ng-template来显示。

  • 可以使用onNodeExpand事件回调来实现延迟加载功能。

  • 为每个树节点应用上下文菜单,使用本地模板引用变量。

  • 使用layout="horizontal"表达式显示树组件的水平布局。

  • 通过启用draggableNodesdroppableNodes属性,可以在源树组件和目标树组件之间实现拖放功能。dragdropScope属性用于将拖放支持限制在特定区域。

可以通过将 API 方法外部化来以编程方式实现行展开或折叠行为。例如,下面显示了一个带有外部按钮的树,这些按钮用于使用事件回调以编程方式展开或折叠树节点。

<p-tree #expandingTree [value]="programmaticTree"></p-tree>
<div>
 <button pButton type="text" label="Expand all" (click)="expandAll()">
   </button>
  <button pButton type="text" label="Collapse all" (click)="collapseAll()"></button>
</div>

在此处显示了使用事件回调函数定义的组件类,以递归方式切换树节点的示例:

expandAll() {
 this.programmaticTree.forEach( (node: any) => {
    this.expandRecursive(node, true);
  } );
}

collapseAll() {
 this.programmaticTree.forEach((node: any) => {
    this.expandRecursive(node, false);
  } );
}

expandRecursive(node: TreeNode, isExpand: boolean) {
  node.expanded = isExpand;
  if (node.children) {
    node.children.forEach( childNode => {
      this.expandRecursive(childNode, isExpand);
    } );
  }
}

该组件还支持四个事件回调,如onNodeExpandonNodeCollapseonNodeDroponNodeContextMenuSelect。以下事件表提供了事件、参数及其描述的完整详细信息:

名称 参数 描述
onNodeSelect
  • event.originalEvent: 浏览器事件

  • event.node: 选定的节点实例

当选择节点时调用的回调函数。
onNodeUnselect
  • event.originalEvent: 浏览器事件

  • event.node: 取消选择的节点实例

当取消选择节点时调用的回调函数。
onNodeExpand
  • event.originalEvent: 浏览器事件

  • event.node: 展开的节点实例

当节点展开时调用的回调函数。
onNodeCollapse
  • event.originalEvent: 浏览器事件

  • event.node: 折叠的节点实例

当节点折叠时调用的回调函数。
onNodeContextMenuSelect
  • event.originalEvent: 浏览器事件

  • event.node: 选定的节点实例

当通过右键单击选择节点时调用的回调函数。
onNodeDrop
  • event.originalEvent: 浏览器事件

  • event.dragNode: 被拖动的节点实例

  • event.dropNode: 被拖放的节点实例

当通过右键单击选择节点时调用的回调函数。

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter5/tree

使用 TreeTable 可视化数据

TreeTable 用于以表格格式显示分层数据。它需要一个TreeNode对象数组作为其值,并提供了许多可选属性的TreeNodeAPI。TreeTable 将列组件定义为具有headerfooterfieldstyle属性的子元素,类似于 DataTable 组件。

将旅游地点树节点作为信息的 TreeTable 组件的基本示例将如下编写:

<p-treeTable [value]="basicTreeTable">
 <p-header>Basic</p-header>
  <p-column field="name" header="Name"></p-column>
  <p-column field="days" header="Days"></p-column>
  <p-column field="type" header="Type"></p-column>
</p-treeTable>

该组件是通过以分层方式排列TreeNode对象来创建的。TreeNode对象包括许多属性,如下所列:

名称 类型 默认 描述
label string null 节点的标签。
data any null 由节点表示的数据。
icon string null 要显示在内容旁边的节点图标。TreeTable 不使用。
expandedIcon string null 用于展开状态的图标。TreeTable 不使用。
collapsedIcon string null 用于折叠状态的图标。TreeTable 不使用。
children TreeNode[] null 作为子节点的树节点数组。
leaf boolean null 指定节点是否有子节点。用于延迟加载。
style string null 节点的内联样式。
styleClass string null 节点的样式类。

旅游景点示例的TreeNode结构如下:

{
  "data": [
    {
      "data": {
        "name": "Asia",
        "days": "15",
        "type": "Continent"
  },
      "children": [
        {
          "data": {
            "name": "India",
            "days": "6",
            "type": "Country"
  },
          "children": [
            {
              "data": {
                "name": "Goa",
                "days": "2",
                "type": "City"
  }...
            }]
          }]
     } }
  ...
}

注入的服务和组件类中的相同服务调用表示几乎与前一节中解释的 Tree 组件相似。以下屏幕截图显示了以层次结构的旅游信息为例的快照结果:

该组件还支持动态列,其中每列都是通过循环ngFor指令创建的。

选择功能 - 单选、多选和复选框

TreeTable 组件支持三种选择功能,包括单选、多选和复选框。单选通过在树表上启用selectionMode属性和selection属性来实现,该属性保存了所选的树表节点。

具有单选功能的 TreeTable 组件,用于选择喜爱的旅游景点,将如下编写:

<p-treeTable [value]="singleSelectionTreeTable" selectionMode="single"   
  [(selection)]="selectedTouristPlace   
  (onNodeSelect)="nodeSelect($event)"   
  (onNodeUnselect)="nodeUnselect($event)" 
  (onRowDblclick)="onRowDblclick($event)" >
    <p-header>Singe Selection</p-header>
    <p-column field="name" header="Name"></p-column>
    <p-column field="days" header="Days"></p-column>
    <p-column field="type" header="Type"></p-column>
</p-treeTable>

以下屏幕截图显示了以单选为例的快照结果:

而多选功能是通过将selectionMode设置为多选(selectionMode="multiple")来启用的。在这种情况下,selection属性保存了所选节点的对象数组。多选也可以通过复选框选择来实现。这可以通过设置selectionMode="checkbox"来实现。

具有多复选框选择功能的 TreeTable 组件,用于选择多个旅游景点,将如下所示:

<p-treeTable [value]="checkboxSelectionTreeTable" selectionMode="checkbox"
 [(selection)]="selectedMultiTouristPlaces">
  <p-header>Checkbox Selection</p-header>
  <p-column field="name" header="Name"></p-column>
  <p-column field="days" header="Days"></p-column>
  <p-column field="type" header="Type"></p-column>
</p-treeTable>

以下屏幕截图显示了复选框选择的快照结果:

选择功能支持两个事件回调,例如onNodeSelectonNodeUnselect,它提供了选定和取消选定的树节点。有关更多详细信息,请参阅事件部分。

基本用法之外 - 高级功能

TreeTable 组件还支持各种高级功能,例如使用onNodeExpand回调进行延迟加载,使用ng-template模板标签进行自定义可编辑内容,以及上下文菜单实现,这与 DataTable 组件类似。它还支持使用p-headerp-footer标签为头部和底部添加外观。

TreeTable 的内容显示是使用ng-template进行自定义的。默认情况下,树节点的标签显示在树节点内。要自定义内容,请在获取列的列中定义ng-template作为隐式变量(let-col),并将rowData定义为节点实例(let-node="rowData")。同样,我们可以自定义此组件的头部和底部。

让我们以可编辑的树节点为例,通过在每个模板中放置一个输入框来实现:

<p-treeTable [value]="templateTreeTable">
 <p-header>Editable Cells with Templating</p-header>
  <p-column field="name" header="Name">
    <ng-template let-node="rowData" pTemplate="body">
      <input type="text" [(ngModel)]="node.data.name" 
        class="edit-input">
    </ng-template>
  </p-column>
  <p-column field="days" header="Days">
    <ng-template let-node="rowData" pTemplate="body">
      <input type="text" [(ngModel)]="node.data.days" 
        class="edit-input">
    </ng-template>
  </p-column>
  <p-column field="type" header="Type">
    <ng-template let-node="rowData" pTemplate="body">
      <input type="text" [(ngModel)]="node.data.type" 
        class="edit-input">
    </ng-template>
  </p-column>
</p-treeTable>

以下屏幕截图显示了具有可编辑模板的快照结果:

在上述快照中,我们可以编辑所有树节点字段。例如,我们将旅游套餐的天数从 9 天更新为 20 天。TreeTable 还支持扩展/折叠节点的事件回调,例如onNodeExpandonNodeCollapse,以及上下文菜单的onContextmenuSelect事件。有关更多详细信息,请参阅事件部分。

PrimeNG 4.1 引入了toggleColumnIndex属性,用于定义包含toggler元素的列的索引。默认情况下,toggleColumnIndex的值为0(如果未定义togglerColumnIndex,TreeTable 始终在第一列显示toggler)。

以下事件表提供了事件、参数及其描述的完整详细信息:

名称 参数 描述
onNodeSelect
  • event.originalEvent:浏览器事件

  • event.node:选定的节点实例

调用节点被选中时的回调。
onNodeUnselect
  • event.originalEvent:浏览器事件

  • event.node:取消选定的节点实例

当节点取消选定时要调用的回调函数。
onNodeExpand
  • event.originalEvent: 浏览器事件

  • event.node: 展开的节点实例

当节点展开时要调用的回调函数。
onNodeCollapse
  • event.originalEvent: 浏览器事件

  • event.node: 折叠的节点实例

当节点折叠时要调用的回调函数。
onContextMenuSelect
  • event.originalEvent: 浏览器事件

  • event.node: 选定的节点实例

当右键选择节点时要调用的回调函数。
onRowDblclick
  • event.originalEvent: 浏览器事件

  • event.node: 选定的节点实例

双击行时要调用的回调函数。

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter5/treetable.

使用日程安排管理事件

日程安排是基于FullCalendar jQuery 插件的全尺寸拖放事件日历。日程安排的事件应该形成一个数组,并使用events属性进行定义。日程安排组件依赖于FullCalendar库,因此它需要您页面中列出的以下资源:

  • 日程安排组件嵌入到网页中,使用样式表和 JavaScript 文件。因此,我们需要在 HTML 页面的head部分包含FullCalendar库的样式表(.css)和 JavaScript(.js)文件。

  • jQueryMoment.js库添加为完整日历的强制库。这两个库必须在加载FullCalendar库的 JavaScript 文件之前加载。

因此,我们在根index.html文件中包含了FullCalendar和其他依赖资源,如下所示:

<!-- Schedule CSS resources--> <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.1.0/
fullcalendar.min.css">
<!-- Schedule Javascript resources--> <script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.13.0/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.1.0/
fullcalendar.min.js"></script>

为整个月份定义的日程安排组件的基本示例将如下所示编写:

<p-schedule [events]="events" [height]="700" 
  [styleClass]="'schedule-width'">
</p-schedule> 

基本上,所有类型的事件都有标题、持续时间(开始和结束日期)、日期类型(全天/部分天)等属性。因此,事件类将如下所示定义:

export class MyEvent {
 id: number;
  title: string;
  start: string;
  end: string;
  allDay: boolean = true;
}

日程安排事件的数据应该按照上述格式作为原型来定义。但在实时情况下,数据是通过远程服务调用获取的,并且在事件发生变化时立即更新到日程安排界面。用于从数据源检索数据的事件服务(在本例中,它使用 HTTP 模块和可观察对象从 JSON 事件文件中检索数据)定义如下:

@Injectable()
export class EventService {

  constructor(private http: Http) { }

  getEvents(): Observable<any> {
    return this.http.get('/assets/data/scheduleevents.json')
      .map(response => response.json().data);
  }
}

注入的服务在网页初始加载时获取数据。如下所示,组件类必须定义可观察对象的订阅:

events: any[];

constructor(private eventService: EventService) { }

ngOnInit() {
 this.eventService.getEvents().subscribe((events: any) => 
  {this.events = events;});
}

以下截图显示了嵌入式日程安排组件显示的快照结果作为示例:

根据前面的快照,标题显示为日期(月份和年份)、今天标签和月份导航控件。主体或内容区域包含了每个月的每一天以及特定日期上的事件,以蓝色覆盖区域显示。

标题定制

在前面的快照中,我们观察到了日程安排的内容区域以及默认标题文本和控件。日程安排元素的默认标题配置对象将被编写如下:

{
  left: 'title', 
  center: '',
  right: 'today prev,next' }

通过header属性修改了上述默认标题显示,该属性保存了标题配置对象,如下所示:

<p-schedule [events]="events" [header]="headerConfig" [height]="700"
 [styleClass]="'schedule-width'"></p-schedule>

让我们定义左侧的导航控件,中间的标题,以及右侧的视图类型(月、周、日),以将其表示为配置对象:

this.headerConfig = {
 left: 'prev,next today',
  center: 'title',
  right: 'month,agendaWeek,agendaDay' };

以下截图显示了自定义日程安排标题的快照结果作为示例:

基本用法之外 - 高级功能

除了上述常规功能之外,日程安排组件还通过onViewRender事件回调支持懒加载,当新的日期范围被渲染或视图类型发生变化时将被调用。带有懒加载事件回调调用的日程安排组件将被编写如下:

<p-schedule [events]="events" (onViewRender)="loadEvents($event)" [height]="700" [styleClass]="'schedule-width'"></p-schedule>

组件类定义了一个懒加载回调,以便按需检索事件数据,并且将被编写如下:

loadEvents(event: any) {
 let start = event.view.start;
  let end = event.view.end;
  // In real time the service call filtered based on  
  //start and end dates
  this.eventService.getEvents().subscribe((events: any) =>
  {this.events = events;});
}

该组件还通过locale属性支持本地化。例如,通过设置locale="de"来表示德语标签。本地化标签应该在类似日历的组件中定义。

当事件数据发生任何变化时,UI 会自动更新。这对于在日程安排上实现 CRUD 操作非常有帮助。

事件和方法

日程安排组件提供了许多事件回调,包括点击、鼠标、调整大小和拖放用户操作,如下所列:

名称 描述
onDayClick 当用户点击某一天时触发
onEventClick 当用户点击事件时触发
onEventMouseover 当用户将鼠标悬停在事件上时触发
onEventMouseout 当用户鼠标移出事件时触发
onEventDragStart 当事件拖动开始时触发
onEventDragStop 当事件拖动停止时触发
onEventDrop 当拖动停止且事件已移动到不同的日期/时间时触发
onEventResizeStart 当事件调整大小开始时触发
onEventResizeStop 当事件调整大小停止时触发
onEventResize 当调整大小停止且事件持续时间发生变化时触发
onViewRender 当新的日期范围被渲染或视图类型切换时触发
onViewDestroy 当渲染的日期范围需要被销毁时触发
onDrop 当可拖动对象被放置到日程表上时触发

此外,它提供了许多 API 方法来处理不同的用例,如下所示:

名称 参数 描述
prev() - 将日程表向后移动一步(可以是一个月、一周或一天)
next() - 将日程表向前移动一步(可以是一个月、一周或一天)
prevYear() - 将日程表向后移动一年
nextYear() - 将日程表向前移动一年
today() - 将日程表移动到当前日期
gotoDate(date) date: 要导航的日期 将日程表移动到任意日期
incrementDate(duration) duration: 要添加到当前日期的持续时间 将日程表向前/向后移动任意时间量
getDate() - 返回日历当前日期的时刻
changeView(viewName) viewName: 要切换到的有效视图字符串 立即切换到不同的视图

上述 API 方法将完全控制日程表。这些方法调用在许多用例中非常有帮助。例如,通过.next()方法访问日程表的下一个视图(月、周或日)如下所示:

<p-schedule [events]="events" #schedule></p-schedule>
<button type="button" pButton (click)="next(schedule)"></p-button>

组件类定义了点击事件回调,将调用下一个日期、周或月,如下所示:

next(schedule) {
  schedule.next();
}

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter5/schedule.

摘要

此时,您将对所有数据迭代组件及其最常用的功能有一个概览,比如选择行、排序、分页、过滤数据等等。接下来,我们能够以表格、网格和列表格式显示(分层)数据。此外,您将了解如何在 DataTable 中实现调整大小、重新排序、切换和分组列,自定义单元格内容,并使用 Tree 和 TreeTable 组件可视化数据。在下一章中,您将看到一些令人惊叹的覆盖层,比如对话框、确认对话框、覆盖面板和通知组件,比如 growl 和消息,以及各种功能。

第六章:令人惊叹的覆盖和消息

令人惊叹的覆盖和消息展示了模态或非模态覆盖(如对话框、灯箱和覆盖面板)中显示的各种内容的不同变体。当内容显示在上述覆盖中时,用户不会离开页面流。覆盖组件会覆盖页面上的其他组件。PrimeNG 还提供通知组件来显示各种消息或咨询信息。这些消息组件也将被描述。

在本章中,我们将涵盖以下主题:

  • 在弹出模式下显示内容

  • OverlayPanel 的多功能场景

  • 在灯箱中显示内容

  • 使用消息和 Growl 通知用户

  • 表单组件的工具提示

在弹出模式下显示内容

网站的附加信息可以以弹出格式表示。这将通过最佳视口改善用户体验。存在两种类型的弹出格式:对话框确认对话框

对话框

对话框是一个容器组件,用于在覆盖窗口中显示内容。为了保存网页的视口,对话框非常有用,可以以弹出格式显示附加信息。对话框的可见性通过visible属性控制。

默认情况下,对话框以falsevisibility隐藏,并启用visible属性显示对话框。由于对话框具有双向绑定的特性,使用关闭图标关闭对话框后,visible属性会自动变为falsecloseOnEscape属性用于使用Esc键关闭对话框。

对话框组件的基本示例与源按钮将如下所示:

<p-dialog header="PrimeNG" [(visible)]="basic"> 
  PrimeNG content goes here.... </dialog>

visible属性在用户操作时被启用。以下屏幕截图显示了基本对话框示例的快照结果:

对话框组件支持两个名为onShowonHide的事件回调,当对话框显示或隐藏时将被调用。

可用性功能

通过使用draggableresizableclosableresponsive属性,对话框组件的用户体验将得到改善,具有可拖动、可调整大小、可关闭和响应式功能。除了这些交互功能,modal属性通过透明背景防止用户在主页面上执行操作,而dismissableMask在用户点击透明背景时隐藏对话框。

这些属性的默认值如下:

  • draggable = true

  • resizable = true

  • closable = true

  • responsive = false

  • modal = false

  • dismissableMask = false

自定义标题和页脚

对话框的标题通过header属性定义,并且可以通过showHeader属性进行控制。对话框组件的页眉和页脚部分可以使用p-headerp-footer标签以更灵活的方式进行定义。为了使用它们,需要导入页眉和页脚组件,并在指令部分声明它。

具有自定义标题和页脚的对话框组件的自定义示例将如下所示:

<p-dialog[(visible)]="custom" modal="true">
 <p-header>
    PrimeNG License declaration
  </p-header>
  All widgets are open source and free to use under MIT License.
  If agree with the license please click 'Yes' otherwise click 'No'.
  <p-footer>
    <div class="ui-dialog-buttonpane ui-widget-content 
      ui-helper-clearfix">
      <button type="button" pButton icon="fa-close" (click)="onComplete()" label="No"></button>
      <button type="button" pButton icon="fa-check" (click)="onComplete()" label="Yes"></button>
    </div>
  </p-footer>
</p-dialog>

以下截图显示了自定义对话框示例的快照结果:

前面的快照显示了如何根据需要或要求自定义标题、消息和页脚图标。默认情况下,对话框组件在视口中居中对齐,但可以使用positionLeftpositionTop属性进行自定义。

ConfirmDialog

ConfirmDialog 是一个用于同时显示多个操作的确认窗口的组件。在这种情况下,它将由使用可观察对象的确认服务支持。需要导入使用多个操作的确认方法的服务。

使用源按钮(或对话框生成器按钮)的 ConfirmDialog 组件的基本示例将如下所示:

<p-confirmDialog></p-confirmDialog>
    <button type="button" (click)="confirmAccept()" pButton
      icon="fa-check" label="Confirm"></button>
    <button type="button" (click)="confirmDelete()" pButton
      icon="fa-trash" label="Delete"></button>

在上面的示例中,确认方法将确认一个实例,用于自定义对话框 UI 以及接受和拒绝按钮。例如,accept函数调用确认服务的确认方法,决定需要执行什么操作:

confirmAccept() {
 this.confirmationService.confirm({
    message: 'Do you want to subscribe for Angular news feeds?',
    header: 'Subscribe',
    icon: 'fa fa-question-circle',
    accept: () => {
      this.msgs = [];
      this.msgs.push({severity:'info', summary:'Confirmed',
                     detail: 'You have accepted'});
    }
  });
}

点击按钮组件后,对话框出现。以下截图显示了基本确认对话框示例的快照结果:

页脚的接受和拒绝按钮决定是否订阅 Angular 新闻订阅系统。

自定义

提供确认对话框的标题、消息和图标有两种方式。一种是声明性的方式,通过属性(headermessageicon)提供所有功能,而另一种方式是程序化的方式,通过确认实例属性使值可以动态变化。甚至页脚部分的按钮也可以通过它们自己的 UI(acceptLabelacceptIconacceptVisibilityrejectLabelrejectIconrejectVisibility)进行自定义,以及本地ng-template变量的接受和拒绝方法。

一个带有标题和页脚的自定义确认对话框组件的示例将如下编写:

<p-confirmDialog header="Confirmation" message="Do you like to use  
  DataTable component" icon="fa fa-question-circle" width="400" 
  height="200" #confirmation>
  <p-footer>
    <button type="button" pButton icon="fa-close" label="No" 
    (click)="confirmation.reject()"></button>
    <button type="button" pButton icon="fa-check" label="Yes" 
    (click)="confirmation.accept()"></button> </p-footer>
</p-confirmDialog>

以下截图显示了自定义确认对话框示例的快照结果:

在上面的快照中,所有标题、消息和图标都是以声明性的方式进行自定义的。确认对话框提供了默认的closableresponsivecloseOnEscape属性,这与对话框组件类似。

完整的演示应用程序及说明可在 GitHub 上找到:

OverlayPanel 的多用途场景

OverlayPanel 是一个容器组件,可以在页面的其他组件上方显示附加信息。使用本地ng-template变量的showtoggle方法来显示该元素,使用hidetoggle方法来隐藏它。请记住,show方法将允许第二个参数作为目标元素,它必须显示 Overlay(而不是源)。Overlay 组件与源按钮生成器的基本示例将如下所示:

<p-overlayPanel #overlaybasic>
 <img src="/assets/data/images/primeng.png" alt="PrimeNG Logo" />
</p-overlayPanel>
<button type="button" pButton label="Logo" (click)="overlaybasic.toggle($event)"></button>

在上面的示例中,Overlay 将在单击按钮组件时出现。以下截图显示了基本 Overlay 示例的快照结果:

在上面的快照中,Overlay 在点击 logo 按钮时显示 PrimeNG logo 作为图像。默认情况下,OverlayPanel 附加到页面的 body 上,但可以使用 appendTo 属性更改目标。

与其他组件集成

OverlayPanel 组件也可以与其他 PrimeNG 组件集成。例如,下面的快照显示了如何使用 ng-template 将 Overlay 组件与 DataTable 组件集成。在这种情况下,按钮需要放置在 DataTable ng-template 内,并通过 toggle 事件触发 Overlay:

在上面的快照中,Overlay 面板用于通过点击每行的结果按钮以弹出格式显示聚合信息,如标记和百分比。

可关闭属性

默认情况下,Overlay 面板外部的交互会立即关闭 Dialog。可以使用 dismissable 属性阻止此行为。同时,还可以使用 showCloseIcon 属性在右上角显示关闭选项。

Dialog 组件支持四个事件回调,分别为 onBeforeShowonAfterShowonBeforeHideonAfterHide,当 Dialog 被显示或隐藏时将被调用。

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter6/overlaypanel.

在 Lightbox 中显示内容

LightBox 组件用于以模态 Overlay 模式显示图像、视频、内联 HTML 内容,以及 iframe 集合。存在两种类型的 LightBox 模式:一种是默认的 image 类型,另一种是 content 类型。在图像模式中,将显示图像集合,其中每个条目代表一个图像对象,代表图像的来源、缩略图和标题。一个带有 Angular 会议集合(或数组)的 LightBox 的基本示例如下:

<p-lightbox [images]="images" name="image"></p-lightbox>

组件将呈现如下截图所示:

在上面的快照中,所有图像都显示为图像库,并通过下一个和上一个图标进行导航。

自定义内容模式

通过将type属性设置为content来启用内容模式,这将提供一个锚点(或链接)来打开 LightBox 并在其中显示内容。一个自定义内容的 LightBox 示例,包含一系列 Angular 会议,如下所示:

<p-lightbox type="content" name="content">
 <a class="group" href="#">
    Watch PrimeNG Video
  </a>
  <iframe width="500" height="300" 
    src="https://www.youtube.com/watch?v=Jf9nQ36e0Fw&t=754s" frameborder="0" allowfullscreen></iframe>
</p-lightbox>

该组件将作为 iframe 视频呈现在覆盖面板内,如下截图所示:

如上面的快照所示,视频列表被显示出来,并且可以在弹出模式下观看视频,以获得更好的体验。

过渡效果

LightBox 组件在图片之间的过渡效果更加强大。这可以通过easing属性实现。在这里,默认值是ease-out(即,使用easing属性自定义效果)。还有许多其他效果可用,支持整个 CSS3 效果列表。此外,默认情况下,效果持续时间为500ms。这也可以通过effectDuration属性进行自定义。

作为 LightBox 的过渡效果的一个示例,包含一系列 Angular 会议的效果如下:

<p-lightbox [images]="images" name="effects" easing="ease-out"  
  effectDuration="1000ms">
</p-lightbox>

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter6/lightbox.

通过消息和 Growl 通知用户

消息组件用于以内联格式显示消息,以通知用户。这些消息是作为特定操作的结果而通知的。PrimeNG API 中的每条消息都是使用Message接口定义的,该接口定义了severitysummarydetail属性。

通知用户的消息的基本示例如下:

<p-messages ([value])="messages" name="basic"></p-messages>

在上面的例子中,消息使用value属性显示,该属性定义了Message接口的数组。该组件将如下截图所示呈现:

消息的严重程度由class属性表示。消息严重程度的可能值如下:

严重程度 类名
success .ui-button-success
info .ui-button-info
warn .ui-button-warn
error .ui-button-error

消息默认情况下可以通过位于右上角的关闭图标关闭。这种行为可以通过closable属性进行修改,即[closable]="false"会禁用消息的可关闭性。

Growl - 另一种通知信息的方式

与消息组件类似,Growl 用于以覆盖模式而不是内联模式显示特定操作的消息。每条消息通过Message接口表示,具有severitysummarydetails。Growl 通知用户的基本示例如下:

<p-growl ([value])="messages" name="basic"></p-growl>

value属性在后台组件模型中定义了Message接口的数组。组件将呈现如下截图所示:

与消息组件类似,Growl 中也可以定义相同的严重类型。PrimeNG 4.1 版本引入了onClick事件回调,当单击消息时将被调用。

粘性行为

默认情况下,Growl 消息在一定时间后会被移除。Growl 消息的默认寿命是3000ms。这可以使用life属性进行自定义(即life="5000")。要使消息成为粘性消息,无论提到的寿命如何,您都应该启用粘性行为,即sticky="true"

PrimeNG 版本 4.0.1 支持 Growl 消息的双向绑定功能。由于这个特性,每当消息从 UI、后端实例或消息中被手动移除时,数组将立即更新。完整的演示应用程序及说明可在 GitHub 上找到

表单组件的工具提示

工具提示为组件提供了咨询信息。这在使用目标组件之前给出了简要的见解。工具提示通过pTooltip指令应用,其值定义要显示的文本。除此之外,还可以使用escape属性显示 HTML 标签,而不是常规文本信息。工具提示的基本示例是为输入提供咨询信息,如下所示:

<input type="text" pInputText pTooltip="Enter your favourite component   
  name" >

工具提示显示在输入框的右侧,如下面的屏幕截图所示:

默认情况下,工具提示位置显示在目标组件的右侧。可以使用tooltipPosition属性将此行为更改为其他值,例如toprightbottom,例如,具有top值的tooltipPosition将导致如下屏幕截图所示:

默认情况下,工具提示在悬停在目标元素上时显示(即,调用工具提示信息的默认事件是悬停)。可以使用tooltipEvent属性进行自定义,该属性提供焦点事件以显示和模糊事件以隐藏工具提示。请记住,工具提示也可以使用tooltipDisabled属性禁用。

输入的工具提示事件示例如下:

<input type="text" pInputText pTooltip="Enter your favourite component 
  name" tooltipEvent="focus" placeholder="Focus inputbox"/>

默认情况下,工具提示分配给文档主体。如果工具提示的目标放置在滚动容器内(例如,溢出的div元素),则将工具提示附加到具有相对位置的元素。可以使用appendTo属性实现这一点(即appendTo="container")。

PrimeNG 版本 4.1 提供了showDelayhideDelay属性,以便在显示和隐藏工具提示时添加延迟(以毫秒为单位的数字值)。延迟功能将应用如下:

<input type="text" pInputText pTooltip="Enter your favourite component 
  name" tooltipEvent="focus" placeholder="Focus inputbox" 
  showDelay="1000" hideDelay="400"/>

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter6/tooltips.

摘要

通过阅读本节,您将能够了解如何在不离开当前页面流的情况下在覆盖窗口中显示图像、视频、iframe 和 HTML 内容。最初,您将看到如何使用对话框、确认对话框、灯箱和覆盖组件。之后,您将学习如何通过消息和生长组件在覆盖中显示内联消息或消息。

最后一个示例介绍了用于显示咨询信息的工具提示组件。所有这些组件都是通过逐步方法解释的,具有所有可能的功能。在下一章中,您将看到如何使用菜单模型 API、导航和菜单变体,如菜单、超级菜单、菜单栏、滑动菜单、面板菜单、分层菜单等,具有各种功能。

第七章:无尽的菜单变体

在本章中,您将了解到几种菜单变体。PrimeNG 的菜单满足所有主要要求。如今,每个网站都包含菜单。通常,菜单被呈现给用户作为要导航或执行的命令的链接列表。菜单有时是按层次组织的,允许通过菜单结构的不同级别进行导航。

将菜单项排列成逻辑组,使用户可以快速找到相关任务。它们具有各种方面,如静态、动态、分层、混合、iPod 风格等,无所不包。读者将面临许多讨论菜单结构、配置选项、自定义以及与其他组件集成的示例。

在本章中,我们将涵盖以下主题:

  • 使用 MenuModel API 创建程序化菜单

  • 静态和动态定位的菜单

  • 通过 MenuBar 访问命令

  • 带有嵌套项的上下文菜单

  • SlideMenu - iPod 风格的菜单

  • TieredMenu - 嵌套覆盖中的子菜单

  • MegaMenu - 多列菜单

  • PanelMenu - 手风琴和树的混合

  • TabMenu - 菜单项作为选项卡

  • Breadcrumb - 提供有关页面层次结构的上下文信息

使用 MenuModel API 创建程序化菜单

PrimeNG 提供了一个MenuModel API,它将被所有菜单组件共享,用于指定菜单项和子菜单。MenuModel API 的核心项目是MenuItem类,具有labeliconurl、带有items选项的子菜单项等选项。

让我们以菜单组件为例,代表常见的工具栏用户界面。菜单组件通过model属性绑定MenuItem类的数组作为项目,如下所示:

<p-menu [model]="items"></p-menu>

MenuItemMenuModel API 中的关键项目。它具有以下属性列表。每个属性都用类型、默认值和描述进行描述:

名称 类型 默认 描述
label 字符串 null 项目的文本。
icon 字符串 null 项目的图标。
command 函数 null 单击项目时要执行的回调。
url 字符串 null 单击项目时要导航到的外部链接。
routerLink 数组 null 用于内部导航的 RouterLink 定义。
items 数组 null 子菜单项的数组。
expanded boolean false 子菜单的可见性。
disabled boolean false 当设置为true时,禁用菜单项。
visible boolean true 菜单项的 DOM 元素是否已创建。
target string null 指定打开链接文档的位置。

表 1.0

菜单操作

具有纯文本只读标签和图标的菜单项并不是真正有用的。具有用户操作的菜单组件需要执行业务实现或导航到其他资源。菜单操作的主要组件是命令调用和导航。这可以通过MenuItem接口的urlrouterLink属性来实现。

MenuItem API 的 URL 和路由链接选项的示例用法如下:

{label: 'View', icon: 'fa-search', command: 
  (event) => this.viewEmployee(this.selectedEmployee)}

{label: 'Help', icon: 'fa-close', url: 
 'https://www.opm.gov/policy-data- oversight/worklife/employee-
  assistance-programs/'}

在接下来的部分,您将看到MenuModel API 将如何在各种菜单组件中使用。

静态和动态定位的菜单

菜单是一个支持动态和静态定位的导航或命令组件。这是所有菜单组件中的基本菜单组件。菜单默认是静态定位的,但通过提供target属性可以使其变为动态定位。静态定位的菜单附加到页面主体作为目标(即appendTo="body"),而分配给其他元素则创建动态定位的菜单。

一个基本的菜单示例,包含项目文档或文件类型的菜单项,如下所示:

<p-menu [model]="items"></p-menu>

菜单项列表需要在一个组件类中进行组织。例如,名为“编辑”的根菜单项将有如下嵌套项:

this.items=[
{
    label: 'Edit',
    icon: 'fa-edit',
    items: [
        {label: 'Undo', icon: 'fa-mail-forward'},
        {label: 'Redo', icon: 'fa-mail-reply'}
    ]
},
//More items ...
}

以下截图显示了基本菜单(包含所有菜单项)示例的快照结果:

从上面的快照中,您可以观察到菜单组件以内联格式显示。但是,通过启用popup属性可以改变此行为,以便以覆盖的形式显示。

菜单组件为Menu API 定义了toggleshowhide方法。每个方法的详细描述如下表所示:

名称 参数 描述
toggle event: 浏览器事件 切换弹出菜单的可见性。
show event: 浏览器事件 显示弹出菜单。
hide - 隐藏弹出菜单。

表 2.0 完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter7/menu.

通过 MenuBar 访问命令

MenuBar 组件是一组水平菜单组件,带有嵌套子菜单(或用于页面导航的下拉菜单组件)。与任何其他菜单组件一样,MenuBar 使用一个包含MenuItem接口列表的常见菜单模型 API。嵌套子菜单的级别没有限制。让我们看一个用于窗口或应用程序特定菜单的基本 MenuBar 示例。这提供了对常见功能的访问,例如打开文件,编辑操作,与应用程序交互,显示帮助文档等,如下所示:

<p-menubar [model]="items"></p-menubar>

菜单项列表需要在组件类中进行组织。例如,名为“编辑”的根菜单项将具有如下所示的嵌套项:

this.items = [
  {
    label: 'Edit',
    icon: 'fa-edit',
    items: [
      {label: 'Cut', icon: 'fa-cut'},
      {label: 'Copy', icon: 'fa-copy'},
      {label: 'Paste', icon: 'fa-paste'},
      {label: 'Undo', icon: 'fa-mail-forward'},
      {label: 'Redo', icon: 'fa-mail-reply'},
      {label: 'Find', icon: 'fa-search', items: [
        {label: 'Find Next'},
        {label: 'Find Previous'}
      ]}
    ]
  },
  // more items......
];

以下屏幕截图显示了基本 MenuBar(带有所有菜单项)示例的快照结果:

组件皮肤可以通过stylestyleClass属性实现。PrimeNG 4.1 允许通过将其放置在 MenuBar 标签内部来使用自定义内容(表单控件)。

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter7/menubar.

带有嵌套项的上下文菜单

ContextMenu 是一个具有图形用户界面 (GUI)表示的菜单,通过右键单击即可出现在页面顶部。通过右键单击,会在目标元素上显示一个覆盖菜单。有两种类型的上下文菜单,一种用于文档,另一种用于特定组件。除了这两种之外,还有与组件(如 DataTable)的特殊集成。

默认情况下,ContextMenu 附加到具有全局设置的文档。一个基本的上下文菜单示例,显示文档或文件类型菜单,如下所示:

<p-contextMenu [global]="true" [model]="documentItems"></p-contextMenu>

菜单项列表需要在组件类中进行组织。例如,名为“文件”的根菜单项将具有如下所示的嵌套项:

this.documentItems = [
  {
    label: 'File',
    icon: 'fa-file-o',
    items: [{
      label: 'New',
      icon: 'fa-plus',
      items: [
        {label: 'Project'},
        {label: 'Other'},
      ],
      expanded: true
  },
    {label: 'Open'},
    {label: 'Quit'}
    ],
  },
  // more items ...
];

以下屏幕截图显示了基本上下文菜单(带有所有菜单项)示例的快照结果:

一旦单击此组件之外,上下文菜单将消失。

ContextMenu 的自定义目标

可以使用target属性更改上下文菜单的默认全局设置(即,上下文菜单将显示在全局文档目标以外的其他元素上)。让我们来看一个上下文菜单示例,在右键单击图像元素时,覆盖或弹出窗口会出现在上面,如下所示:

<p-contextMenu [target]="image" [model]="targetItems" >
</p-contextMenu>
<img #image src="/assets/data/images/primeng.png" alt="Logo">

在这种情况下,只需定义菜单项数组,就可以从上下文菜单执行下一个和上一个操作。

DataTable 集成

在前一节中,您已经看到如何使用target属性将上下文菜单与其他元素集成。但是与 DataTable 组件的集成是一个不同的情况,需要特殊处理。这种组合是 Web 开发中经常使用的用例之一。

DataTable 使用contextMenu属性提供对上下文菜单的引用(即,上下文菜单的模板引用变量应分配给 DataTable 的contextMenu属性)。上下文菜单与 DataTable 的集成将如下所示编写:

<p-contextMenu #contextmenu [model]="tableItems"></p-contextMenu>
<p-dataTable [value]="employees" selectionMode="single" [(selection)]="selectedEmployee" [contextMenu]="contextmenu">
 <p-header>Employee Information</p-header>
  <p-column field="id" header="Employee ID"></p-column>
  <p-column field="name" header="Name"></p-column>
  <p-column field="email" header="Email"></p-column>
  <p-column field="contact" header="Telephone"></p-column>
</p-dataTable>

上下文菜单模型绑定到菜单项数组,例如ViewDelete选项,如下所示:

this.tableItems = [
 {label: 'View', icon: 'fa-search', command: (event) => 
   this.viewEmployee(this.selectedEmployee)},
 {label: 'Delete', icon: 'fa-close', command: (event) => 
   this.deleteEmployee(this.selectedEmployee)},
 {label: 'Help', icon: 'fa-close',
 url: 'https://www.opm.gov/policy-data-oversight/worklife/
   employee-assistance-programs/'}
];

在上面的例子中,我们执行了通知用户消息的命令操作。但在实时中,所有 CRUD 操作都与数据库同步。以下截图显示了上下文菜单与 DataTable 组件集成的快照结果。

根据上面的快照,在右键单击并在行上出现覆盖时,表格行被选中。菜单项选择可以执行业务逻辑或导航到各种网页。

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter7/contextmenu

SlideMenu - iPod 样式菜单

SlideMenu 是一个显示带有滑动动画效果的子菜单的组件。这种滑动菜单组件是 iPod 样式菜单小部件的最佳示例。默认情况下,滑动菜单显示为内联菜单组件。显示文档或文件类型菜单的基本滑动菜单示例如下:

<p-slideMenu [model]="items"></p-slideMenu>

菜单项列表需要在组件类中进行组织。例如,名为“文件”的根菜单项将具有如下嵌套项:

this.items = [
  {
    label: 'File',
    icon: 'fa-file-o',
    items: [
    {
      label: 'New',
      icon: 'fa-plus',
      items: [
        {label: 'Project'},
        {label: 'Other'},
      ]
    },
    {label: 'Open'},
    {label: 'Quit'}
    ]
  },
  // more items ...
]

以下截图显示了基本幻灯片菜单的快照结果,例如,单击文件菜单项时显示文件菜单项:

如前面的快照所示,幻灯片菜单以内联格式显示。通过启用popup属性,可以以弹出模式显示。在幻灯片菜单弹出窗口底部,会出现一个带有“返回”标签的返回按钮,但也可以使用backLabel属性进行自定义。

可以使用toggleshowhide等 API 方法访问幻灯片菜单。幻灯片菜单提供各种动画效果,默认效果为easing-out。可以使用effect属性更改此默认行为。同样,幻灯片菜单的默认效果持续时间为 500 毫秒,但可以使用effectDuration属性进行自定义。

任何可视组件的尺寸都是非常重要的,必须进行配置。考虑到这一标准,菜单尺寸是可配置的。子菜单宽度通过menuWidth属性控制,默认为 180(通常以像素为单位)。同时,可滚动区域的高度通过viewportHeight属性控制,默认值为 175 像素(即,如果菜单高度超过此默认值,则会出现滚动条)。

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter7/slidemenu.

分层菜单 - 嵌套叠加的子菜单

TieredMenu 组件以嵌套叠加模式显示子菜单。默认情况下,幻灯片菜单显示为内联菜单组件。一个基本的分层菜单示例,显示文档或文件类型菜单,如下所示:

<p-tieredMenu [model]="items"></p-tieredMenu>

菜单项列表需要在组件类中进行组织。例如,名为“文件”的根菜单项将具有如下嵌套项:

this.items = [
 {
   label: 'File',
   icon: 'fa-file-o',
   items: [
 {
   label: 'New',
   icon: 'fa-plus',
   items: [
   {label: 'Project'},
   {label: 'Other'},
 ]
 },
   {label: 'Open'},
   {label: 'Quit'}
 },
 // more items
]

以下截图显示了基本分层菜单示例的快照结果:

如前面的快照所示,滑动菜单以内联格式显示。通过启用popup属性,它将以弹出模式显示。PrimeNG 4.1 引入了appendTo属性以附加覆盖。可以使用 API 方法(如toggleshowhide)访问滑动菜单。

滑动菜单和分层菜单组件之间的主要区别在于,滑动菜单通过替换父菜单显示子菜单,而分层菜单以覆盖模式显示子菜单。有关滑动菜单和分层菜单的 API 方法以及更详细的表格格式,请参阅菜单部分表 2.0

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter7/tieredmenu

MegaMenu - 多列菜单

MegaMenu 类似于一个下拉菜单,它展开成一个相对较大和复杂的界面,而不是一个简单的命令列表。它一起显示根项目的子菜单。MegaMenu 由嵌套菜单项组成,其中每个项目的根项目是定义覆盖菜单中列的二维数组。

一个零售商应用程序的基本 MegaMenu 示例,用于购买服装项目,将如下所示:

<p-megaMenu [model]="items"></p-megaMenu>

菜单项列表需要在组件类中进行组织。例如,名为“家居与家具”的根菜单项将具有如下嵌套项:

this.items = [
  {
    label: 'HOME & FURNITURE', icon: 'fa-home',
    items: [
    [
      {
        label: 'Home Furnishing',
        items: [{label: 'Cushions'}, {label: 'Throws'}, 
        {label: 'Rugs & Doormats'},
               {label: 'Curtains'}]
      },
     {
       label: 'Home Accessories',
       items: [{label: 'Artificial Flowers'}, {label: 'Lighting'}, 
               {label: 'Storage'}, {label: 'Photo Frames'}]
     }
   ],
   [
     {
       label: 'Cooking & Dinner',
       items: [{label: 'Cookware'}, {label: 'Dinnerware'}, 
       {label: 'Bakerware'}]
     },
     {
       label: 'Bed & Bath',
       items: [{label: 'Towels'}, {label: 'Bath Mats'}]
     }
   ]
   ]
  },
  // more items...
];

以下截图显示了基本 MegaMenu(带有所有菜单项)示例的快照结果:

MegaMenu 的默认方向是水平的。也可以使用orientation属性(即orientation="vertical")以垂直方式定位。垂直 MegaMenu 如下快照所示:

PrimeNG 4.1 允许通过将它们放置在 MegaMenu 标签内来使用自定义内容(表单控件)。

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter7/megamenu

PanelMenu - 手风琴和树的混合体

PanelMenu 是垂直堆叠的手风琴和分层树组件的混合体。每个父菜单项都有一个可切换的面板;面板显示子菜单项以分层树格式显示。一个基本的面板菜单示例,显示文档或文件类型菜单,如下所示:

<p-panelMenu [model]="items" ></p-panelMenu>

菜单项列表需要在组件类中组织。例如,名为帮助的根菜单项将具有如下所示的嵌套项:

this.items = [
  {
 label: 'Help',
    icon: 'fa-question',
    items: [
           {label: 'Contents'},
           {label: 'Search', icon: 'fa-search',
             items: [{label: 'Text', items: [{label: 'Workspace'}]}, 
             {label: 'File'}]}
    ]
  },
  //more items ...
];

以下屏幕截图显示了基本面板菜单示例的快照结果:

每个菜单项的初始状态通过expanded属性(即expanded="true")进行控制,该属性在MenuItem接口级别上可用。

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter7/panelmenu

TabMenu - 菜单项作为选项卡

TabMenu 是一个导航/命令组件,它将项目显示为选项卡标题(即,父根项目以水平堆叠的选项卡形式表示)。单击每个选项卡时,可以执行各种菜单操作。

一个基本的选项卡菜单示例,以各种选项卡的形式显示 PrimeNG 网站信息,如下所示:

<p-tabMenu [model]="items"></p-tabMenu>

菜单项列表需要在组件类中组织。例如,使用菜单项如下,解释了 PrimeNG 的各种详细信息:

this.items = [
  {label: 'Overview', icon: 'fa-bar-chart', routerLink: 
  ['/pages/overview']},
  {label: 'Showcase', icon: 'fa-calendar', command: (event) => {
    this.msgs.length = 0;
    this.msgs.push({severity: 'info', summary: 'PrimeNG Showcase', 
    detail:'Navigate all components'});}
  },
  {label: 'Documentation', icon: 'fa-book', 
    url:'https://www.primefaces.org/documentation/'},
  {label: 'Downloads', icon: 'fa-download', routerLink: 
    ['/pages/downloads']},
  {label: 'Support', icon: 'fa-support', 
    url:'https://www.primefaces.org/support/'},
  {label: 'Social', icon: 'fa-twitter', 
    url:'https://twitter.com/prime_ng'},
  {label: 'License', icon: 'fa-twitter', 
    url:'https://www.primefaces.org/license/'}
];

以下屏幕截图显示了选项卡面板菜单示例的快照结果:

默认情况下,TabMenu 显示或激活第一个选项卡。但是,可以通过activeItem属性来更改选项卡的默认可见性或初始显示。

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter7/tabmenu

面包屑 - 提供有关页面层次结构的上下文信息

面包屑组件提供有关页面层次结构的上下文信息。它允许您跟踪程序、文档和网站中的位置。这通常显示为水平的,位于网页顶部,由大于号(>)作为层次分隔符。这种菜单变体由一个常见的菜单模型 API 来定义其项目。这些菜单项(菜单项的集合)与model属性相连。

一个电子商务应用程序的基本面包屑示例,用于购买电器,如下所示:

<p-breadcrumb [model]="items"></p-breadcrumb>

项目的model属性是MenuItem类型的数组。MenuModel API 的可能选项或属性在本节的开头进行了描述。在这个例子中,我们为菜单项定义了标签和命令操作。菜单项的列表需要组织起来,以显示如下所示的项目:

this.items.push({
  label: 'Categories', command: (event) => {
    this.msgs.length = 0;
    this.msgs.push({severity: 'info', summary: event.item.label});
  }
});
this.items.push({
  label: 'Best Buy', command: (event) => {
    this.msgs.length = 0;
    this.msgs.push({severity: 'info', summary: event.item.label});
  }
});
this.items.push({
  label: 'TV & Video', command: (event) => {
    this.msgs.length = 0;
    this.msgs.push({severity: 'info', summary: event.item.label});
  }
});
this.items.push({
  label: 'TVs', command: (event) => {
    this.msgs.length = 0;
    this.msgs.push({severity: 'info', summary: event.item.label});
  }
});
this.items.push({
  label: 'Flat Panel TVs', command: (event) => {
    this.msgs.length = 0;
    this.msgs.push({severity: 'info', summary: event.item.label});
  }
});
this.items.push({label: 'LED Flat-Panel', url: 'https://en.wikipedia.org/wiki/LED_display'});

以下屏幕截图显示了基本面包屑的快照结果:

主页图标也是菜单项的一部分,可以使用MenuItem类型的home属性进行自定义。因此,所有菜单项的功能也适用于主页菜单项。home属性必须为面包屑组件定义如下:

<p-breadcrumb [model]="items" [home]="home"></p-breadcrumb>

组件类如下所示包含主页菜单项:

home: MenuItem;
 this.home = {
 label: 'Home',icon: 'fa-globe', command: (event) => {
    this.msgs.length = 0;
    this.msgs.push({severity: 'info', summary: "Home"});
  }
};

这是一个支持自定义图标属性的组件,可以从MenuItem中定义。

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter7/breadcrumb.

摘要

在本章结束时,您现在知道如何处理各种菜单组件,以及如何将它们放在页面上以满足特定的用例。首先,我们从 MenuModel API 开始创建一个项目数组,然后介绍了菜单组件作为基本组件,然后将 MenuBar 移动到具有嵌套复杂子菜单的 MegaMenu 组件,然后是其他菜单变体,如滑动菜单、分层菜单和面板菜单。

稍后,我们将转向上下文菜单和面包屑组件,作为另一种菜单操作。在下一章中,您将看到一个图表模型作为 API,以及如何为数据的可视表示创建出色的图表和地图。所有这些组件都是通过逐步方法解释的,包括所有可能的功能。

第八章:创建图表和地图

在本章中,我们将介绍如何使用 PrimeNG 的丰富图表功能和基于谷歌地图的地图来创建可视化图表的方法。PrimeNG 提供了基本和高级的图表功能,其易于使用和用户友好的图表基础设施。除了标准图表外,还有一种特殊的图表用于可视化分层组织数据。在本章中,还将解释绘制折线、多边形、处理标记和事件等地图功能。

在本章中,我们将涵盖以下主题:

  • 使用图表模型

  • 用线图和条形图表示数据

  • 用饼图和圆环图表示数据

  • 用雷达和极地区域图表表示数据

  • 绘制关系层次的组织图表

  • 与谷歌地图 API 的基本集成

  • GMap 组件的各种用例

使用图表模型

图表组件通过在网页上使用图表来对数据进行可视化表示。PrimeNG 图表组件基于Charts.js 2.x库(作为依赖项),这是一个 HTML5 开源库。图表模型基于UIChart类名,并且可以用元素名p-chart表示。

通过将图表模型文件(chart.js)附加到项目中,图表组件将有效地工作。它可以配置为 CDN 资源、本地资源或 CLI 配置:

  • CDN 资源配置
 <script src="https://cdnjs.cloudflare.com/ajax/libs/
        Chart.js/2.5.0/Chart.bundle.min.js"></script>

  • Angular CLI 配置
 "scripts":  [  "../node_modules/chart.js/dist/
        Chart.js",  //..others  ]

有关图表配置和选项的更多信息,请参阅 Chart.js 库的官方文档(www.chartjs.org/)。

图表类型

图表类型通过type属性定义。它支持七种不同类型的图表,并提供自定义选项:

  • 饼图

  • 条形图

  • 圆环图

  • 极地区域图

  • 雷达图

  • 水平条形图

每种类型都有自己的数据格式,可以通过data属性提供。例如,在圆环图中,类型应该是doughnutdata属性应该绑定到数据选项,如下所示:

<p-chart type="doughnut" [data]="doughnutdata"></p-chart>

组件类必须使用labelsdatasets选项定义数据,如下所示:

this.doughnutdata = {
  labels: ['PrimeNG', 'PrimeUI', 'PrimeReact'],
  datasets: [
    {
      data: [3000, 1000, 2000],
      backgroundColor: [
        "#6544a9",
        "#51cc00",
        "#5d4361"
  ],
      hoverBackgroundColor: [
        "#6544a9",
        "#51cc00",
        "#5d4361"
  ]
    }
  ]
};

除了标签和数据选项之外,还可以应用与皮肤相关的其他属性。

图例默认是可关闭的(也就是说,如果您只想可视化特定的数据变体,那么可以通过折叠不需要的图例来实现)。折叠的图例用一条删除线表示。在图例上点击操作后,相应的数据组件将消失。

自定义

每个系列都是基于数据集进行自定义的,但您可以通过options属性来自定义通用的选项。例如,自定义默认选项的折线图将如下所示:

<p-chart type="line" [data]="linedata" [options]="options">
</p-chart>

该组件需要使用自定义的titlelegend属性来定义图表选项,如下所示:

this.options = {
 title: {
    display: true,
    text: 'PrimeNG vs PrimeUI',
    fontSize: 16
  },
  legend: {
    position: 'bottom'
  }  };

根据上面的示例,title选项使用动态标题、字体大小和条件显示标题进行自定义,而legend属性用于将图例放置在topleftbottomright位置。默认的图例位置是top。在这个例子中,图例位置是bottom

具有上述自定义选项的折线图将产生以下快照:

Chart API 还支持这里显示的实用方法:

方法 描述
refresh 用新数据重新绘制图表
reinit 销毁现有图表,然后重新创建
generateLegend 返回该图表图例的 HTML 字符串

事件

图表组件提供了对数据集的点击事件,以使用onDataSelect事件回调处理所选数据。

让我们通过传递event对象来使用onDataSelect事件回调来看一个折线图的例子:

<p-chart type="line" [data]="linedata" 
  (onDataSelect)="selectData($event)"></p-chart>

在组件类中,事件回调用于以以下消息格式显示所选数据信息:

selectData(event: any) {
  this.msgs = [];
  this.msgs.push({
    severity: 'info',
    summary: 'Data Selected',
    'detail': this.linedata.datasets[event.element._datasetIndex]
    .data[event.element._index]
  });
}

在上述事件回调(onDataSelect)中,我们使用数据集的索引来显示信息。还有许多其他来自event对象的选项:

  • event.element:选定的元素

  • event.dataset:选定的数据集

  • event.element._datasetIndex:图表数据系列的索引

  • event.element._index:图表系列内数据元素的索引

使用折线图和条形图进行数据表示

折线图是一种以一系列数据点(称为标记)通过直线段连接来显示信息的图表类型。折线图通常用于可视化定期时间间隔或时间序列中的实时数据。

关于 Prime 库下载量的线图使用的基本示例如下:

<p-chart type="line" [data]="linedata" width="300" height="100">
</p-chart>

组件类应该定义一条线图数据,其中一条是指 PrimeNG 系列,另一条是指过去一年的 PrimeUI 系列,如下所示:

this.linedata = {
 labels: ['January', 'February', 'March', 'April', 'May', 
 'June', 'July', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
    datasets: [
    {
      label: 'PrimeNG',
      backgroundColor: '#ffb870',
      borderColor: '#cc4e0e',
      data: [13, 22, 15, 38, 41, 42, 25, 53, 53, 63, 77, 93]
     },
     {
      label: 'PrimeUI',
      backgroundColor: '#66ff00',
      borderColor: '#6544a9',
      data: [15, 11, 18, 28, 32, 32, 42, 52, 48, 62, 77, 84]
     }
  ]
};

根据上述代码片段,除了数据和标签之外,我们还可以定义背景和边框颜色,使线图变得像我们喜欢的那样花哨和可定制。以下截图显示了线图的快照结果:

条形图或柱状图是一种呈现分组数据的图表,其中用矩形条表示的值是成比例的。PrimeNG 还支持图表中条的水平表示。

关于 Prime 库下载量的条形图使用的基本示例如下:

<p-chart type="bar" [data]="bardata" width="300" height="100">
</p-chart>

组件类应该定义条形图数据,其中一条是指 PrimeNG 数据,另一条是指过去一年的 PrimeUI 系列,如下所示:

this.bardata = {
  labels: ['January', 'February', 'March', 'April', 'May', 
 'June', 'July', 'Aug', 'Sep',
 'Oct', 'Nov', 'Dec'],
    datasets: [
    {
      label: 'PrimeNG',
      backgroundColor: '#66ff00',
      borderColor: '#6544a9',
      data: [10, 15, 13, 27, 22, 34, 44, 48, 42, 64, 77, 89]
    },
    {
      label: 'PrimeUI',
      backgroundColor: '#ffb870',
      borderColor: '#cc4e0e',
      data: [5, 14, 15, 22, 26, 24, 32, 42, 48, 62, 66, 72]
    }
  ]
};

以下截图显示了一年时间内 PrimeNG 和 PrimeUI 下载量的条形图的快照结果:

在上面的图表中,只有两个数据集在一个固定的时间段内进行比较。这也可以应用于多个数据集。

使用饼图和圆环图表示数据

饼图(或圆环图)是一种将圆形分成片段以说明复合数据的数值比例的圆形统计图。每个片段的弧长等于数据实体的数量。关于 Prime 库下载量的饼图使用的基本示例如下:

<p-chart #pie type="pie" [data]="piedata" width="300" height="100">
</p-chart>

组件类应该定义饼图数据,其中有三个片段分别代表了三个 Prime 库在一段时间内的情况,如下所示:

this.piedata = {
  labels: ['PrimeNG', 'PrimeUI', 'PrimeReact'],
  datasets: [
    {
      data: [3000, 1000, 2000],
      backgroundColor: [
        "#6544a9",
        "#51cc00",
        "#5d4361"
  ],
      hoverBackgroundColor: [
        "#6544a9",
        "#51cc00",
        "#5d4361"
  ]
    }
  ]
};

以下截图显示了一年时间内 PrimeNG、PrimeUI 和 PrimeReact 下载量的饼图的快照结果:

通过悬停在每个饼图的片段上,您可以观察到相应的数据标签及其值。

甜甜圈图是饼图的一种变体,中间有一个空心中心,可以提供有关完整数据的附加信息(即,每个切片代表特定的唯一数据,中心圆表示所有切片的一般附加信息)。

Prime 库下载的甜甜圈图使用的基本示例如下:

<p-chart type="doughnut" [data]="doughnutdata" width="300" 
  height="100">
</p-chart>

组件类应该定义饼图数据,其中包括三个切片,用于一段时间内的三个 Prime 库,如下所示:

this.doughnutdata = {
 labels: ['PrimeNG', 'PrimeUI', 'PrimeReact'],
  datasets: [
    {
      data: [3000, 1000, 2000],
      backgroundColor: [
        "#6544a9",
        "#51cc00",
        "#5d4361"
  ],
      hoverBackgroundColor: [
        "#6544a9",
        "#51cc00",
        "#5d4361"
  ]
    }
  ]
};

以下是一个示例,显示了在一年的时间内使用 PrimeNG、PrimeUI 和 PrimeReact 下载的甜甜圈图的快照结果:

默认情况下,甜甜圈图的切除百分比为50(而饼图为0)。这可以通过cutoutPercentage图表选项进行自定义。

雷达和极地区域图的数据表示

雷达图是以二维图表的形式显示多变量数据的图形表示。它至少有三个或更多的定量变量,这些变量表示在从同一点开始的轴上。这种图表也被称为蜘蛛图星形图。它在衡量任何正在进行的程序的绩效指标方面非常有用,以控制改进的质量。

PrimeNG 和 PrimeReact 项目进展的雷达图使用的基本示例如下:

<p-chart type="radar" [data]="radardata" width="300" height="100">
</p-chart>

组件类应该定义雷达图数据,其中包括两个数据集(PrimeNG 和 PrimeReact),用于 SDLC 过程的六个阶段,如下所示:

this.radardata = {
  labels: ['Requirement', 'Design', 'Implementation', 'Testing', 
 'Deployment', 'Maintainance'],
  datasets: [
    {
      label: 'PrimeNG',
      backgroundColor: 'rgba(162,141,158,0.4)',
      borderColor: 'rgba(145,171,188,1)',
      pointBackgroundColor: 'rgba(145,171,188,1)',
      pointBorderColor: '#fff',
      pointHoverBackgroundColor: '#fff',
      pointHoverBorderColor: 'rgba(145,171,188,1)',
      data: [76, 55, 66, 78, 93, 74]
    },
    {
      label: 'PrimeReact',
      backgroundColor: 'rgba(255,99,132,0.2)',
      borderColor: 'rgba(255,99,132,1)',
      pointBackgroundColor: 'rgba(255,99,132,1)',
      pointBorderColor: '#fff',
      pointHoverBackgroundColor: '#fff',
      pointHoverBorderColor: 'rgba(255,99,132,1)',
      data: [30, 43, 38, 17, 89, 33]
    }
  ]
};

在上面的示例中,数据集不仅指的是数据组件,还为图表提供了背景、边框颜色等皮肤。以下截图显示了雷达图的快照结果,其中包括 PrimeNG 和 PrimeReact 项目在 SDLC 生命周期过程的六个阶段的进展:

极地区域图类似于饼图,但每个部分的角度相同(即,部分的半径根据值的不同而不同)。当我们想要显示与饼图类似的比较数据时,这种类型的图表通常很有用。但是,您也可以为给定上下文显示一组值的比例。

Prime 产品库下载的极坐标图使用的基本示例如下:

<p-chart type="polarArea" [data]="polardata" width="300" height="100">
</p-chart>

组件类应该定义各种 Prime 库的极地图下载数据,如下所示:

this.polardata = {
  datasets: [{
    data: [45, 35, 10, 15, 5],
    backgroundColor: ["#6544a9", "#51cc00", "#5d4361", "#E7E9ED", 
 "#36A2EB"],
    label: 'Prime Libraries'
  }],
  labels: ["PrimeFaces", "PrimeNG", "PrimeReact", "PrimeUI", 
 "PrimeMobile"]
}

组件类创建了数据选项以及外观属性。以下屏幕截图显示了使用 PrimeFaces、PrimeNG、PrimeUI、PrimeReact 和 PrimeMobile 下载的极地图的快照结果,作为一年时间段的示例:

根据数据集,提供了minmax值,并且极地图数据段值将被调整(1、2、3、4、50)。

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter8/charts

绘制关系层次结构的组织图

组织图是一种可视化分层组织数据的图表。PrimeNG 提供了一个名为OrganizationChart的组件,用于显示这种自上而下的关系层次结构。该组件需要TreeNode实例的模型作为其值。TreeNode API 在第五章中的数据迭代组件使用树形可视化数据部分进行了解释。在本节中,我们将介绍OrganizationChart组件的详细信息,并开发一个图表,用于说明组织中的一个项目。

零配置的分层数据

绘制简单图表很容易--只需要value属性:

<p-organizationChart [value]="data"></p-organizationChart>

在组件类中,我们需要创建一个嵌套的TreeNode实例数组。在简单的用例中,提供标签就足够了:

data: TreeNode[];

ngOnInit() {
  this.data = [
    {
      label: 'CEO',
      expanded: true,
      children: [
        {
          label: 'Finance',
          expanded: true,
          children: [
            {label: 'Chief Accountant'},
            {label: 'Junior Accountant'}
          ]
        },
        {label: 'Marketing'},
        {
          label: 'Project Manager',
          expanded: true,
          children: [
            {label: 'Architect'},
            {label: 'Frontend Developer'},
            {label: 'Backend Developer'}
          ]
        }
      ]
    }
  ];
}

默认情况下,具有子节点(叶子)的树节点不会展开。要将树节点显示为展开状态,可以在模型中设置expanded: true。用户可以通过单击节点连接点处的小箭头图标来展开和折叠节点。

简单用例如下图所示:

高级自定义

通过使用ng-template标签进行模板化,可以启用自定义。TreeNode具有type属性,用于匹配pTemplate属性的值。这种匹配允许您为每个单个节点自定义 UI。没有type属性的节点匹配pTemplate="default"

下一个代码片段有两个ng-template标签。第一个匹配具有type属性department的节点。第二个匹配没有类型的节点。当前节点对象通过微语法let-node公开:

<p-organizationChart  [value]="data" styleClass="company">
 <ng-template  let-node pTemplate="department">
    <div  class="node-header ui-corner-top">
      {{node.label}}
    </div>
    <div  class="node-content ui-corner-bottom">
      <img  src="/assets/data/avatar/{{node.data.avatar}}" width="32">
      <div>{{node.data.name}}</div>
    </div>
  </ng-template>
  <ng-template  let-node pTemplate="default">
    {{node.label}}
  </ng-template>
</p-organizationChart>

我们只会展示data数组的一部分来传达这个想法。

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter8/orgchart

this.data = [
  {
    label: 'CEO',
    expanded: true,
    type: 'department',
    styleClass: 'org-dept',
    data: {id: '1', name: 'Alex Konradi', avatar: 'man.png'},
    children: [
      {
        label: 'Finance',
        expanded: true,
        type: 'department',
        styleClass: 'org-dept',
        data: {id: '2', name: 'Sara Schmidt', avatar: 'women.png'},
        children: [
          {
            label: 'Chief Accountant',
            styleClass: 'org-role'
  },
          {
            label: 'Junior Accountant',
            styleClass: 'org-role'
  }
        ]
      },
      ...
    ]
  }
];

自定义的组织图如下所示:

我们指定了自定义样式类来设置节点和切换器的颜色。例如:

.org-role {
 background-color: #00b60d;
 color: #ffffff;
}

.org-dept .ui-node-toggler {
 color: #bb0066 !important;
}

完整的样式设置可在 GitHub 上找到。

选择和事件

选择是通过将selectionMode设置为可能的值之一来启用的:singlemultiple。在single模式下,预期selection属性的值是单个TreeNode。在multiple模式下,预期是一个数组。例如:

<p-organizationChart [value]="data"
  selectionMode="single" [(selection)]="selectedNode">
</p-organizationChart>

组织图支持两个事件:

名称 参数 描述
onNodeSelect
  • event.originalEvent:浏览器事件

  • event.node:选定的节点实例

当通过单击选择节点时调用的回调。
onNodeUnselect
  • event.originalEvent:浏览器事件

  • event.node:取消选择的节点实例

当通过单击取消选择节点时调用的回调。

让我们扩展如下所示的先前开发的组织图:

<p-organizationChart  [value]="data" styleClass="company"
 selectionMode="single" [(selection)]="selectedNode"
  (onNodeSelect)="onNodeSelect($event)">
  ...
</p-organizationChart>

在 GitHub 上的演示应用程序中,我们定义了一个代表个人 VCard 的VCard接口:

export interface VCard {
 id: string;
  fullName: string;
  birthday: string;
  address: string;
  email: string;
}

所有 VCard 实例都是在onNodeSelect回调中延迟获取的。之后,VCard 会显示在 PrimeNG 对话框中:

display: boolean = false; selectedVCard: VCard;
private availableVCards: VCard[];

onNodeSelect(event: any) {
 if (this.availableVCards == null) {
    this.vcardService.getVCards().subscribe(
      (vcards: VCard[]) => {
        this.availableVCards = vcards;
        this.showInfo(event);
      });
  } else {
    this.showInfo(event);
  }
}

private showInfo(event: any) {
 this.selectedVCard = null;

  this.availableVCards.some((element: VCard) => {
    if (event.node.data && element.id === event.node.data.id) {
      this.selectedVCard = element;
      return true;
    }
  });

  if (this.selectedVCard) {
    // show VCard in dialog
  this.display = true;
  } else {
    // show node label in growl
  this.msgs = [];
    this.msgs.push({severity: 'Label', summary: event.node.label});
  }
}

对话框本身如下所示:

<p-dialog  header="VCard" [(visible)]="display"
 modal="modal" width="320" [responsive]="true">
 <i  class="fa fa-address-card-o"></i>
  <ul  style="padding: 0.2em 0.8em;">
    <li>Full name: {{selectedVCard?.fullName}}</li>
    <li>Birthday: {{selectedVCard?.birthday}}</li>
    <li>Address: {{selectedVCard?.address}}</li>
    <li>E-mail: {{selectedVCard?.email}}</li>
 </ul>
</p-dialog>

结果真是令人惊叹:

与 Google Maps API 的基本集成

GMap 组件提供了与 Google Maps API 的集成,以便以更少的配置高效地使用它。它涵盖了诸如绑定选项、各种覆盖物、事件等主要功能。该组件需要 Google Maps API,因此需要在script部分中引用它。

JS 资源文件需要在脚本部分中添加,这需要由 GMap 组件利用,如下所示:

<script type="text/javascript"   
  src="https://maps.google.com/maps/api/js?
  key=AIzaSyA6Ar0UymhiklJBzEPLKKn2QHwbjdz3XV0"></script>

使用地图选项的 GMap 的基本示例如下:

<p-gmap [options]="options" [styleClass]="'dimensions'">
</p-gmap>

在页面加载期间,必须使用坐标/位置尺寸(纬度经度)、缩放选项等来定义选项,如下所示:

this.options = {
 center: {lat: 14.4426, lng: 79.9865},
  zoom: 12 };

以下屏幕截图显示了 GMap 示例的快照结果:

GMap 示例的快照结果

根据前面的快照,根据提供的坐标和缩放设置的可见性模式,显示确切的区域位置。

GMap 组件的各种用例

除了基本的 Google 地图用法之外,GMap 还可以用于各种用例。使用不同类型的覆盖物、地图上的事件、覆盖物等,地图将更加交互。

覆盖物

覆盖物是地图上绑定到纬度/经度坐标或尺寸的对象。覆盖实例数组通过overlays属性进行绑定。由于单向绑定的性质,当数组发生变化时,地图将自动更新。

GMap 支持各种类型的覆盖物,如下所示:

  • 标记:地图上的单个位置。标记还可以显示自定义图标图像。

  • 折线:地图上的一系列直线。

  • 多边形:地图上的一系列直线,但形状是“闭合的”。

  • 圆形和矩形:表示特定区域的圆形/矩形。

  • 信息窗口:在地图顶部的气球中显示内容。

使用覆盖选项的 GMap 示例用法将被编写如下:

<p-gmap [options]="options" [overlays]="overlays"  
  [styleClass]="'dimensions'"></p-gmap>

让我们定义一个覆盖实例数组,例如标记、折线、多边形、圆形等,如下所示:

this.overlays = [
 new google.maps.Marker({position: {lat: 14.6188043, 
 lng: 79.9630253}, title:"Talamanchi"}),
  new google.maps.Marker({position: {lat: 14.4290442, 
 ng: 79.9456852}, title:"Nellore"}),
  new google.maps.Polygon({paths: [
    {lat: 14.1413809, lng: 79.8254154}, {lat: 11.1513809, 
 lng: 78.8354154},
    {lat: 15.1313809, lng: 78.8254154},{lat: 15.1613809, 
 lng: 79.8854154}
    ], strokeOpacity: 0.5, strokeWeight: 1,
 fillColor: '#1976D2', fillOpacity: 0.35
  }),
  new google.maps.Circle({center: {lat: 14.1413809, lng: 79.9513809},  
 fillColor: '#197642', fillOpacity: 0.25, strokeWeight: 1, 
 radius: 25000}), new google.maps.Polyline({path: [{lat: 14.1413809,  
 lng: 79.9254154}, {lat: 14.6413809, lng: 79.9254154}], 
 geodesic: true, strokeColor: '#F0F000', strokeOpacity: 0.5,  
 strokeWeight: 2})
];

以下屏幕截图显示了 GMap 的快照结果,其中包含各种覆盖物作为示例:

在上述地图中,您可以观察到基于提供的坐标以及其他覆盖物特定配置的标记、多边形和圆形。

事件

GMap 在地图上的交互事件中更加强大。有许多可用的回调函数可以钩入事件,例如单击地图、覆盖物单击和拖动覆盖物。

具有各种类型的覆盖物事件以及事件回调的地图组件示例将被编写如下:

<p-gmap #gmap [options]="options" [overlays]="overlaysEvents"
 (onMapReady)="handleMapReady($event)"  
  (onMapClick)="handleMapClick($event)"(onOverlayClick)="handleOverlayClick($event)" 
  (onOverlayDragStart)="handleDragStart($event)"
  (onOverlayDragEnd)="handleDragEnd($event)" 
  [styleClass]="'dimensions'"> 
</p-gmap>

可以通过单击覆盖物来更新现有事件,也可以通过单击地图并使用对话框组件来创建新事件,如下所示:

<p-dialog showEffect="fade" [(visible)]="dialogVisible" 
  header="New Location">
 <div class="ui-grid ui-grid-pad ui-fluid" *ngIf="selectedPosition">
    <div class="ui-grid-row">
      <div class="ui-grid-col-2"><label for="title">Label</label></div>
      <div class="ui-grid-col-10"><input type="text" 
        pInputText id="title"
        [(ngModel)]="markerTitle"></div> . 
      </div>
      <div class="ui-grid-row">
        <div class="ui-grid-col-2"><label for="lat">Lat</label></div>
        <div class="ui-grid-col-10"><input id="lat" 
          type="text" readonly pInputText
          [ngModel]="selectedPosition.lat()"></div> 
        </div>
        <div class="ui-grid-row">
          <div class="ui-grid-col-2"><label for="lng">Lng</label></div>
          <div class="ui-grid-col-10"><input id="lng" 
            type="text" readonly pInputText
            [ngModel]="selectedPosition.lng()"></div> 
        </div>
        <div class="ui-grid-row">
          <div class="ui-grid-col-2"><label for="drg">Drag</label> 
          </div>
          <div class="ui-grid-col-10">
            <p-checkbox [(ngModel)]="draggable" binary="true">
            </p-checkbox></div>     
        </div>
     </div>
    <p-footer>
      <div class="ui-dialog-buttonpane ui-widget-content 
        ui-helper-clearfix">
        <button type="button" pButton label="Add Marker" 
          icon="fa-plus" (click)="addMarker()">
        </button>
      </div>
    </p-footer>
</p-dialog>

组件类必须在初始页面加载时定义各种覆盖类型,如下所示:

if (!this.overlaysEvents || !this.overlaysEvents.length) {
 this.overlaysEvents = [
    new google.maps.Marker({position: {lat: 14.6188043, 
 lng: 79.9630253}, title:'Talamanchi'}),  
    new google.maps.Marker({position: {lat: 14.4290442, 
 lng: 79.9456852}, title:'Nellore'}),
    new google.maps.Polygon({paths: [
      {lat: 14.1413809, lng: 79.8254154}, 
      {lat: 11.1513809, lng: 78.8354154},
      {lat: 15.1313809, lng: 78.8254154}, 
      {lat: 15.1613809, lng: 79.8854154}], 
 strokeOpacity: 0.5, strokeWeight: 1, 
 fillColor: '#1976D2', fillOpacity: 0.35
  }),
    new google.maps.Circle({center: {lat: 14.1413809, 
 lng: 79.9513809}, fillColor: '#197642', 
 fillOpacity: 0.25, strokeWeight: 1, radius: 25000}),
    new google.maps.Polyline({path: [{lat: 14.1413809, 
 lng: 79.9254154}, {lat: 14.6413809, lng: 79.9254154}], 
 geodesic: true, strokeColor: '#F0F000',
      strokeOpacity: 0.5, strokeWeight: 2})];
}

以下快照显示了如何创建或更新叠加层事件:

地图组件支持以下列出的事件回调:

名称 参数 描述
onMapClick event: Google 地图鼠标事件 当地图被点击时,除了标记。

| onOverlayClick | originalEvent: Google 地图鼠标事件 overlay: 点击的叠加层

地图: 地图实例 | 当叠加层被点击时。|

| onOverlayDragStart | originalEvent: Google 地图鼠标事件 overlay: 点击的叠加层

地图: 地图实例 | 当叠加层拖动开始时。|

| onOverlayDrag | originalEvent: Google 地图鼠标事件 overlay: 点击的叠加层

地图: 地图实例 | 当叠加层被拖动时。|

| onOverlayDragEnd | originalEvent: Google 地图鼠标事件 overlay: 点击的叠加层

地图: 地图实例 | 当叠加层拖动结束时。|

onMapReady event.map: Google 地图实例 当地图加载后地图准备就绪时。
onMapDragEnd originalEvent: Google 地图 dragend 当地图拖动(即平移)结束时调用的回调。
onZoomChanged originalEvent: Google 地图 zoom_changed 当缩放级别发生变化时调用的回调。

有两种访问地图 API 的方式。其中一种是 GMap 组件的 getMap() 函数 (gmap.getMap()),另一种是通过事件对象访问 (event.map)。一旦地图准备就绪,那么根据我们的需求就可以使用所有地图函数。例如,getZoom() 方法可用于从当前状态增加或减少。

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter8/gmap.

摘要

通过完成本章,您将能够使用 PrimeNG 图表和 GMap 组件可视化数据表示。最初,我们从图表组件开始。首先,我们从图表模型 API 开始,然后学习如何使用各种图表类型(如饼图、柱状图、折线图、圆环图、极坐标图和雷达图)以编程方式创建图表。我们已经看到,组织图表完美地适应了关系层次的可视化。

接下来,我们转向基于谷歌地图的 GMap 组件。GMap 组件提供了一个方便的 API,用于与谷歌地图 API 进行交互,包括绘制标记、多边形、圆形,注册事件等等。在下一章中,我们将看一些其他用例和需要遵循的最佳实践。

第九章:杂项用例和最佳实践

杂项用例和最佳实践介绍了 PrimeNG 库的更多有趣功能。您将了解文件上传、拖放功能、显示图像集合、实际 CRUD 实现、推迟页面加载、阻止页面片段、显示带有受保护路由的确认对话框等。尽管组件集合全面,用户有时对现有组件的功能有特殊要求,或者需要新的自定义组件。

本章的目的也是专门为了在 PrimeNG 基础架构之上方便组件开发的开始。我们将经历构建可重用组件和开发自定义向导组件的完整过程。该向导可用于涉及多个步骤完成任务的工作流。此外,在阅读本章后,读者将了解 Angular 应用程序中的最新状态管理。

在本章中,我们将涵盖以下主题:

  • 文件上传的全部功能

  • 学习可拖放指令

  • 使用 Galleria 显示图像集合

  • 带有 DataTable 的 CRUD 示例实现

  • 推迟机制以优化页面加载

  • 在长时间运行的 AJAX 调用期间阻止页面片段

  • 流程状态指示器的操作

  • 使用 ColorPicker 选择颜色

  • 显示带有受保护路由的确认对话框

  • 使用步骤实现自定义向导组件

  • 介绍使用@ngrx/store 进行状态管理

文件上传的全部功能

FileUpload 组件提供了一个文件上传机制,具有比基本的 HTML <input type="file">文件上传定义更强大的功能。该组件提供了一个基于 HTML5 的 UI,具有拖放、上传多个文件、进度跟踪、验证等功能。

文件上传组件在所有现代浏览器以及 IE 10 及更高版本中均可使用。

基本、多个和自动文件上传

为了能够使用文件上传,需要两个属性--用于在后端标识上传文件的请求参数的名称以及上传文件的远程 URL。例如:

<p-fileUpload name="demofiles[]" url="http://demoserver.com/upload">
</p-fileUpload>

该组件呈现为一个带有三个按钮的面板:Choose、Upload、Cancel,以及一个带有选定文件的内容部分。Choose 按钮显示一个文件对话框,用于选择一个或多个文件。一旦选择,文件可以通过下面的两个按钮上传或取消。默认情况下始终显示文件名和大小。此外,对于图像,您还将看到预览:

预览图像的宽度可以通过previewWidth属性进行调整。

文件上传还提供了一个更简单的 UI,只有一个按钮 Choose,没有内容部分。您可以通过将mode属性设置为"basic"来激活此 UI:

<p-fileUpload mode="basic" name="demofiles[]"  
              url="http://demoserver.com/upload">
</p-fileUpload>

默认情况下,只能从文件对话框中选择一个文件。将multiple选项设置为true允许一次选择多个文件。将auto选项设置为true会立即开始上传,无需按任何按钮。在自动上传模式下,上传和取消按钮是隐藏的:

<p-fileUpload name="demofiles[]" url="http://demoserver.com/upload" 
              [multiple]="true" [auto]="true">
</p-fileUpload>

文件选择也可以通过从文件系统中拖动一个或多个文件并将它们放到 FileUpload 组件的内容部分来完成。

在撰写本文时,FileUpload 组件的后端无法使用 Angular 的模拟 API 进行模拟。在 GitHub 上的演示应用程序中,我们使用一个简单的本地服务器json-servergithub.com/typicode/json-server)来伪造后端。否则,您将面临异常。安装后,可以使用以下命令启动服务器:

json-server db.json --port 3004

项目根目录中的db.json文件只有一个端点的定义:

{
  "fake-backend": {}
}

现在,您可以使用伪造的远程 URL 而不会出现任何异常:

<p-fileUpload name="demofiles[]" url="http://localhost:3004/
              fake-backend">
</p-fileUpload>

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter9/fileupload

文件类型和大小限制

默认情况下,可以上传任何文件类型。文件大小也没有限制。您可以通过分别设置acceptmaxFileSize选项来限制文件类型和大小:

<p-fileUpload name="demofiles[]" url="http://localhost:3004/
              fake-backend" multiple="true" accept="image/*"     
              maxFileSize="50000">
</p-fileUpload>

在这个例子中,只有最大大小为50000字节的图像才能被上传。违反这些规则会导致验证消息出现在内容部分。

accept属性的可能值:

描述
<文件扩展名> 以点开头的文件扩展名,例如.gif.png.doc等。
audio/* 所有音频文件。
video/* 所有视频文件。
image/* 所有图像文件。
<媒体类型> 根据 IANA 媒体类型(www.iana.org/assignments/media-types/media-types.xhtml)的有效媒体类型。例如,application/pdf

要指定多个值,请使用逗号分隔值,例如,accept="audio/*,video/*,image/*"

自定义

验证消息可以使用以下四个选项进行自定义:

属性名称 描述 默认值
invalidFileSizeMessageSummary 无效文件大小的摘要消息。占位符{0}指的是文件名。 {0}:无效文件大小,
invalidFileSizeMessageDetail 无效文件大小的详细消息。占位符{0}指的是文件大小。 最大上传大小为{0}。
invalidFileTypeMessageSummary 无效文件类型的摘要消息。占位符{0}指的是文件类型。 {0}:无效文件类型,
invalidFileTypeMessageDetail 无效文件类型的详细消息。占位符{0}指的是允许的文件类型。 允许的文件类型:{0}

下一个代码片段和屏幕截图演示了自定义消息。它们还展示了如何为按钮设置自定义标签:

<p-fileUpload name="demofiles[]" url="http://localhost:3004/
              fake-backend"
              multiple="true" accept="image/*" maxFileSize="50000"
              invalidFileSizeMessageSummary="{0} has wrong size, "
              invalidFileSizeMessageDetail="it exceeds {0}."
              invalidFileTypeMessageSummary="{0} has wrong file type, "
              invalidFileTypeMessageDetail="it doesn't match: {0}."
              chooseLabel="Select file"
              uploadLabel="Upload it!"
              cancelLabel="Abort">
</p-fileUpload>

UI 可以通过三个命名的ng-template标签进行完全自定义。您可以自定义工具栏、内容部分和已选择文件的区域。下一个代码片段显示了一个完全可定制的 UI:

<p- name="demofiles[]" url="http://localhost:3004/fake-backend"
    multiple="true" accept=".pdf" maxFileSize="1000000">
  <ng-template pTemplate="toolbar">
    <div style="font-size: 0.9em; margin-top: 0.5em;">
      Please select your PDF documents
    </div>
  </ng-template>
  <ng-template let-file pTemplate="file">
    <div style="margin: 0.5em 0 0.5em 0;">
      <i class="fa fa-file-pdf-o" aria-hidden="true"></i>
      {{file.name}}
    </div>
  </ng-template>
  <ng-template pTemplate="content">
    <i class="fa fa-cloud-upload" aria-hidden="true"></i>
    Drag and drop files onto this area
  </ng-template>
</p-fileUpload>

屏幕截图显示了当没有选择文件时的初始 UI 状态:

从文件对话框中选择后,UI 看起来如下:

请注意,只能选择 PDF 文件。ng-templatepTemplate="file"一起使用时,会将File实例作为隐式变量。此实例具有name属性,我们在自定义 UI 中利用它。

请参阅官方文档,了解有关File的更多信息,网址为developer.mozilla.org/en-US/docs/Web/API/File

下一级别的定制是回调事件,它们在特定时间点触发。有onBeforeUploadonBeforeSendonUploadonErroronClearonSelectuploadHandler事件。下一个代码片段演示了其中两个:

<p-fileUpload name="demofiles[]" url="http://localhost:3004/
              fake-backend" accept="image/*" maxFileSize="1000000"
              (onBeforeSend)="onBeforeSend($event)" 
              (onUpload)="onUpload($event)">
</p-fileUpload>

onBeforeUpload事件在上传前不久触发。注册的回调会得到一个具有两个参数的事件对象:

我们可以使用此回调来自定义请求数据,例如提交参数或标头信息。例如,我们可以设置一个令牌jwt并将其发送到服务器。只需在组件类中编写以下回调方法:

onBeforeSend(event: any) {
  (<XMLHttpRequest>event.xhr).setRequestHeader('jwt', 'xyz123');
}

你看,令牌已经发送了:

当所有选定的文件都上传完成时,将触发onUpload事件。传递的事件对象具有上述XMLHttpRequest实例和类型为File的对象数组。我们可以遍历文件并将它们收集在一起进行进一步处理:

uploadMsgs: Message[] = [];
uploadedFiles: any[] = [];

onUpload(event: any) {
  for (let file of event.files) {
    this.uploadedFiles.push(file);
  }

  // produce a message for growl notification
  this.uploadMsgs = [];
  this.uploadMsgs.push({severity: 'info', 
    summary: 'File Uploaded', detail: ''});
}

通过设置customUpload="true"并定义自定义上传处理程序,可以提供自定义上传实现。例如:

<p-fileUpload name="demofiles[]" customUpload="true"
              (uploadHandler)="smartUploader($event)">
</p-fileUpload>

如何实现smartUploader回调取决于您。回调可以访问event.files,这是一个类型为File的对象数组。

学习可拖动和可放置指令

拖放是一种动作,意味着抓取一个对象并将其放到不同的位置。能够被拖放的组件丰富了网络,并为现代 UI 模式打下了坚实的基础。PrimeNG 中的拖放实用程序允许我们高效地创建可拖放的用户界面。它们使开发人员在浏览器级别处理实现细节变得抽象。

在本节中,您将了解pDraggablepDroppable指令。我们将介绍一个包含一些虚构文档的 DataGrid 组件,并使这些文档可拖动以便将它们放到回收站中。回收站实现为一个 DataTable 组件,显示放置文档的属性。为了更好地理解开发的代码,首先是一张图片:

这张图片展示了拖放三个文档后发生的情况。

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter9/dragdrop

可拖动

pDraggable 附加到一个元素上以添加拖动行为。pDraggable 属性的值是必需的,它定义了与可放置元素匹配的范围。默认情况下,整个元素都是可拖动的。我们可以通过应用 dragHandle 属性来限制可拖动的区域。dragHandle 的值可以是任何 CSS 选择器。在 DataGrid 中,我们只使面板的标题可拖动:

<p-dataGrid [value]="availableDocs">
  <p-header>
    Available Documents
  </p-header>
  <ng-template let-doc pTemplate="item">
    <div class="ui-g-12 ui-md-4" pDraggable="docs"
      dragHandle=".ui-panel-titlebar" dragEffect="move"
      (onDragStart)="dragStart($event, doc)" 
        (onDragEnd)="dragEnd($event)">
      <p-panel [header]="doc.title" [style]="{'text-align':'center'}">
        <img src="/assets/data/images/docs/{{doc.extension}}.png">
      </p-panel>
    </div>
  </ng-template>
</p-dataGrid>

可拖动元素在拖动过程开始、进行和结束时可以触发三个事件,分别是 onDragStartonDragonDragEnd。在组件类中,我们在拖动过程开始时缓冲被拖动的文档,并在结束时重置它。这个任务在两个回调函数中完成:dragStartdragEnd

class DragDropComponent {
  availableDocs: Document[];
  deletedDocs: Document[];
  draggedDoc: Document;

  constructor(private docService: DocumentService) { }

  ngOnInit() {
    this.deletedDocs = [];
    this.docService.getDocuments().subscribe((docs: Document[]) =>
      this.availableDocs = docs);
  }

  dragStart(event: any, doc: Document) {
    this.draggedDoc = doc;
  }

  dragEnd(event: any) {
    this.draggedDoc = null;
  }

  ...
}

在所示的代码中,我们使用了 Document 接口,具有以下属性:

interface Document {
  id: string;
  title: string;
  size: number;
  creator: string;
  creationDate: Date;
  extension: string;
}

在演示应用程序中,当鼠标移动到任何面板的标题上时,我们将光标设置为 move。这个技巧为可拖动区域提供了更好的视觉反馈:

body .ui-panel .ui-panel-titlebar {
  cursor: move;
}

我们还可以设置 dragEffect 属性来指定拖动操作允许的效果。可能的值有 nonecopymovelinkcopyMovecopyLinklinkMoveall。请参考官方文档以获取更多细节:developer.mozilla.org/en-US/docs/Web/API/DataTransfer/effectAllowed

可放置

pDroppable 附加到一个元素上以添加放置行为。pDroppable 属性的值应该与 pDraggable 的范围相同。

放置区域的范围也可以是一个数组,以接受多个可放置元素。

可放置元素可以触发四个事件:

事件名称 描述
onDragEnter 当可拖动元素进入放置区域时调用。
onDragOver 当可拖动元素被拖动到放置区域时调用。
onDrop 当可拖动元素放置到放置区域时调用。
onDragLeave 当可拖动元素离开放置区域时调用。

在演示应用程序中,可放置区域的整个代码如下所示:

<div pDroppable="docs" (onDrop)="drop($event)" 
     [ngClass]="{'dragged-doc': draggedDoc}">
  <p-dataTable [value]="deletedDocs">
    <p-header>Recycle Bin</p-header>
    <p-column field="title" header="Title"></p-column>
    <p-column field="size" header="Size (bytes)"></p-column>
    <p-column field="creator" header="Creator"></p-column>
    <p-column field="creationDate" header="Creation Date">
      <ng-template let-col let-doc="rowData" pTemplate="body">
        {{doc[col.field].toLocaleDateString()}}
      </ng-template>
    </p-column>
  </p-dataTable>
</div>

每当将文档拖放到回收站时,被放置的文档将从所有可用文档列表中移除,并添加到已删除文档列表中。这发生在onDrop回调中:

drop(event: any) {
  if (this.draggedDoc) {
    // add draggable element to the deleted documents list 
    this.deletedDocs = [...this.deletedDocs, this.draggedDoc];
    // remove draggable element from the available documents list
    this.availableDocs = this.availableDocs.filter(
      (e: Document) => e.id !== this.draggedDoc.id);
    this.draggedDoc = null;
  }
}

可用和已删除的文档都通过创建新数组来更新,而不是操作现有数组。这在数据迭代组件中是必要的,以强制 Angular 运行变更检测。操作现有数组不会运行变更检测,UI 将不会更新。

拖动任何带有文档的面板时,回收站区域会变成红色边框。我们通过将ngClass设置为[ngClass]="{'dragged-doc': draggedDoc}"来实现这种突出显示。当设置了draggedDoc对象时,样式类dragged-doc就会启用。样式类定义如下:

.dragged-doc {
  border: solid 2px red;
}

使用 Galleria 显示图像集合

Galleria 组件可用于显示具有过渡效果的图像集合。

让它运行起来

图像集合是以编程方式创建的--它是一个具有以下三个属性的对象数组:

  • source:图片的路径

  • title:标题部分的标题文本

  • alt:标题下方的描述

让我们创建一个GalleriaComponent类:

class GalleriaComponent {
  images: any[];

  ngOnInit() {
    this.images = [];

    this.images.push({
      source: '/assets/data/images/cars/Yeni.png',
      alt: 'This is a first car',
      title: 'Yeni Vollkswagen CC'
    });
    this.images.push({
      source: '/assets/data/images/cars/Golf.png',
      alt: 'This is a second car',
      title: 'Golf'
    });

    ... // more image definitions
  }
}

在 HTML 代码中,集合通过输入属性images进行引用:

<p-galleria [images]="images" panelWidth="400" panelHeight="320"
            [autoPlay]="false" [showCaption]="true">
</p-galleria>

开发的 UI 如下所示:

内容面板的宽度和高度可以通过panelWidthpanelHeight属性进行自定义。showCaption属性可以启用在标题部分显示标题和描述。

底部有一个名为filmstrip的小图像区域。通过showFilmstrip属性,默认情况下启用 filmstrip 的可见性。您可以通过将属性设置为false来禁用它。在 filmstrip 中可视化的帧的宽度和高度分别可以通过frameWidthframeHeight属性进行自定义。所有值应以像素为单位提供。

还有activeIndex属性,可用于设置显示图像的位置。例如,如果您想在初始页面加载时显示第二张图像,可以设置activeIndex="1"。默认值为0

自动播放模式和效果

自动播放模式可以打开幻灯片放映。自动播放模式默认情况下是启用的。在示例中,我们通过设置[autoPlay]="false"来禁用幻灯片放映。自动播放模式中图像之间的过渡在4000毫秒内完成。可以使用transitionInterval属性自定义此时间间隔。

在遍历图像时,可以应用过渡效果。effect属性可以取blindbounceclipdropexplodefade(默认值)、foldhighlightpuffpulsatescaleshakesizeslidetransfer这些值。effectDuration属性也可以用于决定过渡的持续时间。其默认值为250毫秒:

<p-galleria [images]="images" panelWidth="400" panelHeight="320"
            effect="bounce" [effectDuration]="150">
</p-galleria>

事件

只有一个事件onImageClicked,当点击显示的图像时触发:

<p-galleria [images]="images" panelWidth="400" panelHeight="220"
            [autoPlay]="false" [showCaption]="true"
            (onImageClicked)="onImageClicked($event)">
</p-galleria>

调用的回调会得到一个事件对象。除了点击图像的索引和原生点击事件之外,传入的事件对象还保留了集合中的整个图像实例。我们可以在回调中访问源 URL 并在新的浏览器标签中打开图像:

onImageClicked($event: any) {
  window.open($event.image.source, '_blank');
}

带有说明的完整演示应用程序可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter9/galleria

带有 DataTable 的 CRUD 示例实现

PrimeNG 是为企业应用程序创建的。实现CRUD创建、读取、更新删除)场景很容易。本节中的示例演示了使用作为域模型对象的员工的这种场景。可以获取、创建、更新和删除员工。所有 CRUD 操作都是通过 Angular 的 HTTP 服务进行的,该服务与模拟后端进行通信。我们将在本节的使用@ngrx/store 进行状态管理介绍中改进我们的 CRUD 实现。

使用以下接口定义了域模型对象Employee

export interface Employee {
  id: string;
  firstName: string;
  lastName: string;
  profession: string;
  department: string;
}

模拟后端在此处未显示,因为它超出了本书的范围。

带有说明的完整演示应用程序可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter9/crud-datatable

EmployeeService类具有 CRUD 操作,值得在这里列出。它公开了四种方法,返回值为Observable,以便组件类可以调用subscribe()来接收传递的数据:

@Injectable()
export class EmployeeService { 
  private static handleError(error: Response | any) {
    // error handling is done as recommended 
    //in the official Angular documentation
    // https://angular.io/docs/ts/latest/guide/server-
    //communication.html#!#always-handle-errors
    ...
  }

  constructor(private http: Http) { }

  getEmployees(): Observable<Employee[]> {
    return this.http.get('/fake-backend/employees')
      .map(response => response.json() as Employee[])
      .catch(EmployeeService.handleError);
  }

  createEmployee(employee: Employee): Observable<Employee> {
    return this.http.post('/fake-backend/employees', employee)
      .map(response => response.json() as Employee)
      .catch(EmployeeService.handleError);
  }

  updateEmployee(employee: Employee): Observable<any> {
    return this.http.put('/fake-backend/employees', employee)
      .map(response => response.json())
      .catch(EmployeeService.handleError);
  }

  deleteEmployee(id: string): Observable<any> {
    return this.http.delete('/fake-backend/employees/' + id)
      .map(response => response.json())
      .catch(EmployeeService.handleError);
  }
}

当从后端获取员工时,员工将显示在 DataTable 中:

如您所见,当没有选择员工时,只有添加按钮是启用的。添加和编辑按钮触发显示员工个人数据的对话框。保存按钮根据之前选择的是添加还是编辑按钮,创建新员工或更新现有员工。

按钮触发的操作会淡入相应的消息:

表格是使用p-dataTable实现的,如下所示:

<p-dataTable [value]="employees" selectionMode="single"  
             [(selection)]="selectedEmployee"  
             [paginator]="true" rows="15" 
             [responsive]="true" 
             [alwaysShowPaginator]="false">
  <p-column field="firstName" header="First Name" [sortable]="true">
  </p-column>
  <p-column field="lastName" header="Last Name" [sortable]="true">
  </p-column>
  <p-column field="profession" header="Profession" [sortable]="true">
  </p-column>
  <p-column field="department" header="Department" [sortable]="true">
  </p-column>
  <p-footer>
    <button pButton type="button" label="Add" icon="fa-plus" 
      (click)="add()"> </button>
    <button pButton type="button" label="Edit" icon="fa-pencil" 
      (click)="edit()" [disabled]="!selectedEmployee"></button>
    <button pButton type="button" label="Remove" icon="fa-trash-o" 
    (click)="remove()" [disabled]="!selectedEmployee"></button>
  </p-footer>
</p-dataTable>

p-dataTable的值绑定到数组属性employees。通过点击行选择员工,并保存在selectedEmployee属性中。当selectedEmployee未设置时,编辑和删除按钮将被禁用。为了简洁起见,我们将跳过对话框的代码。最有趣的部分是组件类。员工是通过EmployeeServicengOnInit()生命周期回调中获取的:

export class DataTableCrudComponent implements OnInit, 
  OnDestroy {
  employees: Employee[];
 selectedEmployee: Employee;
 employeeForDialog: Employee;
 displayDialog: boolean;
 msgs: Message[] = [];

 get$: Subscription;
 add$: Subscription;
 edit$: Subscription;
 delete$: Subscription;

 constructor(private employeeService: EmployeeService) { }

 ngOnInit(): void {
 this.get$ = this.employeeService.getEmployees().subscribe(
      employees => this.employees = employees,
      error => this.showError(error)
    );
  }

 ngOnDestroy() {
 this.get$.unsubscribe();
    this.add$.unsubscribe();
    this.edit$.unsubscribe();
    this.delete$.unsubscribe();
  }

  ...

  private showError(errMsg: string) {
    this.msgs = [];
    this.msgs.push({severity: 'error', 
                    summary: 'Sorry, an error occurred', 
                    detail: errMsg});
  }

  private showSuccess(successMsg: string) {
    this.msgs = [];
    this.msgs.push({severity: 'success', detail: successMsg});
  }
}

让我们详细探讨其他 CRUD 方法。add()方法构建一个空的员工实例,edit()方法克隆所选的员工。两者都在对话框中使用。属性displayDialog设置为true,强制显示对话框。

该属性在视图中绑定到对话框的可见性,如下所示[(visible)]="displayDialog"

add() {
  // create an empty employee
  this.employeeForDialog = {
    id: null, firstName: null, lastName: null, profession: null,
    department: null
  }; 
  this.displayDialog = true;
}

edit() {
  // create a clone of the selected employee
  this.employeeForDialog = Object.assign({}, this.selectedEmployee);
  this.displayDialog = true;
}

对话框中的保存按钮调用save()方法,其中我们通过id来检查员工是否存在。只有之前保存过的员工才包含id,因为id是在后端分配的。现有员工应该被更新,新员工应该被创建:

save() {
  if (this.employeeForDialog.id) {
    // update
    this.edit$ = 
      this.employeeService.updateEmployee(this.employeeForDialog)
      .finally(() => {
        this.employeeForDialog = null;
        this.displayDialog = false;
      })
      .subscribe(() => {
          this.employees.some((element: Employee, index: number) => {
            if (element.id === this.employeeForDialog.id) {
              this.employees[index] = Object.assign({}, 
              this.employeeForDialog);
              this.employees = [...this.employees];
              this.selectedEmployee = this.employees[index];
              return true;
            }
          });
          this.showSuccess('Employee was successfully updated');
        },
        error => this.showError(error)
      );
  } else {
    // create
    this.add$ = 
      this.employeeService.createEmployee(this.employeeForDialog)
      .finally(() => {
        this.employeeForDialog = null;
        this.selectedEmployee = null;
        this.displayDialog = false;
      })
      .subscribe((employee: Employee) => {
          this.employees = [...this.employees, employee];
          this.showSuccess('Employee was successfully created');
        },
        error => this.showError(error)
      );
  }
}

员工将在后端和employees数组中更新或创建:

如您所见,创建了employees数组的新实例,而不是操作现有的数组。这在数据迭代组件中是必要的,以强制 Angular 运行变更检测。操作现有数组中的元素不会更新数组的引用。结果,变更检测不会运行,UI 也不会更新。

注意,Observable提供了一个finally方法,我们可以在其中重置属性的值。

作为参数传递给finally方法的函数在源可观察序列正常或异常终止后被调用。

remove()方法由删除按钮调用:

remove() {
  this.delete$ = 
  this.employeeService.deleteEmployee(this.selectedEmployee.id)
    .finally(() => {
      this.employeeForDialog = null;
      this.selectedEmployee = null;
    })
    .subscribe(() => {
        this.employees = this.employees.filter(
          (element: Employee) => element.id !== 
          this.selectedEmployee.id);
        this.showSuccess('Employee was successfully removed');
      },
      error => this.showError(error)
    );
}

序列逻辑与其他 CRUD 操作类似。

推迟机制以优化页面加载

大型应用程序总是需要最佳实践来提高页面加载时间。不建议等到所有页面内容完全加载后再显示登陆页面。PrimeNG 提供了一个 defer 指令,可以推迟内容加载直到组件出现在视口中。当页面滚动时,内容将在变得可见时懒加载。

pDefer指令应用于容器元素,内容需要用ng-template指令包装如下:

<div pDefer (onLoad)="loadData()">
  <ng-template>
    deferred content
  </ng-template>
</div>

当您使用数据迭代组件(如p-dataTablep-dataListp-dataGrid等)时,defer 指令非常有助于延迟加载大型数据集。onLoad回调用于在组件通过页面滚动变得可见时按需从数据源查询数据。查询不会在页面加载时启动,因此页面加载速度很快。这里实现了一个具体的例子:

<div pDefer (onLoad)="loadData()">
  <ng-template>
    <p-dataTable [value]="employees">
      <p-column field="firstName" header="First Name"></p-column>
      <p-column field="lastName" header="Last Name"></p-column>
      <p-column field="profession" header="Profession"></p-column>
      <p-column field="department" header="Department"></p-column>
    </p-dataTable>
  </ng-template>
</div>

loadData()方法获取员工信息:

loadData(): void {
  this.employeeService.getEmployees().subscribe(
    employees => this.employees = employees,
    error => this.showError(error)
  );
}

在长时间运行的 AJAX 调用期间阻止页面片段

BlockUI 组件允许我们阻止页面的任何部分,例如在 AJAX 调用期间。BlockUI 组件在目标元素上添加一个层,并提供阻止用户交互的外观和行为。如果您有一个大型的 DataTable 组件,并且 CRUD 操作需要很长时间,这将非常方便。您几乎可以阻止一切事物--甚至整个页面。在本节中,我们将演示如何处理 BlockUI。

BlockUI 组件阻止一个可阻止的目标组件。target属性指向这样一个目标组件的模板引用变量。BlockUI 的可见性由布尔属性blocked控制。例如,以下 BlockUI 在属性blocked设置为true时阻止 Panel 组件,并在其他情况下解除阻止:

<p-blockUI [blocked]="blocked" [target]="pnl">
  // any custom content or empty
</p-blockUI>

<p-panel #pnl header="Panel Header">
  Content of Panel
</p-panel>

target的默认值是document对象。这意味着,如果没有提供target,整个页面都会被阻塞。正如你所看到的,可以在p-blockUI标签中放置任何自定义内容。自定义内容会显示在半透明层上。

我们将利用上一节中的 CRUD 示例来演示 BlockUI 组件的工作原理。为了简洁起见,只有两个按钮可用--一个是重新加载按钮,用于执行数据获取,另一个是删除按钮。

让我们指定阻塞方式--重新加载按钮应该阻塞整个页面,删除按钮应该只阻塞表格。此外,我们希望显示一个加载指示器和文本 Loading...,如图所示:

这些验收标准导致了两个 BlockUI 组件:

<p-dataTable ... #dtable>
  ...
</p-dataTable>

<p-blockUI [blocked]="blockedTable" [target]="dtable">
  <div class="center">
    <div class="box">
      <div class="content">
        <img src="/assets/data/images/loader.svg"/>
        <h1>Loading...</h1>
      </div>
    </div>
  </div>
</p-blockUI>

<p-blockUI [blocked]="blockedPage">
  <div class="center">
    <div class="box">
      <div class="content">
        <img src="/assets/data/images/loader.svg"/>
        <h1>Loading...</h1>
      </div>
    </div>
  </div>
</p-blockUI>

blockedTableblockedPage属性在按钮点击时立即设置为true。CRUD 操作完成后,这些属性设置为false。这种方法在下面的代码块中概述:

export class DataTableCrudComponent {
  ...
  selectedEmployee: Employee;
  blockedTable: boolean;
  blockedPage: boolean;

  reload() {
    this.blockedPage = true;
    this.employeeService.getEmployees()
      .finally(() => {this.blockedPage = false;})
      .subscribe(...);
  }

  remove() {
    this.blockedTable = true;
    this.employeeService.deleteEmployee(this.selectedEmployee.id)
      .finally(() => {this.blockedTable = false; 
        this.selectedEmployee = null;})
      .subscribe(...);
    }
}

被阻塞组件上的半透明层可以按以下方式自定义:

.ui-blockui.ui-widget-overlay {opacity: 0.5;}完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter9/blockui.

进程状态指示器在工作中

ProgressBar 组件指示某个过程、任务或其他内容的状态。它可以处理静态值和动态值。动态值是随时间变化的值。下面的代码片段演示了两个进度条,一个是静态值,一个是动态值:

<p-growl [value]="msgs"></p-growl>

<h3>Static value</h3>
<p-progressBar [value]="40"></p-progressBar>

<h3>Dynamic value</h3>
<p-progressBar [value]="value"></p-progressBar>

动态值每 800 毫秒从 1 到 100 产生,使用Observable方法如下:

export class ProgressBarComponent implements OnInit, OnDestroy {
  msgs: Message[];
  value: number;
  interval$: Subscription;

  ngOnInit() {
    const interval = Observable.interval(800).take(100);
    this.interval$ = interval.subscribe(
      x => this.value = x + 1,
      () => {/** no error handling */ },
      () => this.msgs = [{severity: 'info', summary: 'Success', 
        detail: 'Process completed'}]
    );
  }

  ngOnDestroy() {
    this.interval$.unsubscribe();
  }
}

最后,将显示一个带有文本 Process completed 的 growl 消息。快照图片如下所示:

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter9/progressbar.

使用 ColorPicker 选择颜色

ColorPicker 是一个用于从二维方框中选择颜色的图形用户界面输入小部件。该组件使用ngModel指令进行双向值绑定。基本上,它支持三种颜色格式,如十六进制、RGB 和 HSB,其中十六进制是默认类型。颜色格式用format属性表示,例如,format="rgb"。ColorPicker 是一个可编辑的组件,也可以在模型驱动的表单中使用。一个基本的例子如下:

<p-colorPicker [(ngModel)]="color1"></p-colorPicker>

组件必须为默认十六进制值定义string类型的颜色属性,而颜色属性应该是对象类型,用于 RGB 和 HSB 格式,如下所示:

color1: string;
color2: any = {r: 100, g: 120, b: 140};
color3: any = {h: 80, s: 50, b: 40};

颜色选择器将显示所选颜色如下:

默认情况下,颜色选择器以覆盖格式显示,但可以使用inline属性更改此默认行为,通过启用内联设置,可以启用内联格式。内联格式的颜色选择器组件将如下所示:

<p-colorPicker [(ngModel)]="color3" inline="true"
 (onChange)="change($event)"></p-colorPicker>

此组件还支持带有event对象作为参数的onChange回调。event对象保存浏览器事件和所选颜色值,以通知更改如下:

change(event){
    this.msgs = [];
    this.msgs.push({severity: 'success', 
 summary: 'The color is changed from ColorPicker',
 detail: 'The selected color is ' + event.value});
}

像其他输入组件一样,ColorPicker 支持模型驱动的表单,禁用属性以禁用用户交互,等等。

完整的演示应用程序及说明可在 GitHub 上找到github.com/ova2/angular-development-with-primeng/tree/master/chapter9/colorpicker

显示带有受保护路由的确认对话框

在 Angular 2+中,您可以使用守卫保护路由。最常用的守卫类型是CanActivateCanDeactivate。第一个守卫类型决定是否可以激活路由,第二个守卫类型决定是否可以停用路由。在本节中,我们将讨论CanDeactivate。这是一个只有一个方法canDeactivate的接口。

export interface CanDeactivate<T> {
  canDeactivate(component: T, route: ActivatedRouteSnapshot,                 
    state: RouterStateSnapshot):
    Observable<boolean> | Promise<boolean> | boolean;
}

此方法可以返回Observable<boolean>Promise<boolean>boolean。如果boolean的值为true,用户可以从路由中导航离开。如果boolean的值为false,用户将保持在相同的视图上。如果您想在某些情况下阻止路由导航离开,您必须执行三个步骤:

  1. 创建一个实现CanDeactivate接口的类。该类充当守卫,当从当前视图导航离开时,路由器将对其进行检查。正如你所看到的,该接口期望一个通用组件类。这是当前在<router-outlet>标签中呈现的组件。

  2. 在使用@NgModule注释的模块中将此守卫注册为提供者。

  3. 将此守卫添加到路由器配置中。路由器配置具有canDeactivate属性,可以在其中多次添加此类守卫。

您可能想查看官方 Angular 文档中的示例

angular.io/docs/ts/latest/api/router/index/CanDeactivate-interface.html

在本书中,我们想要实现一个典型的用例,即检查用户是否有一些未保存的输入更改。如果当前视图具有未保存的输入值,并且用户试图导航到另一个视图,应该显示一个确认对话框。我们将使用 PrimeNG 的 ConfirmDialog:

现在,点击“是”按钮会导航到另一个视图:

点击“否”按钮会阻止从当前路由导航的过程。让我们创建第一个视图,其中包含一个input元素,一个submit按钮和<p-confirmDialog>组件:

<h1>This is the first view</h1>

<form novalidate (ngSubmit)="onSubmit(f)" #f="ngForm">
  <label for="username">Username:</label>
  <input id="username" name="username" type="text" 
    pInputText [(ngModel)]="username"/>
  <button type="submit" pButton label="Confirm"></button>
</form>

<p-confirmDialog header="Confirmation" icon="fa fa-question-circle" 
  width="400">
</p-confirmDialog>

该模板对应的组件保持表单的dirty状态,表示表单正在被编辑:

export class FirstViewComponent {
  dirty: boolean;
  username: string;

  constructor(private router: Router) { }

  onSubmit(f: FormGroup) {
    this.dirty = f.dirty;
    this.router.navigate(['/chapter9/second-view']);
  }
}

我们不会实现任何复杂的算法来检查输入值是否真的已更改。我们只检查表单的dirty状态。如果表单没有被编辑,提交时导航应该没问题。无需询问用户有关未保存的更改。现在,我们必须将 PrimeNG 的ConfirmationService注入到我们的守卫实现中,这是显示确认对话框所必需的,并在canDeactivate方法中像这样使用它:

this.confirmationService.confirm({
  message: 'You have unsaved changes. 
  Are you sure you want to leave this page?',
  accept: () => {
    // logic to perform a confirmation
  },
  reject: () => {
    // logic to cancel a confirmation
  }
});

但是有一个问题。confirm方法没有返回所需的Observable<boolean>Promise<boolean>boolean。解决方案是通过调用Observable.create()创建并返回一个Observable对象。create方法期望一个带有一个参数observer: Observer<boolean>的回调。现在我们需要执行两个步骤:

  • 将调用this.confirmationService.confirm()放入回调函数体中。

  • 通过调用observer.next(true)observer.next(false)truefalse传递给订阅者。订阅者是 PrimeNG 的组件ConfirmDialog,需要被告知用户的选择。

下面显示了UnsavedChangesGuard的完整实现:

@Injectable()
export class UnsavedChangesGuard implements 
  CanDeactivate<FirstViewComponent> {

  constructor(private confirmationService: ConfirmationService) { }

  canDeactivate(component: FirstViewComponent) {
    // Allow navigation if the form is unchanged
    if (!component.dirty) { return true; }

    return Observable.create((observer: Observer<boolean>) => {
      this.confirmationService.confirm({
        message: 'You have unsaved changes. 
        Are you sure you want to leave this page?',
        accept: () => {
          observer.next(true);
          observer.complete();
        },
        reject: () => {
          observer.next(false);
          observer.complete();
        }
      });
    });
  }
}

正如我们已经说过的,该守卫已在路由配置中注册:

{path: 'chapter9/first-view', component: FirstViewComponent, 
  canDeactivate: [UnsavedChangesGuard]}

如果您更喜欢Promise而不是Observable,可以返回Promise如下:

return new Promise((resolve, reject) => {
  this.confirmationService.confirm({
    message: "You have unsaved changes. 
    Are you sure you want to leave this page?",
    accept: () => {
      resolve(true);
    },
    reject: () => {
      resolve(false);
    }
  });
});

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter9/guarded-routes

使用 Steps 实现自定义向导组件

PrimeNG 有一个名为 Steps 的组件,用于指示工作流程中的步骤。使用方法很简单:

<p-steps [model]="items" [(activeIndex)]="activeIndex"></p-steps>

modelMenuItem类型的对象集合,我们在第七章中遇到过,无尽菜单变化。属性activeIndex指向项目集合中活动项目(步骤)的索引。默认值为0,表示默认选择第一个项目。我们还可以通过设置[readonly]="false"使项目可点击。

请参考 PrimeNG 展示以查看步骤的操作:

www.primefaces.org/primeng/#/steps

基于<p-steps>,我们将使用两个自定义组件<pe-steps><pe-step>来实现类似向导的行为。前缀pe应该提示"PrimeNG 扩展"。组件<pe-steps>作为多个步骤的容器。基本结构:

<pe-steps [(activeIndex)]="activeIndex" (change)="onChange($event)">
  <pe-step label="First Step">
    // content of the first step
  </pe-step> 
  <pe-step label="Second Step">
    // content of the second step
  </pe-step> 
  <pe-step label="Third Step">
    // content of the third step
  </pe-step>
</pe-steps>

我们可以将这个结构理解为向导。在向导步骤之间的导航通过点击面包屑项目(可点击的步骤)、导航按钮或通过编程设置步骤索引(activeIndex)来实现。下一张截图显示了向导和导航的样子:

在开始实施之前,让我们先指定 API。<pe-step>组件具有以下内容:

属性

名称 类型 默认值 描述
styleClass string null 单个 Step 组件的样式类
label string null 显示此 Step 的标签

样式

名称 元素
pe-step-container 单个 Step 组件的容器元素

<pe-steps>组件具有:

属性

名称 类型 默认值 描述
activeIndex number 0 活动步骤的索引(双向绑定)
- styleClass string null 向导容器元素的样式类
- stepClass string null 每个步骤组件的样式类

事件

名称 参数 描述
change label:当前显示步骤的标签 切换步骤时调用的回调函数

具有这些知识,我们可以实现StepComponentStepsComponent。第一个在模板中有ng-content,以便放置自定义内容。组件类有两个指定的输入。此外,还有一个active属性,指示当前是否显示该步骤:

@Component({
  selector: 'pe-step',
  styles: ['.pe-step-container {padding: 45px 25px 45px 25px; 
           margin-bottom: 20px;}'],
  template: `
    <div *ngIf="active" [ngClass]="'ui-widget-content ui-corner-all
         pe-step-container'" [class]="styleClass">
      <ng-content></ng-content>
    </div>
  `
})
export class StepComponent {
  @Input() styleClass: string;
  @Input() label: string;
  active: boolean = false;
}

第二个组件更复杂。它遍历类型为StepComponent的子组件,并在生命周期方法ngAfterContentInit()中创建项目。如果子组件的active属性与activeIndex匹配,则将其设置为true。否则,将其设置为false。这允许在工作流程中显示一个步骤。完整的清单将超出本书的篇幅。我们只会展示一部分:

@Component({
  selector: 'pe-steps',
  template: `
    <p-steps [model]="items" [(activeIndex)]="activeIndex"
      [class]="styleClass" [readonly]="false"></p-steps> 
      <ng-content></ng-content>
      <button pButton type="text" *ngIf="activeIndex > 0"
        (click)="previous()" icon="fa-hand-o-left" label="Previous">
      </button>
      <button pButton type="text" *ngIf="activeIndex 
        < items.length - 1"
        (click)="next()" icon="fa-hand-o-right" 
          iconPos="right" label="Next"> 
      </button>
  `
})
export class StepsComponent implements AfterContentInit, OnChanges {
  @Input() activeIndex: number = 0;
  @Input() styleClass: string;
  @Input() stepClass: string;
  @Output() activeIndexChange: EventEmitter<any> = new EventEmitter();
  @Output() change = new EventEmitter();
  items: MenuItem[] = [];
  @ContentChildren(StepComponent) steps: QueryList<StepComponent>;

  ngAfterContentInit() {
    this.steps.toArray().forEach((step: StepComponent, 
      index: number) => 
      {
      ...
      if (index === this.activeIndex) { step.active = true; }

      this.items[index] = {
        label: step.label,
        command: (event: any) => {
          // hide all steps
          this.steps.toArray().forEach((s: StepComponent) => 
            s.active = false);

          // show the step the user has clicked on.
          step.active = true;
          this.activeIndex = index;

          // emit currently selected index (two-way binding)
          this.activeIndexChange.emit(index);
          // emit currently selected label
          this.change.next(step.label);
        }
      };
    });
  }

  ngOnChanges(changes: SimpleChanges) {
    if (!this.steps) { return; }

    for (let prop in changes) {
      if (prop === 'activeIndex') {
        let curIndex = changes[prop].currentValue;
        this.steps.toArray().forEach((step: StepComponent, 
          index: number) => {
          // show / hide the step
          let selected = index === curIndex;
          step.active = selected;
          if (selected) {
            // emit currently selected label
            this.change.next(step.label);
          }
        });
      }
    }
  }

  private next() {
    this.activeIndex++;
    // emit currently selected index (two-way binding)
    this.activeIndexChange.emit(this.activeIndex);
    // show / hide steps and emit selected label
    this.ngOnChanges({
      activeIndex: {
        currentValue: this.activeIndex,
        previousValue: this.activeIndex - 1,
        firstChange: false,
        isFirstChange: () => false
      }
    });
  }

  ...
}

完全实现和文档化的组件可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter9/primeng-extensions-wizard

要使实现的向导可分发,我们需要创建WizardModule

import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {StepComponent} from './step.component';
import {StepsComponent} from './steps.component';
import {ButtonModule} from 'primeng/components/button/button';
import {StepsModule} from 'primeng/components/steps/steps';

@NgModule({
  imports: [CommonModule, ButtonModule, StepsModule],
  exports: [StepComponent, StepsComponent],
  declarations: [StepComponent, StepsComponent]
})
export class WizardModule { }

WizardModule类可以像通常在@NgModule内部的imports一样在任何 PrimeNG 应用程序中导入。显示图片的具体用法示例如下:

<pe-steps [(activeIndex)]="activeIndex" (change)="onChange($event)">
  <pe-step label="First Step">
    <label for="firstname">First Name:</label>
    <input id="firstname" name="firstname" type="text" 
      pInputText [(ngModel)]="firstName"/>
    <button pButton label="Go" (click)="next()"></button>
  </pe-step> 
  <pe-step label="Second Step">
    <label for="lastname">Last Name:</label>
    <input id="lastname" name="lastname" type="text" 
      pInputText [(ngModel)]="lastName"/>
    <button pButton label="Go" (click)="next()"></button>
  </pe-step> 
  <pe-step label="Third Step">
    <label for="address">Address:</label>
    <input id="address" name="address" type="text" 
      pInputText [(ngModel)]="address"/>
    <button pButton label="Ok" (click)="ok()"></button>
  </pe-step>
</pe-steps>

<p-growl [value]="msgs"></p-growl>

相应的组件实现了next()ok()方法,以及事件回调onChange()。要前进,只需编写next() {this.activeIndex++;}。有关更多详细信息,请参阅 GitHub 项目。

向导组件可以使用npm run update发布到npm存储库。在 GitHub 项目中没有运行演示应用程序和npm start命令。

使用@ngrx/store 进行状态管理的介绍

在过去几年中,大型 Angular 应用程序中的状态管理是一个薄弱点。在 AngularJS 1 中,状态管理通常是作为服务、事件和$rootScope的混合来完成的。在 Angular 2+中,应用程序状态和数据流更清晰,但在 Angular 核心中仍然没有统一的状态管理。开发人员经常使用Redux--JavaScript 应用程序的可预测状态容器(redux.js.org)。Redux 架构最为人所知的是与React库(facebook.github.io/react)一起使用,但它也可以与 Angular 一起使用。为 Angular 设计的一种流行的类似 Redux 的状态容器是ngrx/storegithub.com/ngrx/store)。

Redux 原则

Redux 遵循三个基本原则:

  • 应用程序的整个状态存储在一个称为store的单个不可变状态树中。不允许在 store 之外进行状态管理。一个中心化的不可变存储有很多好处。您可以通过使用ChangeDetectionStrategy.OnPush来提高性能,因为使用不可变数据,Angular 只需要检查对象引用来检测更改。此外,撤消/重做功能很容易实现。

  • Actions用于将信息从应用程序发送到 store。只有 actions 是 store 的信息来源。Actions 是具有typepayload属性的普通 JavaScript 对象。type属性描述了我们想要的状态变化的类型。payload属性是要发送到 store 以更新它的数据。

  • 状态变化是通过称为reducers的纯函数进行的。纯函数是不会改变对象的函数,而是返回全新的对象。我们可以将 reducers 看作是存储中的处理步骤,允许状态转换。Reducer 在当前状态上操作并返回一个新状态。

总的来说,数据流是双向的。一个组件中的用户输入可能会影响其他组件,反之亦然。Redux 应用程序中的数据流是单向的。视图中的更改触发操作。操作被分派到 store。Reducers 根据操作执行状态更改,通过采用先前的状态和分派的操作返回下一个状态作为新对象。

Object.assign()spread 运算符可以帮助返回新对象(redux.js.org/docs/recipes/UsingObjectSpreadOperator.html)。

多个组件可以订阅存储以观察随时间的状态变化并将其传播到视图。以下图表记忆了所述的 Redux 原则:

经典的 Redux 存储提供了两个重要的 API:

  • 使用 store.dispatch(action) 分发操作

  • 使用 store.subscribe(callback) 注册更改通知的监听器

如您所见,如果您使用 Redux 存储,您不需要手动在组件之间同步状态。

可预测的状态管理允许使用 co 调试应用程序,称为时间旅行调试器。您需要安装 store-devtoolsgithub.com/ngrx/store-devtools)以及适当的 Chrome 扩展。

使用 @ngrx/store 的 CRUD 应用程序

作为一个实际的例子,我们将重用“使用 DataTable 实现 CRUD 示例”部分中的相同 CRUD 示例实现。首先添加 Redux-based 应用程序的 ngrx 依赖项:

npm install @ngrx/store @ngrx/core --save

首先,我们需要为存储定义一个形状。在实际应用中,最有可能可用的员工和当前选择的员工可能会在多个组件之间共享。因此,存储可以定义如下:

export interface AppStore {
  employees: Employee[];
  selectedEmployee: Employee;
}

接下来,我们需要定义由类型和可选载荷组成的操作。

最佳实践是创建封装相关操作的 Action Creator Services

github.com/ngrx/ngrx.github.io/blob/master/store/recipes/actions/action_services.md

我们将创建名为 CrudActions 的服务,其中包含四个 CRUD 操作和相关的操作创建者:

@Injectable()
export class CrudActions {
  static LOAD_EMPLOYEES = 'LOAD_EMPLOYEES';
  static CREATE_EMPLOYEE = 'CREATE_EMPLOYEE';
  static UPDATE_EMPLOYEE = 'UPDATE_EMPLOYEE';
  static DELETE_EMPLOYEE = 'DELETE_EMPLOYEE';

  loadEmployees(employees: Employee[]): Action {
    return {type: CrudActions.LOAD_EMPLOYEES, payload: employees};
  }

  createEmployee(employee: Employee): Action {
    return {type: CrudActions.CREATE_EMPLOYEE, payload: employee};
  }

  updateEmployee(employee: Employee): Action {
    return {type: CrudActions.UPDATE_EMPLOYEE, payload: employee};
  }

  deleteEmployee(id: string): Action {
    return {type: CrudActions.DELETE_EMPLOYEE, payload: id};
  }
}

核心部分是 reducer。reducer 函数接受状态和操作,然后使用 switch 语句根据操作类型返回新状态。当前状态不会被改变:

import {ActionReducer, Action} from '@ngrx/store';
import {AppStore} from './app.store';
import {CrudActions} from './crud.actions';
import {Employee} from '../model/employee';

const initialState: AppStore = {employees: [], selectedEmployee: null};

export const crudReducer: ActionReducer<AppStore> =
  (state: AppStore = initialState, action: Action): AppStore => {
 switch (action.type) {
    case CrudActions.LOAD_EMPLOYEES:
      return {
        employees: [...action.payload],
        selectedEmployee: null
  };

    case CrudActions.DELETE_EMPLOYEE:
      return {
        employees: state.employees.filter(
          (element: Employee) => element.id !== action.payload),
          selectedEmployee: null
  };

 case CrudActions.CREATE_EMPLOYEE:
      return {
        employees: [...state.employees, action.payload],
        selectedEmployee: null
  };

 case CrudActions.UPDATE_EMPLOYEE:
      let index = -1;
      // clone employees array with updated employee
  let employees = state.employees.map(
        (employee: Employee, idx: number) => {
        if (employee.id === action.payload.id) {
          index = idx;
          return Object.assign({}, action.payload);
        }
        return employee;
      });

      let selectedEmployee = index >= 0 ? employees[index] : null;
      return {employees, selectedEmployee};

    default:
      return state;
  }
};

如您所见,还有一个 default 开关语句,它只是在提供的操作不匹配任何预定义操作时返回当前状态。

现在,我们可以使用 ngrx/store 模块配置 AppModule。通过导入 StoreModule,应该调用 provideStore 方法并提供我们的 reducer 的名称:

import {StoreModule} from '@ngrx/store';
import {CrudActions} from './redux/crud.actions';
import {crudReducer} from './redux/crud.reducer';

@NgModule({
  imports: [
    ...
    StoreModule.provideStore({crudReducer})
  ],
  providers: [
    ...
    CrudActions
  ],
  ...
})
export class AppModule { }

通常,您也可以提供多个 reducer。这里显示了一个示例:

let rootReducer = {
  reducerOne: reducerOne,
  reducerTwo: reducerTwo, 
  reducerThree: reducerThree,
  ...
}

StoreModule.provideStore(rootReducer);

在内部,@ngrx/store使用combineReducers方法创建一个meta-reducer,该方法使用正确的状态片段调用指定的 reducer。

最后一步是分派操作并选择数据。我们可以将CrudActions注入EmployeeService并为每个 CRUD 操作创建相应的操作。返回值的类型为Observable<Action>

constructor(private http: Http, private crudActions: CrudActions) { }

getEmployees(): Observable<Action> {
  return this.http.get('/fake-backend/employees')
    .map(response => response.json() as Employee[])
    .map(employees => this.crudActions.loadEmployees(employees))
    .catch(EmployeeService.handleError);
}

createEmployee(employee: Employee): Observable<Action> {
  return this.http.post('/fake-backend/employees', employee)
    .map(response => response.json() as Employee)
    .map(createdEmployee => 
      this.crudActions.createEmployee(createdEmployee))
    .catch(EmployeeService.handleError);
}

updateEmployee(employee: Employee): Observable<Action> {
  return this.http.put('/fake-backend/employees', employee)
    .map(() => this.crudActions.updateEmployee(employee))
    .catch(EmployeeService.handleError);
}

deleteEmployee(id: string): Observable<Action> {
  return this.http.delete('/fake-backend/employees/' + id)
    .map(() => this.crudActions.deleteEmployee(id))
    .catch(EmployeeService.handleError);
}

在组件类中,我们通过调用store.dispatch(action)来接收操作并分派它们。操作的分派仅将演示两个 CRUD 操作:加载所有员工和删除一个员工:

ngOnInit(): void {
  ...

  this.employeeService.getEmployees().subscribe(
    action => this.store.dispatch(action),
    error => this.showError(error)
  );
}

remove() {
  this.employeeService.deleteEmployee(this.selectedEmployee.id)
    .finally(() => {
      this.employeeForDialog = null;
    })
    .subscribe((action) => {
        this.store.dispatch(action);
        this.showSuccess('Employee was successfully removed');
      },
      error => this.showError(error)
    );
}

@ngrx/store中选择数据是通过调用store.select()来实现的。select方法期望选择要在视图中显示的状态片段的 reducer 函数的名称。select方法的返回值是Observable,它允许订阅存储的数据。下一个代码片段演示了这样的订阅:

import {Store} from '@ngrx/store';
import {AppStore} from '../redux/app.store';
...

constructor(private store: Store<AppStore>, 
  private employeeService: EmployeeService) { }

ngOnInit(): void {
  this.store.select('crudReducer').subscribe((store: AppStore) => {
    this.employees = store.employees;
    this.selectedEmployee = store.selectedEmployee;
  });
}

生命周期方法ngOnInit是订阅的好地方。

完整的演示应用程序及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter9/redux.

总结

在本章中,您已经学习了更多用于各种用例的 PrimeNG 组件和指令。本章解释了 FileUpload、Draggable、Droppable、Galleria、Defer、BlockUI、ProgressBar 等有趣的功能。您已经看到了使用 DataTable 和模拟后端实现 CRUD 应用程序的实际示例。还演示了使用 ConfirmationDialog 和受保护的路由的最佳实践。阅读完本章后,您将具备必要的知识,能够为接下来的几个 Angular 和 PrimeNG Web 应用程序创建不同的自定义组件。Redux 架构不再是一个陌生的概念。您已经为复杂的 Web 应用程序中有利的状态管理做好了准备。

下一章将介绍使用现代框架进行单元测试和端到端测试。您将学习如何测试和调试 Angular 应用程序。测试驱动开发的技巧也不会缺席。

第十章:创建健壮的应用程序

测试 Web 应用程序是一个重要的过程,可以消除错误。测试确保编写的代码按预期工作,并且功能在后续更改中不会中断。它们还可以帮助您更好地理解您正在处理的复杂代码。有两种主要类型的测试--单元测试端到端e2e)测试。

单元测试是关于测试孤立的代码片段。单元测试通常使用模拟输入,然后断言预期结果是否发生。e2e 方法执行针对在真实浏览器中运行的应用程序的测试。这些测试断言所有代码片段是否正确地相互交互。Angular 应用程序的标准测试框架是 Jasmine 和 Protractor。Jasmine 与 Karma 一起用于单元测试,Karma 是 JavaScript 的测试运行器。Protractor 是一个 e2e 测试框架。本章简要描述了两种测试类型。您将学习如何高效地设置和使用测试框架。测试和调试技巧为本书画上了句号。

在本章中,我们将涵盖以下主题:

  • 使用 Jasmine 和 Karma 设置单元测试

  • 组件和服务的单元测试

  • 如何加快单元测试的技巧

  • 使用 Protractor 设置 e2e 测试环境

  • 一览自动化 UI 测试

  • 使用 Augury 和 ng.probe 探索 PrimeNG 应用程序

使用 Jasmine 和 Karma 设置单元测试

本节将简要概述 Jasmine (jasmine.github.io) 和 Karma (karma-runner.github.io)。在编写具体的单元测试之前,我们将建立一个测试环境。为此,将扩展在第一章的使用 Webpack 设置 PrimeNG 项目部分介绍的 Webpack 种子项目。

Jasmine 简介

Jasmine 是一个没有依赖的 JavaScript 测试框架。使用npm,您可以按如下方式安装它:

npm install jasmine-core --save-dev

您还需要安装 Jasmine 类型定义文件。否则,TypeScript 编译器将不知道 Jasmine 类型。

npm install @types/jasmine --save-dev

Jasmine 有四个主要概念:

  • 规格: 在 Jasmine 术语中,单元测试称为规格。it(string, function)函数指定一个测试。它接受一个标题和一个包含一个或多个期望的函数。

  • 套件: 规范被包裹在套件中。describe(string, function)函数描述了一个测试套件。它接受一个标题和一个包含一个或多个规范的函数。套件也可以包含其他嵌套套件。

  • 期望: 这些是使用expect(actual)函数指定的断言。该函数接受一个参数--actual值。

  • 匹配器: 断言后面跟着匹配器。Jasmine 有很多匹配器,比如toBe(expected)toEqual(expected)toBeLessThan(expected)等等。expected参数是期望的值。例如,expect(2 + 3).toBeLessThan(6)。匹配器在actualexpected值之间实现布尔比较。如果匹配器返回true,则规范通过;否则,会抛出错误。任何匹配器都可以用not否定;例如,expect(array).not.toContain(member)

完整的匹配器列表可在 GitHub 上找到:github.com/JamieMason/Jasmine-Matchers

Jasmine 的一个简单测试示例如下:

describe("Test matchers:", function() {
  it("Compare two values with 'toBe' matcher", function() {
    var a = 5;
    var b = 2 + 3;

    expect(a).toBe(b);
    expect(a).not.toBe(null);
  });
});

it()describe()函数中的标题用于文档编写。测试应该是自描述的,这样其他开发人员就可以更好地理解测试的作用。

Jasmine 有一些设置和拆卸函数:

  • beforeAll(): 在套件运行之前执行一些代码。

  • beforeEach(): 在每个规范运行之前执行一些代码。

  • afterAll(): 在套件完成后执行一些代码。

  • afterEach(): 在每个规范完成后执行一些代码。

这里演示了使用beforeEach()afterAll()的示例:

describe("Usage of beforeEach and afterAll", function() {
  var foo = 0;

  beforeEach(function() {
    foo += 1;
  });

  afterAll(function() {
    foo = 0;
  });

  it("Check if foo == 1", function() {
    expect(foo).toEqual(1);
  });

  it("Check if foo == 2", function() {
    expect(foo).toEqual(2);
  });
});

使用 Webpack 和 Karma 进行测试设置

有两个运行器来执行测试:

  • Jasmine 独立发布版的 HTML 运行器(github.com/jasmine/jasmine/releases)**: 下载 ZIP 文件,解压缩,并在任何文本编辑器中打开SpecRunner.html文件。这个 HTML 文件包含一些加载测试框架的基本代码。您需要添加常规的 Angular 依赖项、一个 Angular 测试库和一个 SystemJS 加载器,加载.spec文件。之后,您可以在 Web 浏览器中打开 HTML 文件查看测试结果。

  • 命令行运行器 Karma: Karma 可以在不同的浏览器中运行测试,并使用各种报告器报告可能的错误。该运行器可以集成到构建过程中,以便单元测试作为构建的一部分自动执行。

在本书中,我们只关注 Karma 运行器。基于 Webpack 的项目设置需要以下 Karma 依赖:

"devDependencies": {
  ...
  "karma": "~1.7.0",
  "karma-chrome-launcher": "~2.1.1",
  "karma-jasmine": "~1.1.0",
  "karma-jasmine-matchers": "~3.7.0",
  "karma-mocha-reporter": "~2.2.3",
  "karma-phantomjs-launcher": "~1.0.4",
  "karma-sourcemap-loader": "~0.3.7",
  "karma-webpack": "~2.0.3",
  "phantomjs-prebuilt": "~2.1.14"
}

细心的读者会注意到,我们想要针对 Google Chrome 和 PhantomJS(phantomjs.org)运行测试--一个无头浏览器,非常适合测试 Web 应用程序。

完整的项目及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter10/unit-testing

使用 Webpack 和 Karma 进行测试设置需要三个配置文件。首先,我们需要一个 Karma 配置文件:karma.config.js。这个文件将告诉 Karma 测试的位置在哪里,使用哪个浏览器来执行测试,使用哪种报告机制等等。在 GitHub 上的项目中,这个文件的内容如下:

let webpackConfig = require('./webpack.test.js');

module.exports = config => { 
  config.set({
    autoWatch: false,
    singleRun: true,
    browsers: ['Chrome', 'PhantomJS'],
    basePath: '.',
    files: ['spec-bundle.js'],
    exclude: [],
    frameworks: ['jasmine', 'jasmine-matchers'],
    logLevel: config.LOG_INFO,
    phantomJsLauncher: {exitOnResourceError: true},
    port: 9876,
    colors: true,
    preprocessors: {
      'spec-bundle.js': ['webpack', 'sourcemap']
    },
    reporters: ['mocha'],
    webpack: webpackConfig,
    webpackServer: {noInfo: true}
  });
};

这里应该提到两个重要的点。有一个专门的 Webpack 配置用于测试,位于webpack.test.js文件中。它的内容很简单:

var ContextReplacementPlugin = require("webpack/lib/ContextReplacementPlugin");

module.exports = {
  devtool: 'inline-source-map',
  resolve: {extensions: ['.ts', '.js', '.json']},
  module: {
    rules: [
      {test: /\.ts$/, loaders: ['awesome-typescript-loader', 
        'angular2-template-loader']},
      {test: /\.json$/, loader: 'json-loader'},
      {test: /\.(css|html)$/, loader: 'raw-loader'}
    ]
  },
  plugins: [
    new ContextReplacementPlugin(
      /angular(\\|\/)core(\\|\/)@angular/,
      path.resolve(__dirname, '../src')
    )
  ]
};

webpack.test.js没有指定要测试的文件的入口点。测试文件的位置和文件扩展名在spec-bundle.js中定义。这是 Webpack 处理的另一个重要文件。它加载测试所需的 Angular 模块并初始化测试环境。最后,所有测试文件都会在浏览器中加载。spec-bundle.js文件的内容如下:

require('core-js/es6');
require('core-js/es7/reflect');
require('zone.js/dist/zone');
require('zone.js/dist/long-stack-trace-zone');
require('zone.js/dist/proxy');
require('zone.js/dist/sync-test');
require('zone.js/dist/jasmine-patch');
require('zone.js/dist/async-test');
require('zone.js/dist/fake-async-test');
require('rxjs/Rx');

const coreTesting = require('@angular/core/testing');
const browserTesting = require('@angular/platform-browser-dynamic/testing');

// Initialize the test environment
coreTesting.TestBed.resetTestEnvironment();
coreTesting.TestBed.initTestEnvironment(
  browserTesting.BrowserDynamicTestingModule,
  browserTesting.platformBrowserDynamicTesting()
);

// Let the browser show a full stack trace when an error happens
Error.stackTraceLimit = Infinity;
// Let's set the timeout for the async function calls to 3 sec. 
// (default is 5 sec.)
jasmine.DEFAULT_TIMEOUT_INTERVAL = 3000;

// Find all files with .spec.ts extensions
const context = require.context('../src/', true, /\.spec\.ts$/);

// For each file, call the context function that will require 
//the file and load it up here.
context.keys().forEach(context);

约定是将测试文件命名为要测试的文件相同,但在文件扩展名之前加上后缀.spec。例如,header.component.spec.tslanguage.service.spec.ts。最佳实践也是将每个测试文件保存在与相应受测试文件相同的目录中。

package.json中,我们可以配置三个方便的命令。

"scripts": {
  ...
  "test": "karma start ./config/karma.conf.js",
  "test:headless": "karma start ./config/karma.conf.js 
    --browsers PhantomJS",
  "test:chrome": "karma start ./config/karma.conf.js 
    --browsers Chrome"
}

现在,我们准备使用三个命令之一来运行我们的测试。当我们执行npm run test时,Karma 将打开和关闭每个配置的浏览器并打印测试结果。

当使用 Angular CLI 时,它会为我们处理配置。您不需要编写任何配置文件,只需在项目根目录中键入ng test即可快速运行所有测试。此命令会监视更改并自动重新运行测试。

组件和服务的单元测试

在本节中,我们将介绍 Angular 测试工具,并展示如何测试组件和服务。由于本书篇幅有限,不会解释指令、管道、路由器等的测试。

Angular 测试工具

正如您在前一节中看到的,Angular 带有一个测试库@angular/core/testing,它提供了TestBed辅助类和许多其他实用程序。TestBed帮助我们为测试设置依赖项--模块、组件、提供者等。您可以调用TestBed.configureTestingModule()并传递与@NgModule中使用的相同的元数据配置对象。configureTestingModule()函数应该在beforeEach()设置函数中调用。

测试库中的另一个有用函数称为inject()。它允许您将指定的对象注入到测试中。以下代码片段提供了一个示例:

describe('MyService', () => {
  let service;

  beforeEach(() => TestBed.configureTestingModule({
    providers: [MyService]
  }));

  beforeEach(inject([MyService], s => {
    service = s;
  }));

  it('should return the city Bern', () => {
    expect(service.getCities()).toContain('Bern');
  });
});

下一个有用的函数是async()函数。它可用于异步操作,因为它在测试中不会完成,直到测试中的所有异步操作完成或发生指定的超时。async()函数包装了it()函数的第二个参数:

it('do something', async(() => {
  // make asynchronous operation here, e.g. call a REST service
}), 3000));

超时参数是可选的。还可以结合inject()调用async()

it('do something', async(inject([SomeService], s => {
  ...
})));

请注意,如果您更改了某个组件的属性值,并且该属性通过ngModel指令绑定到视图,您也必须在async()中执行此操作。原因是:ngModel会异步更新值。我们将在本节中开发一个适当的示例。

Angular 测试工具中的fakeAsync()函数类似于async(),但它通过在特殊的fakeAsync 测试区域中运行测试主体来实现线性编码风格。fakeAsync()方法与tick()函数一起使用,后者模拟时间的流逝,直到所有挂起的异步活动都已完成。不再有嵌套的then(...)块;测试看起来是同步的:

changeSomethingAsync1();
...
tick();
expect(something).toBeDefined();

changeSomethingAsync2();
...
tick();
expect(something).toBeNull();

fakeAsync()有一个限制--您不能在fakeAsync()中进行 XHR 调用。

测试一个组件

我们想要测试一个名为SectionComponent的组件,它只有一个属性username。该组件具有以下标记:

<label  for="username">Username:</label>
<input  id="username" name="username" type="text" pInputText 
       [(ngModel)]="username"/>

在测试文件section.component.spec.ts中,我们将把值James Bond赋给属性username,然后检查该值是否出现在视图中。测试代码的完整列表如下:

import {TestBed, async, ComponentFixture} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {DebugElement} from '@angular/core';
import {SectionComponent} from './section.component';
import {FormsModule} from '@angular/forms';

describe('Component: SectionComponent', () => {
 let fixture: ComponentFixture<SectionComponent>;
  let sectionComponent: SectionComponent;
  let element: any;
  let debugElement: DebugElement;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [FormsModule],
      declarations: [SectionComponent]
    });

    fixture = TestBed.createComponent(SectionComponent);
    sectionComponent = fixture.componentInstance;
    element = fixture.nativeElement;
    debugElement = fixture.debugElement;
  });

 afterEach(() => {
 if (fixture) {fixture.destroy();}
  });
   it('should render `James Bond`', async(() => {
    sectionComponent.username = 'James Bond';

    // trigger change detection
  fixture.detectChanges();

    // wait until fixture is stable and check then the name
  fixture.whenStable().then(() => {
      // first approach shows one possible way to check the result
  expect(element.querySelector('input[name=username]').value)
        .toBe('James Bond');
      // second approach shows another possible way to check the result
  expect(debugElement.query(By.css('input[name=username]'))
        .nativeElement.value).toBe('James Bond');
    });
  }));
});

让我们解释一下代码。首先,我们需要用TestBed配置测试模块。这通常在beforeEach()中完成。TestBed类有一个静态方法createComponent(component),我们用它来创建ComponentFixture——在测试环境中包装组件实例的包装器。fixture 提供了对组件实例本身、原生根元素以及DebugElement的访问,后者是对该组件的根元素的包装器。此外,ComponentFixture还有许多其他有用的方法:

class ComponentFixture<T> {
  componentInstance: T;
  nativeElement: any;
  debugElement: DebugElement;
  detectChanges(): void;
 whenStable(): Promise<any>;
  ...
}

最重要的方法是detectChanges()whenStable()。第一个触发组件的变更检测周期。这是必要的,以便将更改传播到 UI。第二个返回Promise,可以在所有异步调用或异步变更检测结束时用于恢复测试。我们使用了两种不同的 API 来检查Promise解析后的预期结果。

Jasmine 有一个间谍的概念。间谍模拟任何对象或函数,并跟踪对它及其所有参数的调用。如果间谍被调用,toHaveBeenCalled匹配器将返回true。下一个代码片段在showDetails方法上创建了一个间谍。经过一些交互后,我们可以验证该方法是否被调用:

const spy = spyOn(someComponent, 'showDetails');

// do some interactions
...
fixture.detectChanges();

fixture.whenStable().then(() => {
  expect(spy).toHaveBeenCalled();
});

我们还可以验证方法是否使用特定参数进行了调用,调用了多少次等等。有关更多详细信息,请参阅 Jasmine 文档。

测试一个服务

下一个例子概述了如何测试服务。我们想要测试一个从远程后端返回一些国家的服务:

@Injectable()
export class CountryService {
  constructor(private http: Http) { }

  getCountries(): Observable<Country[]> {
    return this.http.get('/assets/data/countries.json')
      .map(response => response.json().data as Country[]);
  }
}

Country对象具有以下形状:

interface Country {
  name: any;
  dial_code?: any;
  code?: any;
}

我们不希望在测试期间进行 HTTP 调用。为了实现这一点,我们必须用MockBackend替换XHRBackendMockBackend允许我们捕获传出的 HTTP 请求并模拟传入的响应。我们可以按照自己的意愿定义一个响应,然后将服务的结果与我们的期望进行比较。下一个代码片段展示了如何构建一个模拟响应,所以当我们最终调用我们的服务时,它会得到预定义的国家数组:

import {TestBed, inject} from '@angular/core/testing';
import {HttpModule, XHRBackend, Response, ResponseOptions} 
 from '@angular/http';
import {MockBackend, MockConnection} from '@angular/http/testing';
import {CountryService} from './country.service';
import Country from './country';

describe('CountryService (MockBackend)', () => {
 let mockbackend: MockBackend, service: CountryService;
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpModule],
      providers: [CountryService, MockBackend,
        {provide: XHRBackend, useClass: MockBackend}
      ]
    })
  });

  beforeEach(inject([CountryService, MockBackend],
    (cs: CountryService, mb: MockBackend) => {
    service = cs;
    mockbackend = mb;
  }));
   it('should return mocked response', () => {
    let israel: Country = {'name': 'Israel', 
 'dial_code': '+972', 'code': 'IL'};
    let angola: Country = {'name': 'Angola', 
 'dial_code': '+244', 'code': 'AO'};
    let response = [israel, angola];

    mockbackend.connections.subscribe((connection: MockConnection) => {
      connection.mockRespond(new Response(new ResponseOptions({
        status: 200, body: JSON.stringify(response)
      })));
    });

    service.getCountries().subscribe(countries => {
      expect(countries.length).toBe(2);
      expect(countries).toContain(israel);
      expect(countries).toContain(angola);
    });
  });
});

请注意,这里我们不需要 async() 函数,因为 MockBackend 的行为是同步的。现在,当所有测试都成功时,您将看到以下输出:

所演示的服务类测试不是孤立的单元测试。孤立的单元测试探索被测试类的内部逻辑,并且不需要使用 Angular 测试工具。您不需要准备测试模块,调用 inject()async() 等。使用 new 创建类的测试实例,并为构造函数参数提供模拟、间谍或虚假对象。通常,服务和管道是孤立单元测试的良好候选对象。阅读官方的 Angular 测试指南以了解更多细节 (angular.io/docs/ts/latest/guide/testing.html)。

完整的项目及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter10/unit-testing.

加速单元测试的提示

在一个真实的网络应用程序中,您可能有很多测试文件。捆绑和运行所有测试文件可能需要一段时间。Karma 的引导过程也需要一段时间。如果您总是需要运行数百甚至更多的测试来测试单个文件中的小更改,这对于快速软件开发来说是不令人满意的。如果您想要缩小测试范围,只测试您编写测试的文件,或者一组指定的文件,而不重新启动 Karma。如何做到这一点?

这就是 karma-webpack-grep (www.npmjs.com/package/karma-webpack-grep) 可以帮助您的情况。它允许限制由 karma-webpack 捆绑的文件。首先,安装它:

npm install karma-webpack-grep --save-dev

之后,我们必须扩展 karma.conf.js 文件。将新的 karma-webpack-grep 插件放入所有 Webpack 插件的数组中。其他一切保持不变:

let grep = require('karma-webpack-grep');

module.exports = config => {
  webpackConfig.plugins = (webpackConfig.plugins || []).concat(grep({
    grep: config.grep,
    basePath: '.',
    testContext: '../src/'
  }));

  config.set({
    // the same settings as before
    ...
  });
};

请注意,testContext 选项与 require.context(...) 中传递的内容完全相同(请参阅 spec-bundle.js)。但是,config.grep 是从哪里来的?Karma 解析命令行参数。这意味着,如果您执行以下命令:

karma start ./config/karma.conf.js --grep some/path

config.grep 将设置为 some/path。让我们扩展 npm 脚本:

"scripts": {
  ...
  "test:headless:grep": "karma start ./config/karma.conf.js
    --browsers PhantomJS --autoWatch true --singleRun false --grep",
  "test:chrome:grep": "karma start ./config/karma.conf.js
    --browsers Chrome --autoWatch true --singleRun false --grep"
}

现在,您可以在观察模式下运行具体的测试。对于 Chrome 浏览器,它看起来如下:

npm run test:chrome:grep -- app/section/section.component.spec.ts
npm run test:chrome:grep -- app/section/service/country.service.spec.ts

对于 PhantomJS,测试以npm test test:headless:grep开头。测试结果看起来像下面这样(这里是针对CountryService):

START:
  CountryService (MockBackend)
    V should return mocked response

Finished in 0.014 secs / 0.331 secs @ 14:33:58 GMT+0200

SUMMARY:
V 1 test completed

Karma 运行器继续运行并监视文件更改。每次文件更改时,测试结果都会非常快地显示出来。还可以观察并执行特定文件夹中的测试文件。为此,您可以简单地将此文件夹传递给npm脚本。例如:

npm run test:chrome:grep -- app/section

将在src/app/section下的所有测试都被监视和执行。只需专注于编写代码:

完整的项目及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter10/unit-testing

使用 Protractor 设置端到端测试环境

Protractor(www.protractortest.org)是一个开源的端到端测试自动化框架,专门为 Angular web 应用程序设计。Protractor 是一个基于 WebDriverJS 的 Node.js 工具,它是 W3C WebDriver API 的官方实现,用于与浏览器交互。

Protractor 有许多优点。您不再需要为挂起的任务在测试中添加waitssleeps。Protractor 可以在网页完成异步任务(例如,AJAX 更新)时自动执行测试中的下一步。该框架还支持 Angular 的定位策略,允许您轻松地通过绑定、模型等找到特定于 Angular 的元素。本节简要介绍了 Protractor,包括设置和特定的测试构造。

安装和配置 Protractor

首选方法是使用以下命令全局安装 Protractor:

npm install protractor -g

Protractor 将自动下载Selenium 独立服务器和所有浏览器驱动程序。

Selenium 独立服务器通常在您想要连接到远程机器并对远程机器上的浏览器运行测试时需要。当您将测试分布在多台机器上时,通常与Selenium-Grid一起使用。

执行此命令以更新 Selenium 独立服务器和浏览器驱动程序:

webdriver-manager update

确保您已经像在使用 Jasmine 和 Karma 设置单元测试部分所示的那样在本地安装了jasmine-core包。测试将使用 Jasmine 编写,但您也可以使用 Mocha(mochajs.org)-另一个在 Node.js 上运行的 JavaScript 测试框架。此外,还需要安装一个报告生成工具jasmine-spec-reporter

npm install jasmine-spec-reporter --save-dev

Protractor 配置在protractor.conf.js文件中进行。

var path = require('path');
var SpecReporter = require('jasmine-spec-reporter').SpecReporter;

exports.config = {
  allScriptsTimeout: 11000,
  specs: ['../e2e/**/*.e2e-spec.ts'],
  capabilities: {'browserName': 'chrome'},
  directConnect: true,
  baseUrl: 'http://localhost:3000/',
  framework: 'jasmine',
  jasmineNodeOpts: {
    showColors: true,
    defaultTimeoutInterval: 30000,
    print: function () { }
  },
  beforeLaunch: function () {
    require('ts-node').register({
      project: path.resolve(__dirname, '../e2e')
    });
  },
  onPrepare() {
    jasmine.getEnv().addReporter(new SpecReporter());
  }
};

下面列出了最重要的配置选项的描述:

选项 描述
allScriptsTimeout 浏览器上每个脚本运行的超时时间(以毫秒为单位)。
specs Spec 模式相对于此配置的位置。最佳实践是将所有的 e2e 测试放在e2e文件夹中。子文件夹名称对应页面名称。例如,home页面的测试应该在home子文件夹中。文件名也对应页面名称。我们在我们的 spec 文件中添加.e2e-spec.ts后缀。例如,home 页面的 spec 文件是home.e2e-spec.ts
capabilities 浏览器测试的配置对象。您也可以同时在多个浏览器上运行测试。为此,请使用multiCapabilities选项,该选项需要一个能力数组。
seleniumAddress 使用此选项连接到使用webdriver-manager start启动的运行中的 Selenium 服务器。例如,seleniumAddress: 'http://localhost:4444/wd/hub'。Protractor 将向该服务器发送请求以控制浏览器。您可以在http://localhost:4444/wd/hub上查看有关服务器状态的信息。
directConnect 使用此选项直接连接到 Chrome 或 Firefox(仅支持两种浏览器的直接连接)。在本书中,我们使用directConnect而不是seleniumAddress
baseUrl 被测试应用的基本 URL。
framework 使用的测试框架。通常使用 Jasmine 或 Mocha。
beforeLaunch 一旦配置文件被读取但在任何环境设置之前调用的回调函数。这只会运行一次,在onPrepare之前。在前面的配置中,将执行ts-nodets-node模块负责将 TypeScript 文件转换为 JavaScript 文件。您必须通过npm安装它,如npm install ts-node --save-dev。还要考虑project配置选项,它指向具有特定tsconfig.json文件的文件夹。通常,我们需要不同的 TypeScript 编译器选项来进行 e2e 测试。
onPrepare 一旦 Protractor 准备就绪并可用,但在执行规范之前调用的回调函数。我们可以在那里添加一些报告者。

所有准备工作都已完成。现在,请确保应用程序正在http://localhost:3000/上运行,并从项目根目录运行测试:

protractor ./config/protractor.conf.js

为了方便起见,您可以将此命令配置为npm脚本,并使用npm run e2e运行:

"scripts": {
  ...
  "pree2e": "webdriver-manager update",
  "e2e": "protractor ./config/protractor.conf.js"
}

webdriver-manager update命令应该首先作为预挂钩运行。这就是为什么我们需要在scripts部分中有"pree2e"的原因。

可以将 e2e 测试分成不同的套件,并分别运行与套件相关的测试。您可以在配置文件的suites部分完成这项任务。例如,让我们定义两个套件,homepagepayment

suites: {
  homepage: '../e2e/homepage/**/*.e2e-spec.ts',
  payment: '../e2e/payment/**/*.e2e-spec.ts'
}

以下命令将仅运行与主页相关的测试:

protractor ./config/protractor.conf.js --suite homepage

在使用 Angular CLI 时,它会为我们创建配置文件。您可以通过在两个单独的控制台中运行ng serveng e2e来执行 e2e 测试。如果您需要不同的设置、特定的模拟等等用于 e2e 测试,您必须创建一个新的environment.e2e.ts文件,其中包含特定的环境变量,并在.angular-cli.json中的environments下注册它为"e2e": "environments/environment.e2e.ts"。现在,您可以在app.module.ts中导入environment.ts,检查环境变量,并在需要时执行自定义逻辑;例如,提供模拟等等。为了使其工作,应用程序应该以ng serve --env=e2e启动。

一览自动化 UI 测试

本节描述了 Protractor 测试的语法,以及 Page ObjectPage Element 设计模式,这些是最佳的 e2e 测试实践。掌握了这些知识,我们将为 第九章 中介绍的演示应用编写一个完整的 e2e 测试,杂项用例和最佳实践,在 使用受保护的路由显示确认对话框 部分。

浏览器对象、元素和定位器

浏览器对象是围绕 WebDriver 实例全局创建的包装器。它用于导航和页面范围的信息。使用 browser.get(),您可以导航到一个页面,然后检查页面的标题,如下所示:

describe('Google page', function() {
  it('should have a title', function() {
    browser.get('https://www.google.com');
    expect(browser.getTitle()).toEqual('Google');
  });
});

当前 URL 可以通过 browser.getCurrentUrl() 返回。例如:

expect(browser.getCurrentUrl()).toContain('/admin');

Protractor 创建的其他全局对象是 elementbyelement 是一个帮助函数,用于在测试的页面上查找 DOM 元素。它需要一个参数--用于定位元素的 定位器。定位器是使用 by 对象创建的。有各种定位器。我们只会提到其中一些。要获取完整列表,请阅读官方 API 文档 (www.protractortest.org/#/api)。

by.css 选择器使用 CSS 选择器定位元素。例如:

element(by.css('h1'));

by.id 选择器通过其 ID 定位元素。例如:

element(by.id('id'))

by.tagName 选择器定位具有给定标记名称的元素。例如:

element(by.tagName('div'))

请注意,对于 Angular 2+ 应用程序,不支持 by.bindingby.model 定位器。它们仅支持 Angular 1.x。

element 函数返回一个 ElementFinder 对象,可以用于与底层 DOM 元素交互或从中获取信息。以下表格列出了 ElementFinder 的最重要方法:

方法 描述
getText() 返回元素的文本。
click() 在元素上执行点击。
sendKeys(keys) 将传入的字符发送到元素(用于填写表单)。
element(locator) 通过给定的定位器在父元素(此元素)中查找子元素。它将第一个找到的子元素作为 ElementFinder 类型的对象返回。
all(locator) 通过给定的定位器在父元素(此元素)中查找子元素。它将返回一个ElementArrayFinder类型的对象,其中包含所有找到的子元素的数组。ElementArrayFinder对象具有许多有用的方法。例如,count()提供找到的元素数量,get(index: number)在数组中提供指定位置的元素。有关更多信息,请阅读 API 文档。
getId() 获取 DOM 元素的 ID。
isDisplayed() 检查 DOM 元素是否可见。
isEnabled() 检查 DOM 元素是否已启用。

让我们看两个例子,并检查具有样式类info的元素是否显示(可见)如下:

expect(element(by.css('.info')).isDisplayed()).toBe(true);

下一个代码片段检查按钮是否可点击:

expect(element(by.tagName('button')).isEnabled()).toBe(true);

要小心动画。当元素在一些动画之后出现时,等待动画结束是合理的。为了实现这一点,使用browser.wait(),如browser.wait(element(by.id('id')).isDisplayed()).toBe(true)

请注意,对 DOM 元素的访问操作返回的是 promise 而不是元素本身。例如,getText()count()等。Protractor 修补了 Jasmine 的expect()函数,以便自动等待直到 promise 得到解决并且定位的 DOM 元素可访问。之后,将应用匹配器。这很方便,但也有一些情况下,您需要访问已解决 promise 的值。在这些情况下,您可以显式使用 promise 的then()函数。假设您想要输出表格中行的数量。下面的代码片段举例说明了这个想法:

it('should display the correct row count', () => {
  const pcount = element.all(by.css('table tr')).count();
  pcount.then((count) => {
    console.log(`Table has ${count} rows`);
  });
  expect(pcount).toEqual(20);
});

使用页面对象的清洁架构

页面对象封装了网页的行为。在单页面应用程序中,我们有视图。为了方便起见,如果我们说“网页”,我们将指代完整页面和视图。每个网页都有一个页面对象,将页面的逻辑抽象到外部。这意味着与网页的交互被封装在页面对象中。端到端测试操作页面对象。以网店为例:页面对象可以编写为类:ProductPageShoppingCartPagePaymentPage等等。

页面元素(又称HTML 包装器)是网页的另一个细分。它代表一个 HTML 元素,并封装了与该元素交互的逻辑。例如,一个 DatePicker 的 HTML 包装器可以提供 API 方法,如“将日期设置到输入字段中”、“打开日历弹出窗口”和“在日历弹出窗口中选择给定的日期”。HTML 包装器可以是复合的;这意味着它可以由多个小元素组成。例如,产品目录由产品组成,购物车由商品组成,等等。

Martin Fowler(martinfowler.com/bliki/PageObject.html)描述了页面对象和 HTML 包装器作为设计模式。这种架构有很多优势:

  • 测试代码与特定页面代码之间的清晰分离。

  • 在页面上查找元素的by定位器不会透露给外部。页面对象的调用者从不需要关心by定位器。

  • 如果页面发生了一些 UI 更改,您不需要更改相应的测试。我们只需要在一个地方更改代码——在页面对象内部。

  • 页面对象减少了重复代码的数量。如果测试共享相同的场景,您就不需要复制/粘贴代码。只需编写一次,随处共享!

  • 规范文件更易读,更紧凑——一眼看到代码,我们就知道测试的功能。

下一个代码片段演示了一个想象中的登录页面的简单页面对象:

import {browser, element, by, ElementFinder} from 'protractor';
import {promise} from 'selenium-webdriver';

export class LoginPage {
  nameInput: ElementFinder;
  passwordInput: ElementFinder;
  submitButton: ElementFinder;

  constructor() {
    this.nameInput = element(by.css('.name'));
    this.passwordInput = element(by.css('.password'));
    this.submitButton = element(by.css('button[type="submit"]'));
  }

  navigateTo() {
    browser.get('http://www.mywebshop.com');
  };

  login(name: string, password: string) {
    this.nameInput.sendKeys(name);
    this.passwordInput.sendKeys(password);
    this.submitButton.click();
  };

  isUserLoggedIn(): promise.Promise<boolean> {
    return element(by.css('.userProfile')).isDisplayed(); 
  }
}

正如您所看到的,许多步骤被提取到一个方法中。使用LoginPage的测试代码是简洁的:

describe('Web shop user', function() {
  it('should log in successfully', function() {
    let loginPage = new LoginPage();
    loginPage.navigateTo();
    loginPage.login('Max Mustermann', 'mysecret');

    expect(loginPage.isUserLoggedIn()).toBeTruthy();
  });
});

规范文件应该与相应的页面对象分组放在同一个文件夹中。最佳实践是为页面对象文件使用.po.ts后缀。例如,登录页面的页面对象文件称为login.po.ts。对于页面元素文件,我们建议使用.pe.ts后缀;例如,dialog.pe.ts

编写完整的端到端测试

我们将编写端到端的规范,测试 UI 从第九章的显示受保护路由的确认对话框杂项用例和最佳实践。简而言之:第一个视图有一个带有提交按钮的输入字段(用户名)。按钮触发导航到第二个视图。每当用户更改用户名时,将出现一个带有文本“您有未保存的更改。您确定要离开此页面吗?”的确认对话框。用户可以点击“是”或“否”。我们想要编写五个测试用例来验证:

  • 第一页是否在h1标签中显示适当的标题

  • 当没有输入时导航是否发生

  • 当输入存在时,确认对话框是否显示

  • 用户在点击“是”时是否离开当前视图

  • 用户在点击“否”时是否停留在当前视图

在编写规范之前,我们需要一个名为FirstViewPage的页面对象:

import {browser, element, by, ElementFinder} from 'protractor';
import {promise} from 'selenium-webdriver';

export class FirstViewPage {
  nameInput: ElementFinder;
  submitButton: ElementFinder;

  constructor() {
    this.nameInput = element(by.css('input[name="username"]'));
    this.submitButton = element(by.css('button[type="submit"]'));
  }

  navigateTo() {
    browser.get('http://localhost:3000/chapter9/first-view');
  };

  typeUsername(name: string) {
    this.nameInput.sendKeys(name);
  };

  confirm() {
    this.submitButton.click();
  };

  getTitle(): promise.Promise<string> {
    return element(by.css('h1')).getText();
  }
}

页面元素ConfirmDialogPageElement封装了确认对话框的内部结构细节。它提供三种方法来询问对话框的可见性并与“是”和“否”按钮交互:

export class ConfirmDialogPageElement {
  element: ElementFinder;

  constructor(by: By) {
    this.element = element(by);
  }

  isDisplayed(): promise.Promise<boolean> {
    return this.element.isDisplayed();
  }

  confirm() {
    this.clickButton('fa-check');
  }

  cancel() {
    this.clickButton('fa-close');
  }

  private clickButton(icon: string) {
    let button = this.element.$('button .' + 
      icon).element(by.xpath('..'));
    button.click();
  }
}

$()element(by.css())的一个方便的快捷符号。by.xpath('..')定位器允许选择父元素。

规范本身很干净-它们调用FirstViewPageConfirmDialogPageElement的公共 API:

describe('FirstView', () => {
  let page: FirstViewPage;

  beforeEach(() => {
    page = new FirstViewPage();
    page.navigateTo();
  });

  it('should contain proper title', () => { 
    expect(page.getTitle()).toContain('first view');
  });

  it('should change the view when no input exists', () => { 
    page.confirm();
    expect(browser.getCurrentUrl()).not.toMatch(/\/first-view$/);
  });

  it('should display confirmation dialog when input exists', () => {
    page.typeUsername('Admin');
    page.confirm();

    let confirmDialog = new ConfirmDialogPageElement(
        by.css('p-confirmdialog')); 
    expect(confirmDialog.isDisplayed()).toBeTruthy();
  });

  it('should navigate to another view on confirm', () => {
    page.typeUsername('Admin');
    page.confirm();

    let confirmDialog = new ConfirmDialogPageElement(
        by.css('p-confirmdialog'));
    confirmDialog.confirm(); 
    expect(browser.getCurrentUrl()).not.toMatch(/\/first-view$/);
  });

  it('should stay on the same view on cancel', () => {
    page.typeUsername('Admin');
    page.confirm();

    let confirmDialog = new ConfirmDialogPageElement(
        by.css('p-confirmdialog'));
    confirmDialog.cancel(); 
    expect(browser.getCurrentUrl()).toMatch(/\/first-view$/);
  });
});

下一个屏幕截图显示了在 IntelliJ/WebStorm 中的规范报告:

完整的项目及说明可在 GitHub 上找到

github.com/ova2/angular-development-with-primeng/tree/master/chapter10/e2e-testing

使用 Augury 和 ng.probe 探索 PrimeNG 应用程序

Augury 是用于检查 Angular 2+应用程序的 Google Chrome 浏览器扩展(augury.angular.io/)。该工具通过组件树、路由器树、模块依赖关系等可视化应用程序。开发人员立即看到应用程序结构、变更检测和其他有用的特性。他们可以探索几个构建块之间的关系,例如组件、服务、路由、模块、注入器等。Augury 是交互式的。它允许修改应用程序状态并发出事件。

您可以从 Chrome 网络商店安装 Augury:chrome.google.com/webstore/detail/augury/elgalmkoelokbchhkhacckoklkejnhcd

一旦插件成功安装,您将在 Chrome 开发者工具(DevTools)中看到一个新的 Augury 选项卡。打开 DevTools 的快捷键:F12(Windows),Command + Option + I(Mac)。

还有另一种探索 Angular 2+应用程序的方法。在开发模式下,Angular 公开了一个全局的ng.probe函数,该函数接受原生 DOM 元素并返回相应的调试元素。使用调试元素,您可以检查组件、注射器、监听器的当前状态,触发事件等。ng.probe函数可以在浏览器的控制台中访问。

在这一部分,我们将对已知的 CRUD 演示应用程序应用 Augury 和ng.probe,该应用程序来自第九章,杂项用例和最佳实践

Augury 在操作中

Augury 中第一个可见的视图是组件树,显示了加载的组件。在树中选择一个组件会突出显示浏览器中组件的模板。同时,在属性选项卡的右侧呈现有关所选组件的附加信息。让我们选择DataTableCrudComponent

您可以看到所有内部属性、输入和输出。所有属性都是可编辑的。下一张屏幕截图显示了对话框组件的属性。输出属性visibleChange是一个事件发射器。我们可以通过将值设置为true并单击“Emit”按钮来触发事件发射器。作为响应,对话框变得可见:

在属性选项卡旁边是注射器图,显示了组件和服务的依赖关系。如果我们选择 DataTable 组件,则将显示 DataTable 的依赖关系及其祖先链到根注射器:

服务的圆形符号需要澄清。空心红色圆圈表示该服务不是由组件提供的,而是来自依赖树中的祖先。虚线蓝线显示了服务的确切来源。在我们的情况下,所有服务都是由根注入器提供的——即app.module.ts中的主模块声明。填充的红色圆圈表示该服务正在被注入并在同一组件中注册。

路由器树为您提供了应用程序中所有路由的树状视图。您可以探索哪些路由来自应用程序的哪些部分。我们将跳过适当的截图。下一个选项卡 NgModules 列出了应用程序中的所有模块,以及模块的导入、导出、提供者和声明。对 NgModules 的了解可以了解可用模块的复杂性和大小:

使用 ng.probe 进行调试

默认情况下,Angular 在开发模式下运行并构建一个调试元素树——一个树,其结构几乎与呈现的 DOM 相同,但具有 DebugElement 类型的对象。每当在浏览器控制台中调用 ng.probe(element) 时,都会返回相应的调试元素。DebugElement 类扩展了 DebugNode (angular.io/docs/ts/latest/api/core/index/DebugNode-class.html)。

在生产模式下,没有调试元素树可用,也无法使用 ng.probe 调试 Angular 应用程序。生产模式是通过 Angular 包 @angular/core 中的 enableProdMode() 函数启用的。

让我们看看如何使用公开的 DebugElement 的公共方法。首先,我们需要在 DevTools 中选择一个 DOM 元素。这将在变量 $0 中保留对所选 DOM 节点的引用,然后可以从控制台中访问。在 CRUD 应用程序中,我们将选择“添加”按钮,如下截图所示:

现在,我们能够获取按钮所属的组件实例的引用。一旦我们有了该实例,我们就可以与之交互;例如,我们可以更改属性等。让我们重置employees数组:

ng.probe($0).componentInstance.employees = [];

在组件模板中选择任何元素将始终提供相同的组件实例。

有一个问题——所解释的代码没有改变 UI。原因很明显——我们需要手动调用变更检测器。下一行有点复杂,但确实做到了我们需要的事情——运行变更检测周期。结果,员工表变为空:

ng.probe($0).injector.get(ng.coreTokens.ApplicationRef).tick();

injector属性允许访问组件及其父级上的所有提供者。例如,假设存在以下提供者定义:

providers: [{
  provide: 'PaymentEndpoint',
  useValue: 'http://someendpoint.com/v1/payment/'
}]

可以使用以下代码获取值http://someendpoint.com/v1/payment/

let element = document.querySelector('.some');
let endpoint = ng.probe(element).injector.get('PaymentEndpoint');

具有some样式类的 DOM 元素应位于定义提供者可见的组件内部。

有趣的用例之一是触发注册的事件处理程序。在演示应用程序中,我们可以选择“添加”按钮,并按如下方式触发该按钮的点击事件:

ng.probe($0).triggerEventHandler('click');

此命令将打开以下对话框:

您可能还对一个名为ngrev的图形分析工具感兴趣,用于反向工程 Angular 项目(github.com/mgechev/ngrev)。使用此工具,您可以探索应用程序的结构,以及不运行应用程序的情况下不同模块、提供者和指令之间的关系。

总结

在本章结束时,您应该能够为单元测试和端到端测试设置测试环境。本章介绍的事实上标准单元测试框架和运行器分别是 Jasmine 和 Karma。您已经掌握了为大多数常用结构编写单元测试的技能,以及使用 Protractor 框架编写端到端测试的基本技能。

您还可以获得有关创建健壮应用程序的有用提示。karma-webpack-grep插件在开发中启动 Karma 时可以大大提高性能。Augury 工具以及ng.probe可以启用对 Angular 2+应用程序的调试和分析。我们希望本章和整本书能够为您的下一个 Angular 和 PrimeNG 应用程序的无缺开发过程做出贡献。

posted @ 2024-05-18 12:03  绝不原创的飞龙  阅读(72)  评论(0编辑  收藏  举报