[Day26] Angular 的四种 Binding

昨天我们的 Component 只有简简单单的一行 “ironman works!” 其他啥都没有。今天,我们就来替他加点东西,让它显示资料。首先我们先在 ironman.component.html 用原生 HTML 刻出丑丑的使用者资讯页面,存档後我们丑丑的使用者资讯就会长在主页的中间

如果已经执行 ng serve -o 指令,而且没有 ctrl + c 或关闭命令提示字元,存档之後 Angular 会自动套用变更并刷新页面

https://ithelp.ithome.com.tw/upload/images/20210926/20140664nexubzjGFT.png

这时候因为一切从简,连使用者资讯都是写死的字串,但是我们当然不能这麽做,我们一定得把它的资料来源跟某个地方做连结,这样哪天资料做了变更,我们的页面才能正确地更新。

内嵌系结 (interpolation )

首先,我们来介绍最简单的资料系结方法 – 内嵌系结就是简单直接的把 component.ts 里的变数直接拿来嵌入 HTML 中,语法的规定是:在 HTML 用 "双大括号" 把变数包起来。让我们从刚刚的 HTML 把数值搬进 ts 档,用变数存起来,然後在 HTML 中加入内嵌系结的语法

// ironman.component.ts
export class IronmanComponent implements OnInit {

  constructor() { }

  userId = 1;
  userName = 'Alice';
  email = '[email protected]';
  verified = 1;
  
  ngOnInit(): void {
  }
}
<-- !ironman.component.html -->
...
...
<tr>
      <td colspan="2">User Name</td>
      <td>
        <input type="text" value={{userName}}>
      </td>
</tr>
...
...

简单的一些改动,就让我们的 HTML 不再是写死的字串。但是,这样还是完全不行啊!你只不过是换个地方写死而已嘛!欸都…是的,目前还是,所以我们接下来要介绍另外一个系结。

属性系结(Property Binding)

好的,接下来,我们要继续改我们的 ironman 元件,让这个元件里不再存写死的资料,而是让别人把资料传给我们的 ironman,然後我们的 ironman 再把资料镶嵌到 HTML 上。

首先,我们先来定义一个使用者资料的介面(interface),这里特别注意一下,Angular 里的介面与 .NET 的介面有些不同,在 Angular 里如果我们只要定义资料的 model 而没有要实作处理逻辑,那我们会用 interface 而不是 class,选用 interface 的原因是,这些用 interface 定义的资料 model 在编译成 js 後会比较省资源。

export interface IronmanUser
{
  userId: number;
  userName: string;
  email: string;
  verified: number;
}

上面的宣告中,冒号(:)前面是物件的属性名称,後面是型别。

接着,把刚刚宣告的几个拆散的变数删掉,改成一个 IronmanUser 型别的变数然後给一些预设值,并用 @Input() 装饰器修饰这个变数,@Input() 会告诉 Angular 这个变数预期要接受从外部传进来的值。

@Input()
userInfo: IronmanUser = {
    userId: 0,
    userName: '',
    email: '',
    verified: 0
};

改完之後 HTML 的内嵌系结语法会出错,因为我们把资料搬进一个物件里了,稍作修改把这些 error 修掉

...
...
<tr>
      <td colspan="2">User Name</td>
      <td>
        <input type="text" value={{userInfo.userName}}>
      </td>
</tr>
...
...

这时候如果直接执行程序,我们的使用者资讯表格的值会是刚刚宣告的预设值,因为我们还没传值给它。现在,让我们从 app.component.ts 传值给 ironman.component.ts,到 app.component.ts 里,加入一个 IronmanUser 型别的变数,然後给有效的值

export class AppComponent {
  userInfoFromAppComponent: IronmanUser = {
    userId: 1,
    userName: 'Alice',
    email: '[email protected]',
    verified: 1
  }
  title = 'ironman-frontend';
}

再来,我们就要实际用属性系结把值传给 ironman.component.ts。被 @Input() 装饰器修饰的变数,对外部的使用者来讲,就好像是这个 Component 的属性,我们透过对这个属性赋值,就能把值传递给这个 Component。

到 app.component.html,用中括号选择 ironman.component.ts 所拥有的 @Input() 属性,然後把要传递的物件赋予这个属性。

<app-ironman [userInfo]=userInfoFromAppComponent></app-ironman>

重新执行程序,就会看到我们的资料正确的显示出来了~

什麽?你说我只不过是再换一个地方写死而已啊!欸都…目前好像的确是这样没错…,但是但是!请听我解释!以後我们只要把初始化这个变数的地方改成读档或抓 DB,就不会是写死的了!我们过几天也会讲到 Angular 的 HttpClient,到时候就会从我们之前写的 API 抓资料了。

事件系结(Event Binding)

