第 5 天 还我漂漂拳| property binding、interface

前情提要

将英雄们显示在 Mat-Card 上後,我们进一步地要对英雄资料做点加工,并且制作英雄详细介绍页面。今天会完成下列事项:

  • 使用 TypeScript 的 interface 来制作资料模型档案(model)。
  • 将某些英雄资料使用 property binding 放到画面上。
  • 制作英雄介绍元件。

007

现在这些狗狗都召唤出来了,今天天气不错,请问现在是要去遛狗吗?

(说话小心一点,吕布在你後面他很火。)

请问,这群狗是要怎麽拯救世界呢?他们是能变狗狗币吗?

(狗狗币你确定不是来乱的吗...看来你被外表所迷惑了啊。今天我们来让你对这些英灵有更深刻的认识吧!)

你是说品种之类的吗,我看他们品种都一样啦...。

制作资料模型档案

在 TypeScript 里,我们可以用介面(interface)来制作资料模型档案。这可以在开发期间提供很大的帮助,比如在资料传递时提供静态检核,或是在使用物件时,能够提示你物件所拥有的属性,大幅地减少拼字错误的可能性——这些,都仰赖我们确实地制作资料模型档案。

我们可以将资料模型档案放置在 shared 目录下:

src
⌞app
  ⌞ shared
      ⌞ models
          hero.model.ts

现在我们在 hero.model.ts 档案制作规范英雄物件会有的属性或方法:

export interface Hero {
  id: number;        // id
  name: string;      // 姓名
  image?: string;    // 图像
  hp: number;        // 生命值
  attack: number;    // 攻击力
  defence: number;   // 防御力
  weapon?: string;   // 武器
  skill?: string;    // 必杀技
}

在介面中,若後方带有 ? 表示这是可选属性,物件中可能有这个属性、也可能没有。例如,英雄可能不会配有武器(weapon),例如叶问。叶问拿武器就好比 macbook 配滑鼠会被笑的(乱讲)。

接着,我们就制作好的介面放置到对应的位置,打开 hero-list.component.ts,我们在使用 Http 请求英雄资料时,同时宣告会取得的资料型态(在 http.get後方加上 <Hero[]>):

import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { Hero } from './../shared/models/hero.model';
@Component({
  selector: 'app-hero-list',
  templateUrl: './hero-list.component.html',
  styleUrls: ['./hero-list.component.css']
})
export class HeroListComponent implements OnInit {

  heroList: Hero[] = [];

  constructor(
    private http: HttpClient
  ) {}

  ngOnInit(): void {
    this.http.get<Hero[]>('api/heros').subscribe((heroList) => {
      this.heroList = heroList;
    });
  }
}

同时,也在宣告属性 heroList 的同时标注他的介面为 Hero[]。这样当取得资料型态不一致时,在开发阶段就会看到错误讯息。也因为确实地标明了属性的介面,在开发时就可以享受到属性提示的好处:

https://ithelp.ithome.com.tw/upload/images/20210920/20128395qMPbd7z4JE.png

因为我们的经费没有那麽多,请不起这麽多的英灵(其实是为了演示方便),我们先将英灵总数调降为 3 人,并为他们填上较详尽的资料,请将下列资料更新到 db.json 中:

