2017 年 11 月

第 33 卷,第 11 期

孜孜不倦的程序员 - 如何成为 MEAN:Angular 窗体

作者 Ted Neward | 2017 年 11 月

Ted Neward

我们又见面了,MEAN 用户们。
本系列文章的讨论重点都是 Angular;具体而言,是使用 Angular 通过多个页面显示数据和路由。然而,尽管 Web 浏览器能够显示数据,但它也可用于收集数据并将数据传递回服务器,到目前为止,并未对这些内容进行讨论。

Angular 当然可以通过窗体捕获数据,但是这样做会有麻烦,不是在创建和定义浏览器窗体的语法中,而是在基础行为的一个特定方面,我们稍后再讨论。但是我们不要本末倒置。先来讨论一些简单窗体定义和捕获。

构成的组件

在之前的专栏文章中,我谈到了如何将“模板语句”(使用 Angular 术语)绑定到由 Angular 组件提供的事件。最常见的例子是捕获 <button> 元素上的 click 事件,以便调用 Angular 组件上的方法:

<button (click)="console.log('Clicked')">Push me!</button>

就其本身而言,这种方法是好的,但它没有提供任何工具来捕获输入,为了其可用于数据输入,必须满足以下两个条件之一:组件中的代码可以从组件内部引用页面上的控件(这是 ASP.NET Web 窗体及其他框架中将要做的),或者被调用的代码必须能够接收输入数据作为参数。但是,这可以采用几种不同的窗体。

首先,也是最通用的,Angular 模板语句可以引用 $event 对象,它实质上是在用户会话期间生成的 DOM 事件对象。这只需要引用参数作为语句的一部分,例如:

<button (click)="capture($event)" >Save</button>

但是,缺点是传递的对象是表示用户操作的 DOM 事件;在本例中,是跟踪点击鼠标的屏幕位置的 MouseEvent,它并没有真正捕获页面上其他元素的状态。虽然当然可以导航 DOM 层次结构来查找控制元素并从中提取值,但这也不是 Angular 的独到之处。组件应该与 DOM 隔离,模板语句应该能够获得它所需要的数据并将其传递给组件上的方法以供使用。

这种方法表明,Angular 需要一定的方法来识别页面上的输入字段,以便模板语句可以拉取值并将其传入。能够识别窗体元素的需求采用 <input> 元素本身的“标识符”形式,Angular 称之为“模板引用变量”。 像其他一些 Angular 语法一样,它故意使用看起来不像 HTML 的语法:

<input #firstName>

按照相同名称的普通 HTML 标签,该语法将在 HTML 中创建一个输入字段,然后将新变量引入到名为 firstName 的模板作用域,可以从绑定到字段事件的模板语句中引用该变量,像这样:

<button (click)="addSpeaker(firstName, lastName)" >Save</button>

这是不言而喻的:单击按钮时,调用组件的 addSpeaker 方法,相应地传入 firstName 和 lastName 变量,如图 1 所示。

图 1 调用 addSpeaker 方法

import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-speaker-edit',
templateUrl: './speaker-edit.component.html',
styleUrls: ['./speaker-edit.component.css']
})
export class SpeakerEditComponent implements OnInit {
constructor() { }
ngOnInit() { }
addSpeaker(fname: string, lname: string) {
console.log("addSpeaker(", fname, ",", lname, ")")
}
}

但是,如果像这样编写,浏览器控制台中显示的内容不是输入中的预期字符串;会出现像 <input _ngcontent-crf-2> 这样的值以代替前面的每个值。其原因很简单:浏览器控制台返回 DOM 元素的实际 Angular 表示形式,而不是键入的输入数据。相应的解决方案也同样简单:利用模板语句每端的“value”属性来获取用户输入的数据。

因此,如果我需要构建一个用于创建新发言人的组件,我可以创建一个显示两个 <input> 字段的组件、一个具有将调用 addSpeaker 的点击的 <button>,传入 firstName.value 和 lastName.value,并使用该方法调用 SpeakerService(在前面的文章中)将其保存到数据库。但是,这个“创建新发言人”的想法在概念上非常接近于“编辑现有发言人”,以至于一些现代数据库已经开始将插入和更新操作视为基本上一样:upsert。这将是非常好的并且面向组件 - 如果 SpeakerEdit 组件可以在同一个组件中用作创建或编辑操作。

由于前面已经看到 @Input@Output 指令的强大作用,这里的作用实际上是相当微不足道的。为发言者添加一个 @Input 字段以便传入,再添加一个 @Output 字段以便让人们知道用户何时单击“保存”。(如果你决定将 SpeakerEdit 组件始终保存到数据库中,并且组件的客户端没有想要执行的其他操作,则后一个操作是没有必要的。那将会是团队会议中一个上下文高度相关的对话。)

