[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
去渲染对应的 cockpit
和 server-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-list
和 server-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,});}}
同样是 ()
内的代表外部的变量名,而声明的则是组件内部可用的名称
到这里就实现了数据和事件的跨组件交流