{
  "heros": [
    {
      "id": 1,
      "name": "吕布奉先",
      "image": "https://i0.zi.org.tw/ddm/2021/06/1624399697-0ad6a83f8d158a95db281bdd223a56b1.jpg",
      "hp": 999,
      "attack": 900,
      "defence": 900,
      "weapon": "方天画戟",
      "skill": "天喰",
      "description": "出身於三国时代,号称是中华历史中个人战力最强的男人,武器为方天画戟,必杀技为「天喰」。"
    },
    {
      "id": 2,
      "name": "亚当",
      "image": "https://i0.zi.org.tw/ddm/2021/06/1624400056-7d129b933d846f4350625816bb9d5992.jpg",
      "hp": 500,
      "attack": 999,
      "defence": 100,
      "weapon": "指虎",
      "skill": "神虚视",
      "description": "为全世界人类的爸爸,是神按照自己的模样打造的第一个人类,人类档案编号为00000000001,也是最接近神的男人,拥有必杀技「神虚视」,能复制所有的技能,不过他的眼力是有上限的。"
    },
    {
      "id": 3,
      "name": "佐佐木小次郎",
      "image": "https://i0.zi.org.tw/ddm/2021/06/1624400405-c2a2d77d29397a6bfef69367fbeba681.jpg",
      "hp": 600,
      "attack": 700,
      "defence": 600,
      "weapon": "备前长船长光",
      "skill": "双燕斩虎万仞缭乱",
      "description": "日本战国时代的剑士,剑派为岩流,被喻为是「史上最强大的失败者」,他屡战屡败,但他会从每次的失败中学习并进步,即使到了地狱依然不断在学习剑术,他拥有能预判敌人动作的「千手无双」技能,做出事先的闪躲,而必杀技为结合日本各大剑术流派的集大成招式「双燕斩虎万仞缭乱」"
    }
  ]
}

资料来源:多多看电影。《终末的女武神》对战名单|13位最强人类+13最强神只 必杀技角色解析总整理!

并调整 hero-list.component.html 显示的资料:

<div class="hero-container">
  <mat-card class="hero-item" *ngFor="let hero of heroList">
    <mat-card-header>
      <div mat-card-avatar></div>
      <mat-card-title>{{ hero.name }}</mat-card-title>
    </mat-card-header>
    <img mat-card-image [src]="hero.image" [alt]="hero.name">
    <mat-card-content>
      <p>
        {{ hero.description }}
      </p>
    </mat-card-content>
    <mat-card-actions>
      <button mat-button>LIKE</button>
      <button mat-button>SHARE</button>
    </mat-card-actions>
  </mat-card>
</div>

这里,我们在 中使用了属性系结(property binding),将 <img> 拥有的属性放入英雄资料。目前我们的画面如下:

https://ithelp.ithome.com.tw/upload/images/20210920/20128395pt8pclPKdQ.png

制作英雄细节元件(HeroDetailComponent)

现在我们要制作一个英雄资讯细节元件,让使用者可以进一步浏览英雄资讯,让我们到 app 资料夹下执行:

ng g c hero-detail // g for generate, c for component

档案目录结构如下:

src
⌞app
  ⌞ hero-list
  ⌞ hero-detail
      ⌞ hero-detail.component.html
      ⌞ hero-detail.component.css
      ⌞ hero-detail.component.ts

接着分别编辑下列档案:
hero.detail.component.ts:

import { Component, Input, OnInit } from '@angular/core';

import { Hero } from '../shared/models/hero.model';

@Component({
  selector: 'app-hero-detail',
  templateUrl: './hero-detail.component.html',
  styleUrls: ['./hero-detail.component.css']
})
export class HeroDetailComponent implements OnInit {

  @Input() hero: Hero | null = null;

  constructor() { }

  ngOnInit(): void {
  }

}

@Input() 是个装饰器,当一个属性加上此装饰器时,使用 HeroDetailComponent 元件的地方,就可以针对这个属性 hero 使用属性系结来传递资料。等等我们也会在 HeroListComponent 使用 HeroDetailComponent,并对 hero 进行属性系结。

以及hero.detail.component.html:

<mat-card *ngIf="hero">
  <mat-card-header>
    <div mat-card-avatar></div>
    <mat-card-title>{{ hero.name }}</mat-card-title>
  </mat-card-header>
  <img mat-card-image [src]="hero.image" [alt]="hero.name">
  <mat-card-content>
    <mat-list>
      <mat-list-item>HP: {{ hero.hp}}</mat-list-item>
      <mat-divider></mat-divider>
      <mat-list-item>Attack: {{ hero.attack }}</mat-list-item>
      <mat-divider></mat-divider>
      <mat-list-item>Defence: {{ hero.defence}}</mat-list-item>
      <mat-divider></mat-divider>
      <mat-list-item>Weapon: {{ hero.weapon}}</mat-list-item>
      <mat-divider></mat-divider>
      <mat-list-item>Skill:  {{ hero.skill}}</mat-list-item>
      <mat-divider></mat-divider>
    </mat-list>
  </mat-card-content>