刚刚所讲的属性系结是从外部元件(AppComponent)送资料给内部元件(IronmanComponent),而我们很多时候也会需要从内部把资料往外送,这个时候,我们就需要用到事件系结。事件系结的运作方式为:内部元件在某个条件下触发一个事件,事件发射器(EventEmitter)把这个这件跟资料一起往外送,当外部元件收到这个事件时,就能针对这个事件作处理。

要使用事件系结,首先我们要到内层(IronmanComponent)的 ts 档新增一个 @Output() 变数,这个变数固定会是泛型的 EventEmitter,後面的角括号里放我们的事件资料的型别。在 import EventEmitter 到档案中的时候,注意要选「从 "@angular/core"」。

@Output()
testOuputEvent = new EventEmitter<IronmanUser>();

有了这个事件发射器之後,我们就能在程序里自由的决定什麽时候要发送事件,这里,且让笔者偷懒,直接用 setTimeout() 发一个测试事件并携带一笔使用者资料

ngOnInit(): void {
    const mockInfo = {
      userId: 999,
      userName: 'testUser',
      email: '[email protected]',
      verified: 0
    }
    
    setTimeout(() => {
      this.testOuputEvent.emit(mockInfo)
    }, 1000);
}

而要接收事件的 AppComponent 则是在 HTML 中,用小括号指定接收刚刚宣告的 @Output 事件发射器变数所发的事件,并指定一个 function 来处理这个事件发生之後要做的事。

<!-- app.component.html -->
<app-ironman 
    [userInfo]=userInfoFromAppComponent 
    (testOuputEvent)=handleTestEvent($event)>
</app-ironman>
// app.component.ts
handleTestEvent(userInfo: IronmanUser): void {
    alert(`AppComponent 接收到 IronmanComponent 丢出的 userInfo: ${JSON.stringify(userInfo)}`)
}

上面的 $event 是固定用法,事件所携带的资料都会存在这个 $event 变数里。在目前的范例中,mockInfo 就会存在 $event 里,handleTestEvent() 的 userInfo 参数就会变成 IronmanComponent 丢出的 mockInfo。

https://ithelp.ithome.com.tw/upload/images/20210926/20140664xRGZKKxAx2.png

Property Binding + Event Binding = Two Way Binding

Day25 说要介绍三种 Binding 其实是有点错误的说法,因为有第四种:Two Way Binding(双向系结),如果属性系结是撒尿虾,事件系结是牛丸,那麽双向系结就是
https://ithelp.ithome.com.tw/upload/images/20210926/20140664UjBiEgZ2lr.png

现在,就让我们来做撒尿牛丸,到 ironman.component.ts,再宣告一个 @Output 变数,但是因为要做混在一起做撒尿牛丸,所以这个变数的名称一定是要 "@Input() 变数" 的变数名称 + Change,前面我们宣告 @Input() 变数叫做 userInfo,这个新的 @Output() 变数就必须叫 userInfoChange。

@Output()
userInfoChange = new EventEmitter<IronmanUser>();

接着,再让笔者偷懒一下,一样用 setTimeout() 发射事件

const modifiedInfo = {
  userId: 2,
  userName: 'Bob',
  email: '[email protected]',
  verified: 1
}

setTimeout(() => {
  this.userInfoChange.emit(modifiedInfo)
}, 1500);

最後,把 app.component.html 原本用中括号的属性系结改成中括号 + 小括号的双向系结,把 userInfo 包起来。

<app-ironman 
  [(userInfo)]=userInfoFromAppComponent 
  (testOuputEvent)=handleTestEvent($event)>
</app-ironman>

执行程序,就能看到我们画面上的使用者资料在事件触发之後,一起变成了内部元件发送出来的资料。也就是,双向系结的资料是纠缠在一起的,改变其中一端,另一端会同时被改变。
https://ithelp.ithome.com.tw/upload/images/20210926/20140664MfbXyhydOE.png


<<:  [铁人赛 Day11] React 原始码的初见面 ——官方 codebase 指南

>>:  JavaScript Day 17. 认识物件

IIS WordPress 永久连结如何移除 index.php 路径

WordPress 文章的永久连结有分几种模式,预设是「?p=123」这种方式 实际上的连结就变成这...

DAY30 - 完赛心得与下一步

第一次参加铁人赛,原本以为超前部署,开赛前两个星期就开始准备文章存档 本以为一定妥当的啦,没想到後面...

# Day 11 Cache and TLB Flushing Under Linux (三)

废话不多说,我们直接看文件~XD 文件 文件原文:Cache and TLB Flushing Un...

Day 11【连动 MetaMask - Pop Up & Login Detection】Can`t use current password.

【前言】 嗨嗨大家好,今天的主题延续昨天的检测是否已经安装插件後,紧接着而来的是 MetaMask...

Day4. 如何寻找设计切入点

在做新产品开发时,对於用户需求收集,寻找产品切入点,我们常有一个典型的错误假设,那就是认为用户最知...