[Angular 基础] – 自定义事件 & 自定义属性


之前的笔记:

  • [Angular 基础] – Angular 渲染过程 & 组件的创建

  • [Angular 基础] – 数据绑定(databinding)

  • [Angular 基础] – 指令(directives)

以上是能够实现渲染静态页面的基础


之前的内容主要学习了怎么通过绑定原生 HTML(style, class, click 等) 和 Angular(ngFor, (click), {{ string interpolation }} 等) 的事件和属性动态渲染静态页面,这里开始讲组件沟通之间的部分,让页面开始真正的动起来

也就是 组件(component)指令(directives) 的进阶学习

设置项目

目前项目的结构如下:

src/app/├── app.component.css├── app.component.html├── app.component.ts├── app.module.ts├── cockpit│ ├── cockpit.component.css│ ├── cockpit.component.html│ └── cockpit.component.ts└── server-element├── server-element.component.css├── server-element.component.html└── server-element.component.ts3 directories, 10 files

app

其中最基层的 app 的作用是存储一个 serverList,并且使用 serverList 去渲染对应的 cockpitserver-element,具体文件如下:

  • VM 层

    import { Component } from '@angular/core';@Component({selector: 'app-root',templateUrl: './app.component.html',styleUrls: ['./app.component.css'],})export class AppComponent {serverElements = [];}
  • V 层

    <div class="container"><app-cockpit></app-cockpit><hr /><div class="row"><div class="col-xs-12"><app-server-element*ngFor="let element of serverElements"></app-server-element></div></div></div>

    这里就会开始涉及组件之间的沟通:

    • cockpit 会创建一个 server,并且将数据添加到 serverElements
    • server-element 会接受 element,也就是 for 循环里的元素

cockpit

有些无关紧要的说明:

駕駛艙(英語:Cockpit),是飞行员控制飛機的座艙,通常位於一架飛機的前端。除了早期的部分飛機,如今大部分飛機的駕駛艙采用密閉式的設計。