</mat-card>

并且,因为使用了更多 Angular Material 元件,我们也要在 app.module.ts 汇入 MatDividerModuleMatListModule

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatListModule } from '@angular/material/list';

import { AppComponent } from './app.component';
import { HeroListComponent } from './hero-list/hero-list.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';

@NgModule({
  declarations: [
    AppComponent,
    HeroListComponent,
    HeroDetailComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    BrowserAnimationsModule,
    MatCardModule,
    MatButtonModule,
    MatDividerModule,
    MatListModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

我们已经完成了 HeroDetailComponent ,现在我们将它放到 HeroListComponent 上来使用。请调整 hero-list.component.html

<div class="hero-container">
  <mat-card class="hero-item" *ngFor="let hero of heroList">
    <mat-card-header>
      <div mat-card-avatar></div>
      <mat-card-title>{{ hero.name }}</mat-card-title>
    </mat-card-header>
    <img mat-card-image [src]="hero.image" [alt]="hero.name">
    <mat-card-content>
      <p>
        {{ hero.description }}
      </p>
    </mat-card-content>
    <mat-card-actions>
      <button mat-button (click)="viewHeroDetail(hero.id)">浏览细节</button>
      <button mat-button>SHARE</button>
    </mat-card-actions>
  </mat-card>
</div>

<app-hero-detail [hero]="selectedHero"></app-hero-detail>

我们在下方使用新元件 <app-hero-detail>,并属性系结 hero ,传入的资料为 selectedHero。而在第一个按钮,我们绑定 click 事件,当发生 click 事件时就要呼叫 viewHeroDetail 方法,并且要将 hero.id 作为参数传入此方法中。

我们的规划是:viewHeroDetail 这个方法会找到选择英雄资料,将资料赋予 selectedHero。之後 selectedHero 便会透过属性系结(绑定 HeroDetailComponent 的 hero 属性),selectedHero 便会将自己传入 <app-hero-detail> 中。现在,我们来 hero-detail.component.ts 实现这个机制:

import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { Hero } from './../shared/models/hero.model';
@Component({
  selector: 'app-hero-list',
  templateUrl: './hero-list.component.html',
  styleUrls: ['./hero-list.component.css']
})
export class HeroListComponent implements OnInit {

  heroList: Hero[] = [];
  selectedHero: Hero | null = null;

  constructor(
    private http: HttpClient
  ) {}

  ngOnInit(): void {
    this.http.get<Hero[]>('api/heros').subscribe((heroList) => {
      this.heroList = heroList;
    });
  }

  viewHeroDetail(heroId: number): void {
    this.selectedHero = this.heroList.find((hero) => hero.id === heroId)!;
  }

}

目前的画面如下,如果点击不同英雄卡片中的按钮「浏览细节」,下方的细节资讯就会切换显示。

https://ithelp.ithome.com.tw/upload/images/20210920/20128395xKlOLTibsi.png


<<:  【Day 7】机器学习基本功(五)

>>:  什麽是前端工程师?

[Java Day27] 6.4. 多型

教材网址 https://coding104.blogspot.com/2021/06/java-p...

【C#】Structural Patterns Bridge Mode

The Bridge design pattern decouples an abstraction...

Ruby 学习笔记簿:Metaprogramming Workshop - Before Action

实作前准备 需要先了解以下主题: Method Wrappers: Around Aliases M...

[Day4] 安装Django

夥伴们大家好,今天要来说明如何安装Django啦~~~ 但是在安装前我们要先查看一下,我们使用的py...

Day 12 - 动态组件(Dynamic Components)

动态元件(Dynamic-Components)是指Vue可以根据传入参数的,来去切换不同的元件。 ...