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