前面我们已经使用三个章节介绍了从组件到视图的单向数据绑定,本章我们将介绍从视图到组件的单项数据绑定。
从视图到组件
从视图到组件的数据改变,通常是由用户直接或间接触发的,比如用户的键盘输入、鼠标点击等动作,因此这类数据绑定实际就是事件绑定。
事件绑定
Angular 事件绑定使用如下语法:
(target-event)="TemplateStatement"
target-event
使用()
包围,是事件的名字;TemplateStatement
是模板语句。当target-event
发生时,TempalteStatement
会被自动执行。例如:
@Component({ selector: 'app-my', template: ` <button (click)="onButtonClicked()">Button</button> `, styles: [ ] }) export class MyComponent { onButtonClicked() { // ... } }
上面代码中,我们将<button>
的click
事件绑定到组件类的onButtonClicked()
函数。当用户点击按钮,发出click
事件时,onButtonClicked()
会自动调用。
回忆一下,之前我们在介绍插值或者属性绑定的时候,使用的是“模板表达式 template expression”,而这里使用的是“模板语句 template statement”。前面我们说过,模板表达式不能具有副作用,不能改变应用的状态;但模板语句是可以的。这意味着,我们使用事件绑定的时候,可以对组件状态做修改。当事件绑定发生时,Angular 会自动执行数据的变更检测,按照新的数据更新视图,从而保证数据的保存和显示是一致的。
我们可以将多个事件处理器绑定到一个事件。其写法类似普通的 JavaScript 代码,例如:
@Component({ selector: 'app-my', template: ` <button (click)="onButtonClicked(); clickCount++">Button</button> `, styles: [ ] }) export class MyComponent { clickCount = 0; onButtonClicked() { // ... } }
$event
事件可能有参数,这个参数使用$event
传递给模板语句。例如:
@Component({ selector: 'app-my', template: ` <button (click)="onButtonClicked($event)">Button</button> `, styles: [ ] }) export class MyComponent { clickCount = 0; onButtonClicked(event: MouseEvent) { // ... } }
注意这里的$event
仅在模板中使用,其它地方是没有定义的。所以,我们在 HTML 中使用的是$event
,而在对应的组件 TypeScript 中使用的则是普通的event
作为参数名(当然,形参名字写作$event
也没有关系,但这与模板中的$event
是完全不一样的)。上面的代码中,由于click
事件是 DOM 事件,所以$event
类型是MouseEvent
。如果是我们自定义的事件,则$event
类型就是我们自定义事件的参数类型。这一点我们会在后面章节详细介绍。
但是,对于 DOM 事件,简单地使用$event
并不是一个好的做法,因为这会将组件和 DOM 事件绑定在一起,导致组件知道了过多的模板信息,并且难以进行测试。为解决这一问题,我们可以使用模板引用变量。
模板引用变量
前面我们说过,模板引用变量代表了一个模板中的元素。例如:
@Component({ selector: 'app-my', template: ` <input #input /> <button (click)="onButtonClicked(input)">Button</button> `, styles: [ ] }) export class MyComponent { value: string; onButtonClicked(input: HTMLInputElement) { this.value = input.value; } }
使用模板引用变量,事件处理函数的参数类型就是模板引用变量指向的元素的类型。直接使用模板引用变量与之前提到的直接使用$event
没有本质的区别,问题都是组件和 DOM 耦合在一起。所以,更好地做法是:
@Component({ selector: 'app-my', template: ` <input #input /> <button (click)="onButtonClicked(input.value)">Button</button> `, styles: [ ] }) export class MyComponent { value: string; onButtonClicked(value: string) { this.value = value; } }
这样,我们就避免了将整个元素作为参数,而仅使用了其输入的值。当然,我们也可以同时传递多个参数,例如:
@Component({ selector: 'app-my', template: ` <input #input /> <button (click)="onButtonClicked(input, $event, 'btn')">Button</button> `, styles: [ ] }) export class MyComponent { value: string; onButtonClicked(input: HTMLInputElement, event: MouseEvent, id: string) { this.value = input.value; } }
键盘事件过滤
我们使用keydown
和keyup
事件监听键盘事件,例如下面的代码:
<input (keyup)="onKeyUp($event)">
但是很多时候,我们只关心特定键盘按键的事件,例如,我们只去处理回车事件。当然,我们可以在keyup
事件绑定的回调函数中利用if
语句进行判断。Angular 为我们提供了更方便的方法:键盘事件过滤。我们只需使用特殊的语法就可以达到这一目的:
<input (keyup.enter)="onEnterKeyUp($event)" (keyup.escape)="onEscKeyUp($event)">
上面的代码,我们只监听回车和 ESC 键的事件,其余按键则不关心。Angular 将这种语法称为“伪事件”。伪事件功能强大,我们甚至可以监听组合键:
<input (keyup.control.shift.enter)="whatever()">
自定义事件
前面我们使用的都是 DOM 事件。事实上,Angular 提供了自定义事件,允许我们发出自己的事件。自定义事件使用EventEmitter
类实现。例如下面的示例:
@Component({ selector: 'app-my', template: ` <button (click)="onButtonClicked()">Button</button> ` }) export class MyComponent { @Output() buttonClicked: EventEmitter<void> = new EventEmitter<void>(); onButtonClicked() { this.buttonClicked.emit(); } }
我们定义了一个app-my
组件,使用@Output()
修饰符定义了一个事件buttonClicked
,其类型是EventEmitter
。当用户点击了组件中的按钮时,这个buttonClicked
事件就会发出(通过emit()
函数)。那么,在使用了app-my
的组件中,就可以像使用 DOM 事件一样使用这个自定义事件:
<app-my (buttonClicked)="youClickThisButton()"></app-my>
我们会在以后的章节中详细介绍自定义事件。