之前的笔记:
[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
的作用是存储一个 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 大概是因为一个 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 Namelabel>
<input type="text" class="form-control" [(ngModel)]="newServerName" />
<label>Server Contentlabel>
<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,并且将其渲染到页面上
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
内的东西注释掉,以防报错
如果不会报错的话则可以忽略,我后面又做了点修改……
先新建一个 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
) {}
}
这里主要就是在数组里放一个数据,新增代码如下:
export class AppComponent {
serverElements: ServerElement[] = [
{ type: 'server', name: 'Testserver', content: 'Just a test!' },
];
}
这里会更新一下代码,绑定 自定义属性 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"
就是新增的代码,也就是绑定的 自定义属性
这里是选择接受参数的地方,已经从上面的 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>
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,而非使用传递过来的变量名——比如说可能父元素会创建一个事件然后传递 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>
实现后效果如下:
这个和自定义属性的方式实现的也差不多:
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,
});
}
}
同样是 ()
内的代表外部的变量名,而声明的则是组件内部可用的名称
到这里就实现了数据和事件的跨组件交流