十七、Angular 和 RxJS
在前一章中,我们浏览了反应式扩展的核心概念,并学习了 RxJS 中的可观察对象、观察者、订阅和操作符。现在,我们将看看如何在 Angular 中使用反应式扩展。
在写 AngularJS 的时候,反应式扩展还没有出现,但是 promises 出现了。AngularJS 使用了很多 promise 对象,包括 h t t p 、 http、 http、interval 和$timeout 模块。Promise 对象可以用来表示异步结果:成功(返回值)或失败(返回错误)。Promise 对象用于与服务器和许多其他对象的 HTTP 通信。图 17-1 给出了一个例子。
图 17-1
Promise objects in AngularJS
在 Angular,承诺正在消失,取而代之的是可观的。不过,它们并没有完全消失。
可观察到的东西比承诺有一些优势:
- Promises 只发出一个值/错误。随着时间的推移,可观测量可以发出多个值。例如,使用 observable,您可以在一段时间内监听 web 套接字上的事件。有承诺只能听一次。
- 您可以使用带有观察点的运算符进行映射、过滤等操作。
- 你可以取消可观测量。
可观测量和 Angular
Angular 在 DOM 事件和 HTTP 服务中使用异步数据流的可观察对象。在监听 DOM(文档对象模型)事件的过程中,您可以观察到用户在用户界面中正在做什么的稳定数据流,比如击键、鼠标事件等等。对于 Http 服务,您可以监听服务器响应,打开连接并响应传入的数据。
Observables 和 DOM 事件:示例
DOM 是表示 HTML 文档中的对象并与之交互的一种方式。文档节点被组织在一个称为 DOM 树的结构中,树中的对象使用对象上的方法进行寻址和操作。
Angular DOM 事件是可以观察到的。为了使用 DOM 事件,我们将使用模块 Rx。DOM(RxJS 的 HTML DOM 绑定)到 rx.angular。
您可以过滤事件,并将观看多个不同的事件和在一个地方观察结合起来。
这个例子检测用户在 5 秒钟内没有做任何事情。当这种情况发生时,我们在组件的显示中添加一行“idle”,如图 17-2 所示。这将是示例 rxjs-和-angular-ex100。
图 17-2
Displaying idle users
让我们来看看这个例子:
-
使用 CLI 构建应用:使用以下命令:
ng new rxjs-and-angular-ex100 --inline-template --inline-style
-
开始
ng serve
:使用以下代码:cd rxjs-and-angular-ex100 ng serve
-
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”
-
编辑类:编辑 app.component.ts,修改为:
import { Component } from '@angular/core'; import * as Rx from 'rxjs'; @Component({ selector: 'app-root', template: ` Search: <input type="text"> <div *ngFor="let log of _logs">Search: {{log}}</div> `, styles: [] }) export class AppComponent { _logs: Array<string> = []; constructor(){ const observable: Rx.Observable<any> = Rx.Observable.merge( Rx.Observable.fromEvent(document,'keydown'), Rx.Observable.fromEvent(document,'click'), Rx.Observable.fromEvent(document,'mousemove'), Rx.Observable.fromEvent(document,'scroll'), Rx.Observable.fromEvent(document,'touchstart') ); const idleEventObservable = observable.bufferTime(5000) .filter(function(arr) { return arr.length == 0; }) .subscribe(idleEvent => this._logs.push('idle')); } }
你的应用应该工作在本地主机:4200。请注意,在构造函数中,我们将来自文档事件keydown
、click
、mousemove
、scroll
和touchstart
的排放合并为一个可观察值。我们将其缓冲为每 5 秒一次,并过滤掉这段时间内发生的事件。然后我们订阅结果,当结果出现时,我们添加一个“空闲”日志,显示在组件中。
可观察对象和 HTTP 服务
$http 和 http 模块
AngularJS 有自己的 Http 模块。$http 服务是一个核心的 AngularJS 服务,它通过浏览器的XMLHttpRequest
对象或 JSONP 促进了与远程 http 服务器的通信。
Angular 2 & 4 Http 模块(@angular/http)类似于 Angular 第一版中的 Http 模块,除了它使用了反应式扩展——换句话说,就是 observables。反应式扩展带来了很多好处,提供了前面章节中提到的所有操作符。
当 Angular 5 发布时,它包含了一个新的 httpClient 模块(@angular/common/http)来取代之前的 Http 模块。您仍然可以使用旧的 Http 模块(@angular/http),但它已被弃用,并将在未来的版本中被删除。
下一章将介绍新的 Angular HttpClient 模块。
摘要
本章非常短,但您现在应该了解以下内容:
- Angular 使用 observables 来处理 DOM 事件和 HTTP 服务调用的结果(调用服务器上的 HTTP 服务并接收结果)。
- 可观测量使用户能够使用 RxJS 处理数据流。例如,您可以发出一个 HTTP 调用来获取一些数据,并使用 RxJS
map
操作符来转换结果。
我使用 RxJs 处理过几次 DOM 事件,但是我经常在 HTTP 服务中使用 RxJs 操作符。我将在下一章介绍 HTTP 服务以及如何在 RxJs 中使用它们。
十八、HTTP 和 HttpClient 模块
99%的 Angular 项目涉及客户端(浏览器)和一些远程服务器之间的通信。通常这是通过 HTTP 完成的。因此,了解 HTTP 通信的工作原理以及如何为其编写代码非常重要。这就是本章的内容。
超文本传输协议(HTTP)旨在实现客户端和服务器之间的通信。HTTP 是客户端和服务器之间的请求-响应协议。我们将在本章中更详细地讨论这一点。
HTTP 方法已经存在很长时间了(在 AJAX 和不同类型的 web 应用之前)。HTTP 方法可以用于传统的服务器端 web 应用,也可以用于客户端 AJAX web 应用。
每当客户端使用 HTTP 与 web 服务器通信时,它都会包含有关请求方法的信息。该方法描述了客户端希望服务器做什么——请求的意图。最常用的方法是GET
和POST
。GET
方法用于从服务器请求数据。POST
方法用于向服务器发送数据,以便保存或更新数据。
最常用的 HTTP 方法如下:
POST
GET
PUT
PATCH
DELETE
HTTP 头允许客户机和服务器在请求或响应中传递附加信息。请求头由其名称组成,名称不区分大小写,后跟一个冒号(😃,再后跟其值(没有换行符)。
如果您在浏览器中使用开发工具,您可以看到网络通信,包括 HTTP 调用。如果您使用 web 浏览器的开发工具来检查 HTTP 请求,您将看到向服务器发出的请求和返回的请求。
图 18-1 和 18-2 显示了 HTTP 请求和响应头。
图 18-2
HTTP response headers
图 18-1
HTTP request headers
Http 正文
http 主体允许客户端和服务器在请求或响应的报头后传递附加信息。Http 主体并不总是必需的,因为信息主体并不总是必需的。例如,Http“get”请求不需要在正文中包含信息——所有信息都已经包含在报头中了。
以下是服务器响应的 http 正文示例:
用 HTTP 传递信息
从浏览器向服务器传递信息有多种方式。服务器通常在正文中返回信息,尽管它可以通过在 HTTP 头中返回数据来传递信息。
查询参数
Angular Http 客户端允许您使用查询参数将 URL 中的信息传递给服务器。比如 http://localhost:4200/sock js-node/info?t=1498649243238。
某些字符不允许作为 URL 的一部分(例如空格),而其他字符在 URL 中可能有特殊的含义。为了解决这个问题,URL 语法允许对参数进行编码,以确保 URL 有效。例如,以下 URL 中大西洋和城市之间的空格字符编码为%20: https://trailapi-trailapi.p.mashape.com/?q[city_cont]=Atlantic%20City
。
当使用 JavaScript 方法encodeURIComponent
构建带有字符串连接的 URL 时,可以执行这种编码。如果您使用一个有 Angular 的对象(比如URLSearchParams
)来构建查询参数字符串,它会自动为您完成这项工作。
在浏览器中导航时,用户可以在地址栏中看到查询参数。但是,当使用 Angular Http 客户端执行 AJAX 请求时,它们是不可见的。
查询参数不能用来传递像请求体那样多的信息。
矩阵参数
Angular Http 客户端允许您使用矩阵参数在 URL 中将信息传递给服务器,例如 Http://localhost:4200/sock js-node/info;t=1498649243238。矩阵参数类似于查询字符串,但使用不同的模式。他们的行为也不同,因为没有?,它们可以被缓存。此外,矩阵参数可以有多个值。通过指定包含矩阵参数的 URL,可以在 Angular 中使用矩阵参数。但是,Angular 目前没有任何内置对象来创建带有矩阵参数的 URL。
矩阵参数不能用来传递像请求体那样多的信息。
路径参数
Angular Http 客户端允许您使用路径参数将信息传递到 URL 中的服务器,例如 Http://localhost:4200/API/badges/9243238。
在请求正文中传递数据
在过去,HTML 表单(带有form
标签和input
字段)是向服务器发送数据的最佳方式。用户将填写一个表单并点击 Submit,数据将在请求体中被发送(使用 HTTP POST
方法)到服务器。
现在 Angular Http 客户端允许您以编程方式做同样的事情:使用 Http 客户端的POST
方法在请求体中将信息传递给服务器。
与使用查询或矩阵参数在 URL 中传递数据相比,在请求正文中可以传递更多的数据。
休息
RESTful 应用是一个服务器应用,它将其状态和功能公开为一组客户机(浏览器)可以操作的资源,并且符合一组特定的原则。资源的例子可能是客户列表或他们的订单。
所有资源都是唯一可寻址的,通常通过 URIs,尽管也可以使用其他寻址方式。例如,您可以使用 orders/23 来访问订单编号 23,或者使用 orders/24 来访问订单编号 24。
所有资源都可以通过一组受约束的众所周知的动作来操作,通常是 CRUD(创建、读取、更新、删除),最常见的是通过 HTTP 方法POST
、GET
、PUT
和DELETE
来表示。有时只使用这些 HTML 方法中的一部分,而不是全部。例如,您可以使用一个 HTTP DELETE
to orders/23 来删除该订单。
所有资源的数据都是通过一定数量的众所周知的表示形式进行传输的,通常是 HTML、XML 或 JSON。JSON 是最常见的。
数据
JSON 代表 JavaScript 对象符号。这是一种用于在客户端和服务器之间双向传递数据的数据格式。JSON 与 JavaScript 语言使用的数据格式相同。它使用逗号来分隔项目,使用冒号来分隔属性名称和该属性的数据。它使用不同类型的括号来表示对象和数组。
下面是传递包含数据的对象的 JSON。注意如何使用{
和}
括号来表示对象的开始和结束:
{ "name":"John", "age":31, "city":"New York" }
这里是 JSON 传递一个数组。注意如何使用[
和]
括号来表示数组的开始和结束:
[ "Ford", "BMW", "Fiat"]
这是 JSON,用于传递对象数组。请注意括号是如何组合起来创建一个具有两个属性的cars
对象的:Nissan
和Ford
。每个属性都有一系列模型:
{
"cars": {
"Nissan": [
{"model":"Sentra", "doors":4},
{"model":"Maxima", "doors":4}
],
"Ford": [
{"model":"Taurus", "doors":4},
{"model":"Escort", "doors":4}
]
}
}
Angular Http 客户端
Angular Http client 是一个服务,您可以将它注入到您的类中,以执行与服务器的 Http 通信。这项服务可以通过新的 Angular 5 Http 客户端模块@angular/common/http
获得,它取代了旧的 Angular 4 Http 模块@angular/common/http
。您需要修改您的模块类(项目的模块类)来导入这个模块:
@NgModule({
imports: [
...
HttpClientModule,
...
],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
您可以通过以下方式将 Angular Http 服务直接注入到您的组件中:
@Injectable()
class CustomerComponent {
...
constructor(private http: HttpClient) {
...
}
}
这对原型设计来说很好,但从长远来看对代码的可维护性来说是不可取的。实际上,您不应该在服务类之外直接使用 HttpClient 进行数据访问。相反,您应该编写使用 Http 客户端的服务类,然后将这些类注入到您的代码中需要数据访问的地方。如果你查看 angular.io 上的官方 Angular 文档,你会看到以下内容:这是一条黄金法则:总是将数据访问委托给支持服务类。
下面是一个使用 HttpClient 的服务类的示例:
@Injectable()
class CustomerCommunicationService {
...
constructor(private http: HttpClient) {
...
}
}
class CustomerComponent {
...
constructor(private http: CustomerCommunicationService) {
...
// perform data access
}
}
无商标消费品
在 Angular 5 中,新的 HttpClientModule 允许我们在调用 HTTP 请求时使用泛型。泛型使我们能够告诉 Angular 我们期望从 HTTP 请求中收到的响应的类型。响应类型可以是“任何”(允许任何类型的响应)、变量类型(例如字符串)、类或接口。例如,下面的代码执行一个 http“get ”,将预期的响应指定为一个语言对象数组:
this._http.get<Array<Language>>('https://languagetool.org/api/v2/languages');
这使得 Angular 能够为我们解析响应,这样我们就不必这么做了。不再需要调用 JSON.parse 来将响应字符串转换为对象。
异步操作
在 JavaScript 中,发出 HTTP 请求是一个异步操作。它向 API 发送 HTTP 请求,在继续下一行代码之前不等待响应。当 API 在几毫秒、几秒或几分钟后做出响应时,我们会得到通知,并可以开始处理响应。
在 Angular 中,有两种方法来处理这些异步操作:我们可以使用承诺或可观察值(在几章前已经讨论过)。
通常我们调用我们的支持服务类,它们返回异步结果,我们在组件中处理。
请求选项
很快我将介绍您可以进行的每种类型的 HTTP 调用,但是首先让我们讨论一下请求选项。当您调用与服务器的 HTTP 通信时,您有许多配置通信的方法。你应该使用什么标题?您应该从服务器接受什么媒体?您应该向服务器传递什么凭证?您在名为RequestOptionsArgs
的 Angular 对象中设置这些选项,并将其作为参数传递给 Angular Http 客户端方法调用。
这里有一个打GET
电话时使用RequestOptionsArg
的例子。请注意如何使用该对象来指定 URL、HTTP 方法、参数、身份验证令牌和主体:
var basicOptions:RequestOptionsArgs = {
url: 'bankInfo',
method: RequestMethods.Get,
params: {
accountNumber: accountNumber
},
headers: new HttpHeaders().set('Authentication': authenticationStr),
body: null
};
HTTP GET 方法:示例
GET
方法非常常用于从服务器“获取”数据。它通常不使用请求体。比如对于 URL /customers/getinfo.php?id=123,没有请求体。下面是GET
的一些方面:
- 这是幂等的——多次调用同一个
PUT
与调用一次效果相同。 - 它可以保留在浏览器历史中。
- 可以收藏。
- 它有长度限制。
- 请求使用 HTTP 头。
- 作为 HTTP 正文返回的响应。
GET
方法应该在服务器上以独立的方式实现。换句话说,发出多个相同的请求与发出一个请求具有相同的效果。注意,虽然幂等操作在服务器上产生相同的结果(没有副作用),但是响应本身可能不相同(例如,资源的状态可能在请求之间发生变化)。
图 18-3 显示了一个从 Snapchat 获取语言和语言代码列表的组件。
图 18-3
Getting a list of languages and language codes from Snapchat
这将是示例 http-ex100:
-
使用 CLI 构建应用:使用以下命令:
ng new http-ex100 --inline-template --inline-style
-
开始
ng serve
:使用以下代码:cd http-ex100 ng serve
-
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”
-
编辑模块:编辑 app.module.ts,修改为:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { SwaggerService } from './swagger.service'; import { HttpClientModule } from '@angular/common/http'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, HttpClientModule ], providers: [SwaggerService], bootstrap: [AppComponent] }) export class AppModule { }
-
创建服务:创建 swagger.service.ts::
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Language } from './language'; @Injectable() export class SwaggerService { constructor(private _http: HttpClient){} getLanguages() { return this._http.get<Array<Language>>('https://languagetool.org/api/v2/languages'); } }
-
创建数据对象类:创建语言. ts.
export class Language { private _code: string; private _name: string; public get code() { return this._code; } public get name() { return this._name; } public set code(newValue: string){ this._code = newValue; } public set name(newValue: string){ this._name = newValue; } }
-
编辑组件:编辑 app.component.ts,修改为:
import { Component, OnInit } from '@angular/core'; import { SwaggerService } from './swagger.service'; import { Language } from './language'; @Component({ selector: 'app-root', template: ` <h1>Countries</h1> <ul> <li *ngFor="let language of _languages"> {{language.name}} ({{language.code}}) </li> </ul> `, styles: [] }) export class AppComponent implements OnInit{ _languages = new Array<Language>(); constructor(private _swaggerService: SwaggerService) {} ngOnInit(){ this._swaggerService.getLanguages().subscribe( res => { this._languages = res; }, error => { console.log('an error occurred'); } ) } }
您的应用应该在 localhost:4200 上工作,您应该会看到一个语言列表。请注意以下几点:
- swagger.service.ts 文件创建一个具有可注入注释的服务,使其能够被注入到 app 组件中。这个服务有一个构造函数,Angular Http 模块被注入到这个构造函数中。它还包含方法
getLanguages
,该方法对服务器进行 HTTP 调用,服务器返回一个可观察值。请注意,get 方法使用泛型将响应类型指定为“语言”(参见 >)。 - 文件“language.ts”定义了语言数据对象,我们将使用该对象将数据从服务传递到组件。
- app.component.ts 文件创建一个组件。注意,swagger 服务是使用构造函数注入到这个组件中的。当组件初始化时,它调用 swagger 服务并用两种方法订阅可观察的结果:第一种方法表示成功,第二种方法表示失败。
- 第一个方法(成功的方法)接受 HTTP 结果作为参数,并将实例变量
_langages
设置为返回的 JavaScript 对象数组,然后在组件中可见。
使用参数的 HTTP GET 方法:示例
我们已经讨论了GET
,但是“得到”什么呢?一个GET
使用参数来获取一个特定的东西(或一些东西)——如果您想从服务器上获取特定客户的信息,这非常有用。您可以通过简单地修改GET
的 URI 以包含查询参数来实现,或者您可以使用嵌入到RequestOptionsArgs
对象中的 Angular 搜索或参数对象来实现。
图 18-4 显示了一个使用查询参数以三种不同方式执行 HTTP GET
的例子,由三个不同的按钮触发。
图 18-4
GET
in three different ways
这将是 http-ex200 的示例:
-
使用 CLI 构建应用:使用以下命令:
ng new http-ex200 --inline-template --inline-style
-
开始
ng serve
:使用以下代码:cd http-ex200 ng serve
-
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”
-
编辑模块:编辑 app.module.ts,修改为:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { HttpClientModule } from '@angular/common/http'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, HttpClientModule, FormsModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
-
编辑组件:编辑 app.component.ts,修改为:
import { Component } from '@angular/core'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; @Component({ selector: 'app-root', template: ` <input [(ngModel)]="_search" placeholder="city"> <button (click)="doSearchConcatenatedUrl()">Search (Concatenated URL)</button> <button (click)="doSeachHttpParams1()">Search (Http Params1)</button> <button (click)="doSeachHttpParams2()">Search (Http Params2)</button> <p>JSON {{_result | json}}</p> `, styles: [] }) export class AppComponent { _search = 'Atlanta'; _result = {}; constructor(private _http: HttpClient){ } doSearchConcatenatedUrl(){ const concatenatedUrl: string = "https://trailapi-trailapi.p.mashape.com?q[city_cont]=" + encodeURIComponent(this._search); const mashapeKey = 'OxWYjpdztcmsheZU9AWLNQcE9g9wp1qdRkFjsneaEp2Yf68nYH'; const httpHeaders: HttpHeaders = new HttpHeaders( {'Content-Type': 'application/json', 'X-Mashape-Key': mashapeKey}); this._http.get(concatenatedUrl, { headers: httpHeaders }).subscribe( res => { this._result = res; }); } doSeachHttpParams1(){ const url: string = 'https://trailapi-trailapi.p.mashape.com'; const mashapeKey = 'OxWYjpdztcmsheZU9AWLNQcE9g9wp1qdRkFjsneaEp2Yf68nYH'; const httpHeaders = new HttpHeaders( {'Content-Type': 'application/json', 'X-Mashape-Key': mashapeKey}); const params = new HttpParams({ fromString: 'q[city_cont]=' + this._search; }); this._http.get(url, {headers: httpHeaders, params: params}).subscribe( res => { this._result = res; }); } doSeachHttpParams2(){ const url: string = 'https://trailapi-trailapi.p.mashape.com'; const mashapeKey = 'OxWYjpdztcmsheZU9AWLNQcE9g9wp1qdRkFjsneaEp2Yf68nYH'; const httpHeaders = new HttpHeaders( {'Content-Type': 'application/json', 'X-Mashape-Key': mashapeKey}); const params = new HttpParams().set('q[city_cont]', this._search); this._http.get(url, {headers: httpHeaders, params: params}).subscribe( res => { this._result = res; }); } }
你的应用应该工作在本地主机:4200。请注意以下几点:
- 方法
doSearchConcatenatedUrl
通过将带有编码的‘q[city _ cont]’参数集的 URL 附加到编码的输入字符串来手动构建 URL 字符串。在 Http 客户端调用GET
方法。 - 方法 doSeachHttpParams1 从一个查询字符串构建一个 HttpParams 对象,类似于上面的方法
'doSearchConcatenatedUrl'
。在 Http 客户端调用GET
方法,在第二个参数中传递HttpParams
对象。注意,HttpParams 对象为我们进行编码。 - 方法 doSeachHttpParams2 创建一个 HttpParams 对象,并将“q[city_cont]”参数设置为输入字符串。在 Http 客户机上调用
GET
方法,在第二个。
使用路径参数的 Http GET 方法:示例
这是一个使用路径参数执行 HTTP GET
的例子。向用户显示文章列表,每一篇文章都有一个显示按钮,如图 18-5 所示。用户可以点击 Show 按钮,一个 HTTP GET
将被调用,带有路径参数,以获取文章的细节,然后显示在一个弹出的模态上。
图 18-5
Showing an article from a list
这将是 http-ex300 的示例:
-
使用 CLI 构建应用:使用以下命令:
ng new http-ex300 --inline-template --inline-style
-
开始
ng serve
:使用以下代码:cd http-ex300 ng serve
-
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”
-
编辑模块:编辑 app.module.ts,修改为:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { HttpClientModule } from '@angular/common/http'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, HttpClientModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
-
编辑组件:编辑 app.component.ts,修改为:
import { Component, OnInit, AfterViewInit, ViewChild } from '@angular/core'; import { HttpClient } from '@angular/common/http'; @Component({ selector: 'app-root', template: ` <ul> <li *ngFor="let post of _posts"> {{post.title}} <button (click)="showPost(post.id)">Show</button> </li> </ul> <div #modal id="myModal" class="modal"> <div class="modal-content"> <span class="close" (click)="closeModal()">×</span> <h3>{{this._post.title}}</h3> <p>{{this._post.body}}</p> </div> </div> `, styles: [] }) export class AppComponent implements OnInit { _posts = []; _post = {}; @ViewChild('modal') _myModal: any; constructor(private _http: HttpClient) { } ngOnInit() { return this._http.get<any>("http://jsonplaceholder.typicode.com/posts").subscribe( res => { this._posts = res; } ); } showPost(postId: number) { this._http.get<any>(`http://jsonplaceholder.typicode.com/posts/${postId}`).subscribe( res => { this._post = res; this._myModal.nativeElement.style.display = 'block'; } ) } closeModal() { this._myModal.nativeElement.style.display = 'none'; } }
-
编辑样式:编辑 styles.css 并将其更改为以下内容:
.modal { display: none; position: fixed; z-index: 1; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgb(0,0,0); background-color: rgba(0,0,0,0.2); } .modal-content { background-color: #fefefe; margin: 15% auto; padding: 20px; border: 1px solid #888; width: 60%; } .close { color: #aaa; float: right; font-size: 28px; font-weight: bold; } .close:hover, .close:focus { color: black; text-decoration: none; cursor: pointer; }
注意,在 app 组件方法showPost
中,我们使用模板文字将文章 ID 注入 URL 字符串。
HTTP POST 方法:示例
POST
非常常用于向服务器发布数据。它通常在请求正文中发送数据。例如,对于 URL /customers/new,请求主体是 name = Mark&city = Atlanta&state = GA。
以下是 HTTP POST
的一些重要方面:
- 它不是幂等的——多次调用同一个 put 会产生与调用一次不同的效果。
- 不能缓存。
- 它不能保留在浏览器历史中。
- 不能加书签。
- 它没有长度限制。
- 请求使用 HTTP 正文。
- 响应作为 HTTP 主体返回。
就其本质而言,HTTP POST 不是等幂的。它有副作用—例如,通过两次提交数据来两次添加客户(双重提交)。
图 18-6 显示了一个执行 HTTP POST
的例子。用户可以输入标题和正文,然后单击 Add 将它们发送到服务器。服务器向浏览器返回信息,这些信息被添加到底部的“您添加的”列表中。
图 18-6
HTTP POST
这将是示例 http-ex400:
-
使用 CLI 构建应用:使用以下命令:
ng new http-ex400 --inline-template --inline-style
-
开始
ng serve
:使用以下代码:cd http-ex400 ng serve
-
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”
-
编辑模块:编辑 app.module.ts,修改为:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { HttpClientModule } from '@angular/common/http'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, HttpClientModule, FormsModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
-
编辑组件:编辑 app.component.ts,修改为:
import { Component, OnInit, AfterViewInit, ViewChild } from '@angular/core'; import { HttpClient } from '@angular/common/http'; @Component({ selector: 'app-root', template: ` <div> Title: <br/> <input type="text" [(ngModel)]="_title" size="50" /> <br/> <br/> Body: <br/> <textarea [(ngModel)]='_body' rows="2" cols="50"> </textarea> <br/> <button (click)="onAdd()">Add</button> </div> <p><b>You Added:</b></p> <p *ngIf="_added.length == 0">None</p> <p *ngFor="let added of _added"> {{added.title}} </p> `, styles: ['div { padding: 20px; background-color: #C0C0C0 }'] }) export class AppComponent { _title: string; _body: string; _added: Array<any> = new Array<any>(); constructor(private _http: HttpClient) { } onAdd(){ const requestBody = { title: this._title || '[Unspecified]', body: this._body || '[Unspecified]', }; this._http.post("http://jsonplaceholder.typicode.com/posts", requestBody).subscribe( res => { this._added.push(res); } ) } }
请注意以下几点:
-
在 app 组件方法
onAdd
中,我们创建了将在请求体中发送到服务器的对象。 -
在 app 组件方法
onAdd
中,我们使用了or
操作符来确保我们向服务器传递了一些有效的东西——要么是this._title
(如果存在的话),要么是文本“[Unspecified]
:title: this._title || '[Unspecified]'
-
在 app 组件方法
onAdd
中,我们订阅 HTTPPOST
并使用箭头函数来处理返回的结果。我们将返回的结果添加到_added
的数组中,因此它出现在底部。
使用路径参数的 HTTP PUT 方法
除了在 REST 服务中通常用于更新资源而不是创建资源之外,PUT
方法类似于POST
方法。
HTTP PUT
的一些重要方面包括:
- 这是幂等的——多次调用同一个
PUT
与调用一次效果相同。 - 它没有长度限制
- 它不可缓存。
- 请求使用 HTTP 正文。
- 响应作为 HTTP 头返回。
使用路径参数的 HTTP 修补方法
PATCH
方法类似于PUT
,除了它不是幂等的。例如,将某项资源的价值增加一定的数量会很有用。
PATCH
的一些重要方面包括:
- 它不是幂等的——多次调用同一个
PUT
与调用一次有不同的效果。 - 它没有长度限制。
- 它不可缓存。
- 请求使用 HTTP 正文。
- 响应作为 HTTP 头返回。
使用路径参数的 HTTP 删除方法
DELETE
方法用于从服务器上删除资源。请注意以下几点:
- 它不是幂等的——多次调用
DELETE
与调用一次效果相同。 - 它不可缓存。
- 请求使用 HTTP 正文。
- 响应作为 HTTP 头返回。
修改服务器响应:示例
记住,Http 客户端服务调用返回可观察的对象。这意味着服务器向客户端(浏览器)返回一个异步数据流,并且您可以使用 RxJS 模块使用第十六章中讨论的操作符来处理该数据。这包括我们可以用来转换数据的map
操作符。
图 18-7 显示了一个使用 RxJS map
操作符(带函数)修改从服务器返回的响应的例子。在这种情况下,我们使用它将数据转换为类型化数据(在类中构造的数据)。
图 18-7
Using map
to modify a response
这将是 http-ex500 的示例:
-
使用 CLI 构建应用:使用以下命令:
ng new http-ex500 --inline-template --inline-style
-
开始
ng serve
:使用以下代码:cd http-ex500 ng serve
-
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”
-
创建类:在 app 中创建以下 TypeScript 类
post.``ts
:export class Post { _title: string = ""; _body: string = ""; constructor(title: string, body: string){ const titleNaN = title || ''; const bodyNaN = body || ''; this._title = titleNaN.length > 10 ? titleNaN.substring(0,9): titleNaN; this._body = bodyNaN.length > 20 ? bodyNaN.substring(0,19): bodyNaN; } get title(): string{ return this._title; } get body(): string{ return this._body; } }
-
编辑模块:编辑 app.module.ts,修改为:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { HttpClientModule } from '@angular/common/http'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, HttpClientModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
-
编辑组件:编辑 app.component.ts,修改为:
import { Component } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Post } from './Post'; import 'rxjs/Rx'; @Component({ selector: 'app-root', template: ` <ul> <li *ngFor="let post of _posts"> <b>{{post.title}}:</b> {{post.body}} </li> </ul> `, styles: [] }) export class AppComponent { _posts: Array<Post>; constructor(private _http: HttpClient) {} ngOnInit() { return this._http.get<Array<Post>>("http://jsonplaceholder.typicode.com/posts") .map( response => { const postsArray: Array<Post> = new Array<Post>(); for (const responseItem of response){ const post = new Post(responseItem['title'], responseItem['body']); postsArray.push(post); } return postsArray; } ) .subscribe( response => { this._posts = response; } ); } }
我们创建 typescript 类Post
来存储每篇文章。请注意,这个类有一个构造函数,用于修剪每篇文章的标题和正文。我们使用or
技巧将“不真实”的值转换为空字符串:
const titleNaN = title || '';
我们使用下面的代码从服务器获取数据。我们使用映射将响应(一个对象数组)转换成一个Post
类的类型化数组。然后我们订阅结果:
return this._http.get("http://jsonplaceholder.typicode.com/posts")
.map(
response => {
const postsArray: Array<Post> = new Array<Post>();
for (const responseItem of response){
const post =
new Post(responseItem['title'], responseItem['body']);
postsArray.push(post);
}
return postsArray;
}
)
.subscribe(
response => {
this._posts = response;
}
);
处理服务器错误响应:示例
当您订阅 HTTP 方法调用时,您提供了一个处理结果的处理程序方法。但是,您也可以提供其他处理程序方法—一个用于处理错误,另一个用于处理完成:
.subscribe(
function(response) { console.log("Success " + response)},
function(error) { console.log("Error " + error)},
function() { console.log("Completion")}
);
这是一个处理服务器错误并显示适当错误信息的例子(如图 18-8 所示)。
图 18-8
Displaying an error message
这将是 http-ex600 的示例:
-
使用 CLI 构建应用:使用以下命令:
ng new http-ex600 --inline-template --inline-style
-
开始
ng serve
:使用以下代码:cd http-ex600 ng serve
-
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”
-
创建服务类:在应用中创建下面的 TypeScript 类
service.ts
:import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import 'rxjs/Rx'; @Injectable() export class Service { constructor(private _http: HttpClient) { } getPosts() : Observable<any> { return this._http.get("http://jsonplaceholder.typicode.com/postss"); }; }
-
编辑模块:编辑 app.module.ts,修改为:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { HttpClientModule } from '@angular/common/http'; import { Service } from './Service'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, HttpClientModule ], providers: [HttpClientModule, Service], bootstrap: [AppComponent] }) export class AppModule { }
-
编辑组件:编辑 app.component.ts,修改为:
import { Component } from '@angular/core'; import { Service } from './Service'; import 'rxjs/Rx'; @Component({ selector: 'app-root', template: ` <ul> <li *ngFor="let post of _posts"> <b>{{post.title}}:</b> {{post.body}} </li> </ul> <div *ngIf="_error"> Error: {{_error.status}}: {{_error.statusText}} </div> `, styles: ['div {font-size:20px; padding: 5px; background-color: red;color: white}'] }) export class AppComponent { _posts = []; _error; constructor(private _service: Service) {} ngOnInit() { this._service.getPosts() .subscribe( response => { this._posts = response; }, error => { this._error = error; } ); } }
请注意以下几点:
- 服务类
service.
ts
使用了不正确的 URL,这将引发 404 错误。 - 组件 app.component.ts 调用方法
getPosts
并订阅其结果,使用两个方法,每个方法都用一个箭头函数实现。第一个处理成功的结果,第二个处理错误。在这个例子中,我们处理错误,将实例变量_error
设置为它的结果。 - 模板中引用了
_error
实例变量。如果设置,它显示一条红色消息。
异步管道:示例
异步管道订阅可观察对象或承诺,并返回它发出的最新值。当发出一个新值时,异步管道标记要检查更改的组件。当组件被破坏时,异步管道会自动取消订阅,以避免潜在的内存泄漏。
图 18-9 显示了一个使用映射来转换服务器输出,然后使用管道输出的例子。
图 18-9
Using a pipe to output
这将是 http-ex700 的示例:
-
使用 CLI 构建应用:使用以下命令:
ng new http-ex700 --inline-template --inline-style
-
开始
ng serve
:使用以下代码:cd http-ex700 ng serve
-
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”
-
编辑模块:编辑 app.module.ts,修改为:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { HttpClientModule } from '@angular/common/http'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, HttpClientModule ], providers: [HttpClientModule], bootstrap: [AppComponent] }) export class AppModule { }
-
编辑组件:编辑 app.component.ts,修改为:
import { Component } from '@angular/core'; import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import 'rxjs/Rx'; import { Observable } from 'rxjs/Observable'; @Component({ selector: 'app-root', template: ` <h1>Post Title Names</h1> <p>{{_result|async}}</p> `, styles: [] }) export class AppComponent { _result: any; constructor(private _http: HttpClient) {} ngOnInit() { this._result = this._http.get<Array<any>>("http://jsonplaceholder.typicode.com/posts") .map( response => { let titles = ''; for (const responseItem of response){ titles += responseItem['title']; } return titles; } ); } }
请注意以下几点:
- 组件 app.component.ts 调用 HTTP
GET
方法返回帖子列表。它使用map
操作符(和一个函数)将其转换成所有文章标题的字符串。然后,它将结果赋给_result
实例变量。 - 组件 app.component.ts 使用一个模板来显示
_result
实例变量的值。
摘要
这一重要章节讲述了 Http 通信的基础知识,以及如何使用 HTTP 服务来获取和发送数据到服务器。我建议您完成所有的练习,因为 Http 服务对您将来编写单页应用非常重要。请记住,您将使用的 Http 服务可能有自己的自定义头和内容类型。例如,您的服务器开发人员可能会引入一个自定义头,以便在发生错误时将错误信息从服务器返回到客户端。您可能还需要为每个 Http 服务调用添加安全令牌。您将需要知道这些 Http 服务是如何工作的,以及如何扩展它们的功能以按照您想要的方式工作。
下一章也很重要:它涵盖了表单,Angular 的另一个你会一直用到的特性。
十九、表单
没有表单,您无法在应用中输入数据。AngularJS 允许用户快速创建表单,使用NgModel
指令将输入元素绑定到$scope 中的数据。您也可以在 Angular 中做同样的事情,但是 Angular 4 有一个新的表单模块,可以更容易地执行以下操作:
- 动态创建表单
- 用通用验证器验证输入(必需)
- 用自定义验证器验证输入
- 测试表格
两种书写表格的方式
您可以像以前在 AngularJS 中一样继续编写表单,但是我推荐使用新的表单模块,因为它为您做了更多的工作。表单模块提供了两种处理表单的主要方式:模板驱动的表单和反应式表单。这两种方式都适用于同一个表单模块。
模板驱动的表单
这与 Angular.JS 中的工作方式类似。我们构建 HTML 模板并添加一些指令来指定附加信息(如验证规则),Angular 负责在幕后为我们构建模型对象:底层表单、表单组和控件。
- 优点:简单,快速入门,非常适合简单的表单,不需要知道表单模型对象是如何工作的
- 缺点:HTML 和业务规则是耦合的,没有单元测试
反应式表单
反应式表单不同。我们自己构建模型对象(包括验证表单规则),表单绑定(并同步)到模板。我通常使用反应式表单多于模板驱动的表单。
- 优点:更多的控制,对于更高级的表单是完美的,支持单元测试,HTML 和业务规则是分离的
- 缺点:需要知道表单模型对象是如何工作的,需要更多的时间来开发
在撰写本书时,Angular CLI 生成的项目已经建立了对表单模块的节点依赖。您所要做的就是调整您的模块来导入表单模块。下面是 app.module.ts 文件的一个示例:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
表单模型对象
本节适用于两种形式的写作:模板和反应。两者使用相同的模型对象。让我们快速浏览一下。
NgForm
存储表单的状态信息,包括以下内容:
- 表单内所有控件的值
- 表单中的字段组
- 表单中的字段
- 验证器
表单组
存储一组FormControl
实例的值和有效状态:
- 表单组中所有控件的值
表单控件
存储单个控件的值和有效状态,例如列表框:
- 价值
- 验证状态
- 状态(例如,禁用)
您可以添加订户来响应表单控件值的更改:
this.form.controls['make'].valueChanges.subscribe(
(value) => { console.log(value); }
);
您可以添加订户来响应表单控制状态的更改:
this.form.controls['make'].statusChanges.subscribe(
(value) => { console.log(value); }
);
示例输出:
INVALID
VALID
表单阵列
这用于跟踪多个FormControl
、FormGroup
或FormArray
的值和状态。这对于处理多个表单对象和跟踪整体有效性和状态很有用。
表单和 CSS
本节适用于两种编写表单的方法:模板和反应式。当您进行表单验证时,您需要在无效数据出现时突出显示它。表单模块被设计成与 CSS 一起工作,使得突出显示无效的用户输入变得非常容易。表 19-1 中列出的样式会自动添加到表单元素中——你需要做的只是添加 CSS 代码来产生所需的视觉效果。
表 19-1
Styles Added to Form Elements
| 风格 | 描述 | | :-- | :-- | | `ng-touched` | 控件失去焦点时应用的样式 | | `ng-untouched` | 如果控件尚未失去焦点,则应用样式 | | `ng-valid` | 控件通过验证时应用的样式 | | `ng-invalid` | 控件未通过验证时应用的样式 | | `ng-dirty` | 如果用户已经与控件集成,则应用样式 | | `ng-pristine` | 用户尚未与控件交互时应用的样式 |模板表单:示例
如前所述,模板表单使用指令来创建表单模型对象。您在模板中构建输入表单和输入,并添加一些指令,表单就准备好了,可以工作了。模板表单非常适合快速构建具有简单验证的简单表单。
模板表单异步工作。因此,在视图初始化和指令处理完成之前,模型对象是不可用的。甚至不是所有的模型对象在AfterViewInit
生命周期方法中都可用。
要使用 Angular 模板表单,您的应用模块需要从@angular/forms 节点模块导入表单模块:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
让我们来创建一个模板表单,看看需要什么来使它工作。这是表格示例-ex100。
-
使用 CLI 构建应用:使用以下命令:
ng new forms-ex100 --inline-template --inline-style
-
开始
ng serve
:使用以下代码:cd forms-ex100 ng serve
-
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”
-
编辑模块:编辑 app.module.ts,修改为:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, FormsModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
-
编辑组件:编辑 app.component.ts,修改为:
import { Component, ViewChild } from '@angular/core'; import { NgForm, RequiredValidator } from '@angular/forms'; @Component({ selector: 'app-root', template: ` <form #f novalidate> <p>First Name <input name="fname"/></p> <p>Last Name <input name="lname"/></p> Valid: {{ f.valid }} Data: {{ f.value | json }} </form> `, styles: [] }) export class AppComponent { @ViewChild('f') f: NgForm; }
-
View app: Notice that this component just displays the input forms, as shown in Figure 19-1. It doesn’t display any further information.
图 19-1
Displaying input forms
-
编辑组件:现在我们将向表单添加一些指令和输入标签,以使表单作为模板表单工作。这些变化在下面的代码中以粗体突出显示:
import { Component, ViewChild } from '@angular/core'; import { NgForm, RequiredValidator } from '@angular/forms'; @Component({ selector: 'app-root', template: ` <form #f="ngForm" novalidate> <p>First Name <input name="fname" ngModel required /></p> <p>Last Name <input name="lname" ngModel required /></p> Valid: {{ f.valid }} Data: {{ f.value | json }} </form> `, styles: [] }) export class AppComponent { @ViewChild('f') f: NgForm; }
-
View app: Note that this component displays the input forms and the state of the form in Figure 19-2—its validity and its data.
图 19-2
State of the form
这展示了使用ngForm
和ngModel
指令制作模板表单有多快,其中的form
对象保存表单状态(包括数据)。还要注意 HTML 输入字段是如何使用name
属性的——这是由表单指令获取的,用来标识控件及其值。
模板变量和数据绑定:示例
有时,您需要访问每个控件来访问其状态、值等。您可以使用以下语法将模板变量设置为控件的ngModel
(即其FormControl
对象)。您也可以使用ViewChild
来访问作为变量的FormControl
:
import { Component, ViewChild } from '@angular/core';
import { NgForm, FormControl, RequiredValidator } from '@angular/forms';
@Component({
selector: 'app-root',
template: `
<form #f="ngForm" novalidate>
<p>First Name <input name="fname" ngModel #fname="ngModel" required />
</p>
<h2>Form Template Variable</h2>
Valid {{ fname.valid}}
Data: {{ fname.value | json }}
<h2>From Instance Variable</h2>
Valid {{ fname2.valid}}
Data: {{ fname2.value | json }}
</form> `,
styles: []
})
export class AppComponent {
@ViewChild('f') f: NgForm;
@ViewChild('fname') fname2: FormControl;
}
您还可以使用模板变量来查询表单控件状态,如表 19-2 所示。这使得在模板中添加隐藏和显示错误消息的逻辑变得非常容易。
表 19-2
Template Variables
| 可变的 | 描述 | | :-- | :-- | | `.touched` | 用户是否在该字段中进行了任何输入?返回真或假。 | | `.valid` | 字段输入是否通过验证?返回真或假。 | | `.value` | 当前表单值。 | | `.hasError('required')` | 是否出现了指定的错误?返回真或假。 |有时,您需要将每个控件的值双向绑定到模型,以便可以根据需要获取和设置每个控件的值。如果要设置表单控件,这很有用。更改ngModel
指令以使用双向绑定,并将其链接到实例变量——在下面的例子中是_name
:
<input type="text" class="form-control" name="name" placeholder="Name (last, first)" [(ngModel)]="_name" required>
让我们来创建一个模板表单并将表单控件绑定到实例变量。让我们用 bootstrap 样式构建这个表单,这样它看起来会很好。提交表单有一个根据用户输入启用或禁用的按钮,如图 19-3 所示。
图 19-3
Creating a template form binding form controls to instance variables
这将是表格示例-ex200:
-
使用 CLI 构建应用:使用以下命令:
ng new forms-ex200 --inline-template --inline-style
-
开始
ng serve
:使用以下代码:cd forms-ex200 ng serve
-
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”
-
编辑网页:编辑 index.html 文件,将其更改为:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>FormsEx200</title> <base href="/"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ" crossorigin="anonymous"> <script src="https://code.jquery.com/jquery-3.1.1.slim.min.js" integrity="sha384-A7FZj7v+d/sdmMqp/nOQwliLvUsJfDHW+k9Omg/a/EheAdgtzNs3hpfag6Ed950n" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js" integrity="sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb" crossorigin="anonymous"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/js/bootstrap.min.js" integrity="sha384-vBWWzlZJ8ea9aCX4pEW3rVHjgjt7zpkNpZk+02D9phzyeVkE+jo0ieGizqPLForn" crossorigin="anonymous"></script> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico"> </head> <body> <app-root></app-root> </body> </html>
-
编辑模块:编辑 app.module.ts 文件,将其更改为:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, FormsModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
-
编辑组件:编辑 app.component.ts 文件,将其更改为:
import { Component, ViewChild } from '@angular/core'; import { NgForm, RequiredValidator } from '@angular/forms'; @Component({ selector: 'app-root', template: ` <form #appointmentForm="ngForm" novalidate (ngSubmit) = "onSubmitForm(appointmentForm)"> <legend>Appointment</legend> <div class="form-group"> <label for="name">Name</label> <input type="text" class="form-control" name="name" placeholder="Name (last, first)" [(ngModel)]="_name" required> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" class="form-control" name="password" placeholder="Password" [(ngModel)]="_password" required> </div> <div class="form-group"> <div class="form-check"> <div> <label>Appointment Time</label> </div> <label class="form-check-label"> <input type="radio" class="form-check-input" name="time" value="12pm" [(ngModel)]="_time" required> 12pm </label> </div> <div class="form-check"> <label class="form-check-label"> <input type="radio" class="form-check-input" name="time" value="2pm" [(ngModel)]="_time" required> 2pm </label> </div> <div class="form-check"> <label class="form-check-label"> <input type="radio" class="form-check-input" name="time" value="4pm" [(ngModel)]="_time" required> 4pm </label> </div> </div> <div class="form-group"> <label for="exampleTextarea">Ailment</label><textarea class="form-control" name="ailment" rows="3" [(ngModel)]="_ailment" required ></textarea> </div> <button type="submit" class="btn btn-primary" [disabled]="!_appointmentForm.valid">Submit</button> Valid: {{ _appointmentForm.valid }} Data: {{ _appointmentForm.value | json }} </form> `, styles: ['form { padding: 20px }', '.form-group { padding-top: 20px }'] }) export class AppComponent { @ViewChild('appointmentForm') _appointmentForm: NgForm; _name: string = 'mark'; _password: string = ''; _time: string = ''; _ailment: string = ''; onSubmitForm() { alert("Submitting data:" + JSON.stringify(this._appointmentForm.value)); } }
你的应用应该工作在本地主机:4200。注意,文件 index.html 被修改为链接到引导 CSS 和 JavaScript 文件。
文件 app.component 执行以下操作:
-
设置一个模板变量
appointmentForm
的表单。表单在提交时触发方法onSubmitForm
。 -
设置输入字段,并使用双向绑定和
ngModel
指令将每个字段的值链接到一个实例变量。 -
模板中包含以下标记以启用或禁用提交按钮:
<button type="submit" class="btn btn-primary" [disabled]="!_appointmentForm.valid">Submit</button>
-
在下面显示表单有效性和值。
模板表单和 CSS:示例
让我们通过创建一个带有颜色编码的输入表单来形成验证状态。绿色表示有效输入,红色表示无效输入。还有错误信息的代码,如图 19-4 所示。
图 19-4
Input form with color coding
这将是表格示例-ex300:
-
使用 CLI 构建应用:使用以下命令:
ng new forms-ex300 --inline-template --inline-style
-
开始
ng serve
:使用以下代码:cd forms-ex300 ng serve
-
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”
-
编辑样式:编辑文件 styles.css,并将其更改为:
input.ng-valid { border-left: 5px solid #42A948; /* green */ } input.ng-invalid { border-left: 5px solid #a94442; /* red */ } .error { color: #ff0000; } label { display: inline-block; width: 100px; } button { border: 1px solid black; margin: 20px; }
-
编辑模块:编辑文件 app.module.ts,修改为:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, FormsModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
-
编辑组件:编辑 app.component.ts 文件,将其更改为:
import { Component, ViewChild } from '@angular/core'; import { NgForm, FormControl, RequiredValidator } from '@angular/forms'; @Component({ selector: 'app-root', template: ` <form #f="ngForm" novalidate> <p><label>First Name</label><input name="fname" ngModel #fname="ngModel" required /> <span class="error" *ngIf="fname.touched && fname.hasError('required')">Required</span> </p> <p><label>Last Name</label><input name="lname" ngModel #lname="ngModel" required /> <span class="error" *ngIf="lname.touched && lname.hasError('required')">Required</span> </p> <p><label>Email</label><input name="email" ngModel #email="ngModel" required email /> <span class="error" *ngIf="email.touched && email.hasError('required')">Required</span> <span class="error" *ngIf="email.value && email.touched && email.hasError('email')">Invalid email</span> </p> <button (click)="onSubmit()" [disabled]="!f.valid">Submit</button> </form>`, styles: [] }) export class AppComponent { onSubmit(){ alert('Submitted'); } }
你的应用应该工作在本地主机:4200。请注意以下几点:
- 文件 styles.css 将所需的样式应用于适当的状态——例如,当表单控件有有效数据时,将
ng-valid
样式设置为显示绿色指示器。 - app.component.ts 文件包含根据表单控件状态显示错误信息的逻辑。
反应式表单:示例
您为表单构建模型对象——它们与模板表单相同——然后将它们绑定到模板中的输入控件。因此,您正在类中构建表单控件,并修改模板以链接到这些控件。这使您可以完全控制表单、表单值和表单验证。您可以直接操作模型对象(例如,更改值),绑定会立即同步生效。事实上,值和有效性的更新总是同步的,并且在您的控制之下。
要使用 Angular 模板表单,您的应用模块需要从@angular/forms 节点模块导入反应式表单模块:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
ReactiveFormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
要将模板绑定到模型,您需要在组件的模板中创建 HTML form
和 HTML 输入。然后在组件的类中创建一个表单模型。现在,您使用以下指令将两者绑定在一起:
<form [formGroup]="registerForm">
:连接表单模型和模板中的form
HTML。<fieldset formGroupName="address">
:连接表单组和模板中的fieldset
HTML。<input formControlName="name">
:连接模型中的表单控件和模板中的form input
HTML。
让我们来创建一个反应式表单,看看需要什么来使它工作。这将是表格示例-ex400:
-
使用 CLI 构建应用:使用以下命令:
ng new forms-ex400 --inline-template --inline-style
-
开始
ng serve
:使用以下代码:cd forms-ex400 ng serve
-
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”
-
编辑样式:编辑文件 styles.css,并将其更改为:
input.ng-valid { border-left: 5px solid #42A948; /* green */ } input.ng-invalid { border-left: 5px solid #a94442; /* red */ } .error { color: #ff0000; } label { display: inline-block; width: 100px; } button { border: 1px solid black; margin: 20px; }
-
编辑模块:编辑 app.module.ts 文件,将其更改为:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, ReactiveFormsModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
-
编辑组件:编辑 app.component.ts 文件,将其更改为:
import { Component, OnInit } from '@angular/core'; import { FormGroup, FormControl, FormControlName, Validators } from '@angular/forms'; @Component({ selector: 'app-root', template: ` <form #form [formGroup]="formGroup" (ngSubmit)="onSubmit(form)" novalidate> <label>Name: <input formControlName="name"> </label> <br/> <label>Location: <input formControlName="location"> </label> <br/> <input type="submit" value="Submit" [disabled]="!formGroup.valid"> </form> `, styles: [] }) export class AppComponent implements OnInit{ formGroup: FormGroup; ngOnInit(){ this.formGroup = new FormGroup({ name: new FormControl('', Validators.required), location: new FormControl('', Validators.required) }); } onSubmit(form: FormGroup){ alert('sumit'); } }
你的应用应该工作在本地主机:4200。请注意以下几点:
- 文件 styles.css 设置 css 样式。
- app.component.ts 文件包含表单模板中的 HTML。
- 当组件初始化时,app.component.ts 文件初始化模型,这是一个在
ngInit
方法中带有表单控件的表单组。-
app.component.ts 文件将模板中的 HTML 链接到模型。它使用以下代码将 HTML
form
链接到formGroup
:<form #form [formGroup]="formGroup" (ngSubmit)="onSubmit(form)" novalidate>
-
它使用以下代码将 HTML
input
链接到formControl
:<input formControlName="name">
-
反应式表单:表单生成器
FormBuilder
类旨在帮助您用更少的代码构建表单模型。将FormBuilder
注入到你的组件的类中,并使用表 19-3 中列出的方法。
表 19-3
FormBuilder Methods
| 方法 | 目的 | 争论 | 返回 | | :-- | :-- | :-- | :-- | | `group` | 创建表单组 | 配置对象,额外参数(验证器,异步验证器) | `FormGroup` | | `control` | 创建表单控件 | 当前表单状态(值/禁用状态),验证器数组,异步验证器数组 | `FormControl` | | `array` | 创建表单数组 | 配置对象(数组)、验证器、异步验证器 | `FormArray` |在接下来的例子中,我们将开始使用FormBuilder
。
反应式表单:表单组嵌套示例
有时我们的表单由多种不同的元素组成。例如,如果您正在输入客户订单,信息可以按以下方式组织:
- 名字
- 地址
- 命令
- 订单项目
- 信用卡信息
每个元素都可以包含一个或多个表单控件,所以我们需要能够管理每个元素。这就是表单组的用武之地。在这种情况下,你可以拥有如图 19-5 所示的表单组层次结构。
图 19-5
Hierarchy of form groups
该示例使用户能够输入并提交一个订单,其中包括客户名称、客户地址和商品列表,如图 19-6 所示。
图 19-6
Entering and submitting an order
这将是表格示例-ex500:
-
使用 CLI 构建应用:使用以下命令:
ng new forms-ex500 --inline-template --inline-style
-
开始
ng serve
:使用以下代码:cd forms-ex500 ng serve
-
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”
-
编辑模块:编辑 app.module.ts 文件,将其更改为:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, ReactiveFormsModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
-
编辑组件类:编辑 app.component.ts 文件,将其更改为:
import { Component, OnInit } from '@angular/core'; import { FormGroup, FormArray, FormBuilder, Validators } from '@angular/forms'; @Component({ selector: 'app-root', templateUrl: 'app.component.html', styles: ['div { background-color: #f2f2f2; padding: 15px; margin: 5px }', 'p { margin: 0px }' ] }) export class AppComponent implements OnInit { public _parentForm: FormGroup; public _name: FormGroup; public _addr: FormGroup; public _items: FormArray; constructor(private _fb: FormBuilder){} ngOnInit() { this._name = this._fb.group({ fname: ['', [Validators.required]], lname: ['', [Validators.required]] }); this._addr = this._fb.group({ addr1: ['', [Validators.required]], addr2: [''], city: ['', [Validators.required]], state: ['', [Validators.required]], zip: ['', [Validators.required, Validators.minLength(5), Validators.maxLength(5)]], }); this._items = this._fb.array( [this.createItemFormGroup()] ); this._parentForm = this._fb.group({ name: this._name, addr: this._addr, items: this._items }); } createItemFormGroup(){ return this._fb.group({ name: ['', Validators.required], qty: ['1', Validators.required], price: ['', Validators.required] }); } addItem(){ this._items.push(this.createItemFormGroup()); } deleteItem(index){ delete this._items[index]; } onSubmit(form: FormGroup){ alert('Submitted'); } }
-
编辑组件模板:编辑文件 app.component.html,将其更改为:
<form [formGroup]="_parentForm" novalidate (ngSubmit)="onSubmit(parentForm)"> <div formGroupName="name"> <b>Name</b> <br/> <label>First Name <input type="text" formControlName="fname"> <small *ngIf="_name.controls.fname.touched && !_name.controls.fname.valid">Required.</small> </label> <br/> <label>Last Name <input type="text" formControlName="lname"> <small *ngIf="_name.controls.lname.touched && !_name.controls.lname.valid">Required.</small> </label> </div> <br/> <div formGroupName="addr"> <b>Address</b> <br/> <label class="left">Address #1 <input type="text" formControlName="addr1"> <small *ngIf="_addr.controls.addr1.touched && !_addr.controls.addr1.valid">Required.</small> </label> <br/> <label>Address #2 <input type="text" formControlName="addr2"> </label> <br/> <label>City <input type="text" formControlName="city"> <small *ngIf="_addr.controls.city.touched && !_addr.controls.city.valid">Required.</small> </label> <br/> <label>State <select formControlName="state"> <option>AL</option> <option>GA</option> <option>FL</option> </select> <small *ngIf="_addr.controls.state.touched && !_addr.controls.state.valid">Required.</small> </label> <br/> <label>Zip <input type="number" formControlName="zip"> <small *ngIf="_addr.controls.zip.touched && !_addr.controls.zip.valid">Required.</small> </label> </div> <br/> <div formArrayName="items"> <b>Items</b> <br/> <p [formGroupName]="i" *ngFor="let item of _items.controls;let i=index"> <label>Name: <input type="text" formControlName="name" size="30"> <small *ngIf="item.controls.name.touched && !item.controls.name.valid">Required.</small> </label> <label>Qty: <input type="number" formControlName="qty" min="1" max="10"> <small *ngIf="item.controls.qty.touched && !item.controls.qty.valid">Required.</small> </label> <label>Price: <input type="number" formControlName="price" min="0.01" max="1000" step=".01"> <small *ngIf="item.controls.price.touched && !item.controls.price.valid">Required.</small> </label> </p> </div> <br/> <div> <input type="button" value="Add Item" (click)="addItem()"/> <input type="submit" value="Submit" [disabled]="!_parentForm.valid"/> </div> </form>
你的应用应该工作在本地主机:4200。请注意以下几点:
- 我们至少有四个固定的
FormGroup
对象:一个用于名称,一个用于地址,一个用于第一项,另一个用于父表单。 FormArray
包含一个FormGroup
对象,但是如果用户点击添加项目按钮,它可以包含其他FormGroup
对象。- 整个表单的有效性仍然控制着 Submit 按钮的启用和禁用。
验证器
Angular 为我们的表单提供了一些验证器。您可以向同一个FormControl
(在FormGroup
中的一个项目)添加多个验证器:
-
所需验证:
this.form = fb.group({ 'name': ['', Validators.required], });
-
最小长度验证:
this.form = fb.group({ 'name': ['', Validators.required, Validators.minLength(4)] });
-
最大长度验证:
this.form = fb.group({ 'name': ['', Validators.required, Validators.maxLength(4)] }); }
组合多个验证器
Validators
类提供了compose
方法,允许用户为一个控件指定多个验证器:
constructor(private fb: FormBuilder){
this.form = fb.group({
'name': ['', Validators.compose( [Validators.required, Validators.maxLength(6)] ) ],
});
}
自定义验证示例
Angular Forms 模块允许您创建一个自定义类来验证您的输入。验证方法是静态的,只有在出错时才返回验证结果。如果一切正常,这个方法返回一个空值。这个自定义类可以在指定FormBuilder
中的字段时使用,也可以在组件模板中使用,以提供可视提示。
该组件不允许用户进入奔驰,如图 19-7 所示。
图 19-7
Custom validation
这将是表格示例-ex600:
-
使用 CLI 构建应用:使用以下命令:
ng new forms-ex600 --inline-template --inline-style
-
开始
ng serve
:使用以下代码:cd forms-ex600 ng serve
-
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“应用工作!”
-
编辑样式:编辑文件 styles.css,并将其更改为:
input.ng-valid { border-left: 5px solid #42A948; /* green */ } input.ng-invalid { border-left: 5px solid #a94442; /* red */ }
-
编辑模块:编辑 app.module.ts 文件,将其更改为:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, ReactiveFormsModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
-
编辑组件:编辑 app.component.ts 文件,将其更改为:
import { Component, OnInit } from '@angular/core'; import { AbstractControl, FormGroup, FormControl, FormControlName, Validators } from '@angular/forms'; export function validateNotMercedes(control: AbstractControl) { return (control.value.toLowerCase() != 'mercedes') ? null : { validateNotMercedes: { valid: false } } } @Component({ selector: 'app-root', template: ` <form #form [formGroup]="formGroup" (ngSubmit)="onSubmit(form)" novalidate> <label>Make: <input formControlName="make"> </label> <br/> <label>Model: <input formControlName="model"> </label> <br/> <input type="submit" value="Submit" [disabled]="!formGroup.valid"> </form> `, styles: [] }) export class AppComponent implements OnInit{ formGroup: FormGroup; ngOnInit(){ this.formGroup = new FormGroup({ make: new FormControl('', [Validators.required, validateNotMercedes]), model: new FormControl('', Validators.required) }); } onSubmit(form: FormGroup){ alert('sumit'); } }
你的应用应该工作在本地主机:4200。请注意以下几点:
- 文件 app.component.ts 中的代码导出
validateNotMercedes
函数来验证 make。注意,它返回一个 null 来表示有效性——否则,它返回一个属性valid
设置为 false 的对象。 - 文件 app.component.ts 中的代码使用
FormControl
对象设置表单组。注意这里的make FormControl
是如何将validateNotMercedes
函数指定为验证器的。
摘要
你不必使用有角的表单模块,但是它们为你做了很多工作,节省了你很多时间。Angular 为您提供了两种选择:快速简单的模板表单和更高级的反应式表单。您需要了解这两者,因为它们都非常有用并且实现得很好。他们可能需要一些时间来学习,但回报是值得的。
下一章讨论管道。管道不是必不可少的,但可能很有用。