这里命名为 cockpit 大概是因为一个 server 既可以是 server,也可以是一个 blueprint。这个不用细究 class/object 的区别,主要还是自定义事件和属性方面的问题

  • VM 层

    import { Component } from '@angular/core';@Component({selector: 'app-cockpit',templateUrl: './cockpit.component.html',styleUrl: './cockpit.component.css',})export class CockpitComponent {newServerName = '';newServerContent = '';onAddServer() {}onAddBlueprint() {}
  • V 层

    <div class="row"><div class="col-xs-12"><p>Add new Servers or blueprints!</p><label>Server Name</label><input type="text" class="form-control" [(ngModel)]="newServerName" /><label>Server Content</label><input type="text" class="form-control" [(ngModel)]="newServerContent" /><br /><div class="btn-toolbar"><button class="btn btn-primary" (click)="onAddServer()">Add Server</button><button class="btn btn-primary" (click)="onAddBlueprint()">Add Server Blueprint</button></div></div></div>

server-element

这里会接受一个 server,并且将其渲染到页面上

  • VM 层

    import { Component } from '@angular/core';@Component({selector: 'app-server-element',templateUrl: './server-element.component.html',styleUrl: './server-element.component.css',})export class ServerElementComponent {}
  • V 层

    <div class="panel panel-default"><div class="panel-heading">{{ element.name }}</div><div class="panel-body"><p><strong *ngIf="element.type === 'server'" style="color: red">{{ element.content }}</strong><em *ngIf="element.type === 'blueprint'">{{ element.content }}</em></p></div></div>

此时因为组件之间的交流还没有完成,所以代码运行肯定会失败的,不过最基础的是已经完成了

绑定自定义属性

首先是从渲染 server-listserver-element 开始,所以需要将 cockpit 内的东西注释掉,以防报错

如果不会报错的话则可以忽略,我后面又做了点修改……

model

先新建一个 server-element 的 model 让其他文件引用,我改了下结构,现在 model 在这里:

❯ tree src/app/src/app/├── model│ └── server-element.model.ts

内容如下:

export class ServerElement {constructor(public name: string,public type: 'server' | 'blueprint',public content: string) {}}

app VM 层

这里主要就是在数组里放一个数据,新增代码如下:

export class AppComponent {serverElements: ServerElement[] = [{ type: 'server', name: 'Testserver', content: 'Just a test!' },];}

app V 层

这里会更新一下代码,绑定 自定义属性 element

<div class="container"><app-cockpit></app-cockpit><hr /><div class="row"><div class="col-xs-12"><app-server-element*ngFor="let serverElement of serverElements"[element]="serverElement"></app-server-element></div></div></div>

其中 [element]="serverElement" 就是新增的代码,也就是绑定的 自定义属性

server-element V 层

这里是选择接受参数的地方,已经从上面的 V 层知道传进来的自定义属性是 element,因此这里就用 element 作为变量名:

<div class="panel panel-default"><div class="panel-heading">{{ element.name }}</div><div class="panel-body"><p><strong *ngIf="element.type === 'server'" style="color: red">{{ element.content }}</strong><em *ngIf="element.type === 'blueprint'">{{ element.content }}</em></p></div></div>

server-element VM 层

VM 层是掌管数据的地方,因此 VM 层还需要声明一下 element 的存在:

import { Component } from '@angular/core';import { ServerElement } from '../model/server-element.model';@Component({selector: 'app-server-element',templateUrl: './server-element.component.html',styleUrl: './server-element.component.css',})export class ServerElementComponent {// 不做类型声明也不会报错,但是会有简易element: ServerElement;}

这时候效果如下:

Angular 渲染了一个元素,但是这个元素是空的,这个原因是因为 scoping 的问题,element 本质上还是只对父组件——即 app 组件——可见,如果想让它在子组件里也能被访问到,需要用一个新的装饰器:@Input(),修改如下:

export class ServerElementComponent {@Input() element: ServerElement;}

随后即可正常渲染:

⚠️:Input 需要从 @angular/core 中导入

自定义属性的 alias

有的时候会想要设置 alias,而非使用传递过来的变量名——比如说可能父元素会创建一个事件然后传递 event 到子元素中,子元素则可以根据需求去重命名这是一个 mouseEvent, inputEvent, formEvent 或是其他,修改方法如下:

export class ServerElementComponent {// () 内的才是父组件里使用的变量名@Input('element') aliasElement: ServerElement;}

这个时候,对于当前组件来说,可访问的变量为 aliasElement,因此 V 层也需要进行对应的修改:

<div class="panel panel-default"><div class="panel-heading">{{ aliasElement.name }}</div><div class="panel-body"><p><strong *ngIf="aliasElement.type === 'server'" style="color: red">{{ aliasElement.content }}</strong><em *ngIf="aliasElement.type === 'blueprint'">{{ aliasElement.content }}</em></p></div></div>

绑定自定义事件

这个时候需要将 cockpit 里的代码还原

这里同样需要注意的一点是数据的传输方向,在父组件中,只有 serverElements 被声明了,具体的添加事件是发生在子组件中的,也就是说,事件的传输方向并不是由父组件向子组件进行传输,而是从子组件传递到父组件。准确的说也不是传送,而是发送(emit )。和 React 相反,Angular 的事件通常情况下是从子组件发送到父组件,父组件通过监听事件进行对应的处理

其实这个处理大方向和上面绑定自定义属性差不多,最大的差别就是 flow

cockpit VM 层

实现如下:

export class CockpitComponent {@Output() serverCreated = new EventEmitter<Omit<ServerElement, 'type'>>();@Output() blueprintCreated = new EventEmitter<Omit<ServerElement, 'type'>>();newServerName = '';newServerContent = '';onAddServer() {this.serverCreated.emit({name: this.newServerName,content: this.newServerContent,});}onAddBlueprint() {this.blueprintCreated.emit({name: this.newServerName,content: this.newServerContent,});}}

⚠️:这里的 Output 同样需要从 angular-core 导入

:注意这里的语法,这是一个 EventEmitter,并且类型是 Output。这也说明了事件的方向是自下而上,而非自上而下——对比 React,React 将 event handler 从上往下传,并在子元素进行调用

cockpit V 层

保持不变

app VM 层

变动如下

export class AppComponent {serverElements: ServerElement[] = [{ type: 'server', name: 'Testserver', content: 'Just a test!' },];serverData: ServerElement;onServerAdded(serverData: Omit<ServerElement, 'type'>) {this.serverElements.push({type: 'server',name: serverData.name,content: serverData.content,});}onBlueprintAdded(blueprintData: Omit<ServerElement, 'type'>) {this.serverElements.push({type: 'blueprint',name: blueprintData.name,content: blueprintData.content,});}}

⚠️:Omit 是 TypeScript 的语法,详细的使用方法可以查看官方文档:Utility Types

app V 层

变动如下:

<div class="container"><app-cockpit(serverCreated)="onServerAdded($event)"(blueprintCreated)="onBlueprintAdded($event)"></app-cockpit><hr /><div class="row"><div class="col-xs-12"><app-server-element*ngFor="let serverElement of serverElements"[element]="serverElement"></app-server-element></div></div></div>

实现后效果如下:

自定义事件的 alias

这个和自定义属性的方式实现的也差不多:

import { Component, EventEmitter, Output } from '@angular/core';import { ServerElement } from '../model/server-element.model';@Component({selector: 'app-cockpit',templateUrl: './cockpit.component.html',styleUrl: './cockpit.component.css',})export class CockpitComponent {@Output('serverCreated') svCreated = new EventEmitter<Omit<ServerElement, 'type'>>();@Output('blueprintCreated') bpCreated = new EventEmitter<Omit<ServerElement, 'type'>>();newServerName = '';newServerContent = '';onAddServer() {this.svCreated.emit({name: this.newServerName,content: this.newServerContent,});}onAddBlueprint() {this.bpCreated.emit({name: this.newServerName,content: this.newServerContent,});}}

同样是 () 内的代表外部的变量名,而声明的则是组件内部可用的名称


到这里就实现了数据和事件的跨组件交流