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


之前的笔记:

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


之前的内容主要学习了怎么通过绑定原生 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 去渲染对应的 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 大概是因为一个 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 基础] - 自定义事件 &amp; 自定义属性-LMLPHP

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

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

随后即可正常渲染:

[Angular 基础] - 自定义事件 &amp; 自定义属性-LMLPHP

⚠️: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>

实现后效果如下:

[Angular 基础] - 自定义事件 &amp; 自定义属性-LMLPHP

自定义事件的 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,
    });
  }
}

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


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

02-10 09:53