这样一来,SpeakerEdit 组件代码便如图 2 所示。

图 2 SpeakerEdit 组件

export class SpeakerEditComponent implements OnInit {
@Input() speaker: Speaker;
@Output() onSave = new EventEmitter<Speaker>();
constructor(private speakerService: SpeakerService) { }
ngOnInit() {
if (this.speaker === undefined) {
this.speaker = new Speaker();
}
}
save(fn: string, ln: string) {
this.speaker.firstName = fn;
this.speaker.lastName = ln;
this.onSave.emit(this.speaker);
}
}

而且,正如你所期待的那样,考虑到我的设计技巧,这个模板相当简单但功能很强大:

<div>
Speaker Details: <br>
FirstName: <input #firstName><br>
LastName:  <input #lastName><br>
<button (click)="save(firstName.value, lastName.value)">Save</button>
</div>

再次注意,我使用 “.value” 从 firstName 和 lastName 输入字段中提取字符串值。

使用这个组件(在这个练习中,来自主 AppComponent)非常简单:

<h3>Create a new Speaker</h3>
<app-speaker-edit (onSave)="speakerSvc.save($event)"></app-speaker-edit>

在这种情况下,我选择不从组件内保存发言人,而是让客户端从发出的事件中保存。speakerSvc 对象是依赖项注入的 SpeakerService,来自以前关于在 Angular 中构建服务的文章 (msdn.microsoft.com/magazine/mt826349)。

这正是你相信组件化的原因:UI“控件”一经创建就可以投入使用,而不必考虑它们如何在内部工作。

触发事件

在这里进行总结是很好,但 Angular 中有一个关于事件的转向情况值得我们关注。开发人员通常需要捕获键击等事件并对输入的数据作出反应,例如,显示一些自动完成的建议值。对此,传统的 DOM 事件就是像 onBlur 或 onKeyUp 一样的方法。例如,假设最好是跟踪每次键击并在用户输入时显示。对 Angular 不熟悉的开发人员可能会期望按如下所示工作:

@Component({
  selector: 'loop-back',
  template: `
    <input #box>
    <p>{{box.value}}</p>
  `
})
export class LoopbackComponent { }

但当运行时,这些代码在键入时并不会显示每个字符,实际上,它什么也不显示。这是因为除非浏览器触发事件,否则 Angular 不会触发事件。出于这个原因,Angular 需要触发一个事件,即使该事件中被触发的代码是一个完整的无操作,例如模板语句 0,具体如下所示:

@Component({
  selector: 'loop-back',
  template: `
    <input #box (keyup)="0">
    <p>{{box.value}}</p>
  `
})
export class LoopbackComponent { }

请注意“keyup”绑定。这告诉 Angular 在 Input 元素上注册关键事件,这给了 Angular 触发事件的机会,然后会更新视图。刚开始使用会有些难,但 Angular 并不是一直在轮询任何类型的事件,因此不必消耗太多的 CPU 周期。

总结

一些资深的 Web 开发人员可能会觉得这有点令人困惑和尴尬:“为什么我们不能回到良好的 HTML 窗体?” Angular 的独到之处并不总是显而易见的,但是在许多方面,一旦推理和理由变得清晰,它们通常是可以理解和合理的。在这种特殊情况下,Angular 的独到之处是利用组件的概念,并考虑构建可用的“结构”,该结构知道如何捕获和处理输入。这允许一定程度的灵活性,这种灵活性在“旧的 Web”思维方式中并不真正存在,例如在单个页面上具有多个这样的组件。(当你必须刷新整个页面才能显示结果时,最终必须按照输入要求提供一个“输入-验证-保存-渲染”周期。)

但是,这里还有一些其他问题,比如在用户编辑现有的 Speaker 时知道如何更新系统的其他部分。Angular 为此提供了一个解决方案,但在进入响应式编程领域之前还有几个步骤要执行。胜利就在眼前,跟我一起再坚持一下吧。但是,与往常一样,编码快乐!


Ted Neward 就职于总部位于西雅图的 Polytechnology 公司,担任顾问、讲师兼导师,目前在 Smartsheet.com 担任开发者关系主管。他写过大量文章,独自撰写并与人合著过十几本书,工作足迹遍及全世界。可通过 ted@tedneward.com 与他联系,也可阅读他的博客 blogs.tedneward.com

衷心感谢以下 Microsoft 技术专家对本文的审阅:James Bender


在 MSDN 杂志论坛讨论这篇文章