第 10 天 别说吕布了,你听过青铜五小强吗 |Template-driven-form、ngModel、Template variables

前情提要

昨日我们聊了一些关於「页面」与「元件」在规划上,可能需要注意的地方。今天,我们会实际带着「页面还是元件」这样的问题意识,来实作新增英雄表单功能,在表单方面,我们将演示范本驱动的表单。

Angular 在表单方面,提供了「范本驱动」(Template driven form)及「响应式」(Reactive From)两种不同的方式。简要来说,前者多数程序会撰写在 HTML 档案、处理较简单的表单内容,後者多数程序会撰写在 TS 档案,可以更好地处理逻辑较复杂的表单。

008

(你终於领悟了要怎麽召唤英雄了。不过,你後面背一个大卷轴是在干嘛?)

看来你是不懂什麽叫做仙人模式啊,大闲人...话说你消失了很多天?

(哼哼...上次因为经费不足让英雄阵容缩编,让我痛定思痛...)

你真的去买狗狗币了?

(不,我发现应该要寻找廉价劳工热血的有志之士!)

听着都不合法啊。

(你知道有一种人,不怎麽厉害,但是怎麽打也打不死。然後突然就小宇宙爆发了吗?)

我好像知道你在说什麽...

(而且就纪录片来看,他们根本是弑神专门户啊!)

呃,你说的纪录片是平面的还是 3D?

https://ithelp.ithome.com.tw/upload/images/20210925/20128395HNGfHpiup7.jpg
图片来源:GreatGame

(特别是有个叫瞬的,只要招募到他,连他哥哥都会免费加入了,买一送一!而且他哥超厉害。)

你再说下去,我都不知道自己在睡觉还是中了什麽幻魔拳了。

(总之我们快想办法来让他们签下去吧!)

参考资料:不负责任疯动漫。《【特别加映】圣斗士星矢之五小强成长史「黄金12宫篇」》。

规划新增英雄功能

如同昨日的讨论,「新增英雄」功能会需要填写「英雄资料表单」,而可以想见的是,日後大概率会提供「编辑英雄」功能,而它也应该是编辑一样的「英雄资料表单」。但如果我们直接将「英雄资料表单」元件当作两个功能路径对应的页面的话,那可能会需要额外处理逻辑,幅度随两个页面在画面上的异同程度增减。

一个比较好的做法应该是,建立两个页面层级的元件「新增英雄」页面元件及「编辑英雄」页面元件,并将「英雄资料表单」作为一个共用元件,目前的专案结构规划应该是这样:

src
⌞app
  ⌞ pages
      ⌞ AddHeroPageComponent
      ⌞ EditHeroPageComponent
  ⌞ shared
      ⌞ components
          HeroInfomationFormComponent

依序输入下列指令:

ng g c pages/add-hero-page --skip-selector // 新增英雄页面元件
ng g c pages/edit-hero-page --skip-selector // 编辑英雄页面元件
ng g c shared/components/hero-information-form // 英雄资讯表单元件

并配置对应的路由,编辑 app-routing.module.ts,将两个页面元件配置在路径 'heroes' 下的 'add'、'edit':

const routes: Routes = [
  {
    path: '',
    redirectTo: '/heroes',
    pathMatch: 'full'
  },
  {
    path: 'heroes',
    children: [
      {
        path: '',
        component: HeroListComponent
      },
      {
        path: ':id',
        component: HeroDetailComponent,
      },
      {
        path: 'add',
        component: AddHeroPageComponent
      },
      {
        path: 'edit',
        component: EditHeroPageComponent
      }
    ]
  },
]

这时候如果我们启动应用程序,并输入路径 http://localhost:4200/heroes/addhttp://localhost:4200/heroes/edit 会发现画面一片空白,并且 console 都会出现 response error:

https://ithelp.ithome.com.tw/upload/images/20210925/20128395RlUoPjeSSs.png

发生了什麽事呢?这是因为我们将新增的两个路由配置到参数路由 :id 之後,因此,接在 heroes 之後的 addedit 均被视为 id 参数,因此导向 HeroDetailComponent 并执行取得个别英雄资料的方法 getHero(heroId),因为後台查询不到匹配这两个 id( addedit) 的英雄,因此产生 Response Error。

因为一旦路由匹配成功,就不会继续往下观察路径。所以放在参数路径之後的路由都是无效的,我们应将新增的两个路由配置到参数路由之前:

const routes: Routes = [
  {
    path: '',
    redirectTo: '/heroes',
    pathMatch: 'full'
  },
  {
    path: 'heroes',
    children: [
      {
        path: '',
        component: HeroListComponent
      },
      {
        path: 'add',
        component: AddHeroPageComponent
      },
      {
        path: 'edit',
        component: EditHeroPageComponent
      },
      {
        path: ':id',
        component: HeroDetailComponent,
      },
    ]
  },
]

如此一来,就能正常进入新配置的路由:

https://ithelp.ithome.com.tw/upload/images/20210925/201283957Jet0oIeh6.png

接着让我们来实作英雄资讯表单这个共用元件。

实作英雄资讯表单元件

目前我们的英雄资料模型如下(hero.model.ts):

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

这边作了稍微的调动,我们将 id 属性给为选择性的(可以不提供这个属性)。为什麽呢?这是因为大多时候,id 是由後端产生的,也就是说,在新增英雄时我们不需要传送 id 属性。

当然这可能不是一个很好的做法,也许会造成解读上的误会(原来英雄可以不用有 id?)。可以采用的方法至少有:

  • 新增另外一个 AddHero 介面,在这个介面中不包含 id 属性。
  • 将原先的 Hero 属性删除 id 属性,并新增一个 DisplayHero 属性来继承 Hero,并扩充出 id 属性。用它来负责前端资料的模型。
  • ...

不过为了方便演示,目前我们先将 id 属性作为一个选填属性,将焦点放在完成表单。

首先,我们要在 AppModule 先汇入 FormsModule,这样我们才可以使用 Template-driven Form 相关的指令:

import { FormsModule } from '@angular/forms';
(略)
@NgModule({
(略)
  imports: [
    (略)
    FormsModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

我们以 name 属性为例来讨论,在范本驱动表单的模式下,可以写为下面这样(hero-information-form.component.html):

  <div class="form-field">
    <label for="name">NAME</label>
    <input
      #tName="ngModel"
      name="name"
      ngModel
      required
      type="text"
      id="name" />
  </div>

解释一下与 Angular 相关的程序码。

最核心的就是 ngModel 指令,这个指令会产生一个表单控制项(FormControl)的实例。听起来就控场控很大,没错!表单控制项就是 Angular Form 的场控基石(连欧拉夫都解不了)。表单控制项可以让我们获得该栏位的状态资讯,比较常使用到的包含:

  • value // 现在的值
  • valid/invalid // 是否合法
  • errors // 是否有错误
  • touched/untouched // 表单是否已被使用者接触过(眼神不算)
  • dirty/pristine // 表单是否被编辑过/原初状态

并且会提供相应於上述表单控制状态的 class(例如不合法时提供 ng-invalid class),因此,你可以很方便地完成表单状态样式的显示。

当你使用了 ngModel 指令後,你就可以继续使用检核相关的指令,例如:

  • required // 此栏位为必填
  • email // 此栏位输入格式须符合 email
  • minlength // 此栏位最少字元限制
  • maxlength // 此栏位最多字元限制
  • pattern // 此栏位输入格式须符合指定的正则表达式
  • ...

在姓名栏位,我们使用了 required 检核指令,标示这是一个必填栏位。

这样我们就完成了一个表单栏位的设定。但在画面上,我们常常需要知道检核状态,例如需要知道它是否有错误、要显示错误讯息。因此我们把这个表单控制项的实例指派给一个范本参考变数(也就是 #tName)。如此一来,我们就可以在 HTML 档案中,以 tName 来使用表单控制项提供的各种场控技能。

例如我们新增一个「储存」按钮,并设置它在这个名称栏位不合法的时候(没有填写)是 disabeld 的:

<div class="form-field">
  <label for="name">NAME</label>
  <input
    #tName="ngModel"
    name="name"
    ngModel
    required
    type="text"
    id="name" />
</div>

<button
  type="button"
  [disabled]="tName.invalid">
  储存
</button>

我们先在 AddHeroPageCompoent 使用这个表单元件来看看效果:

<h1>新增英雄</h1>
<app-hero-information-form></app-hero-information-form>

画面如下,在没有输入值的时候,按钮无法点击的:

https://ithelp.ithome.com.tw/upload/images/20210925/20128395Ya5hcxBcXu.png

当输入之後,就可以点击了:

https://ithelp.ithome.com.tw/upload/images/20210925/20128395oMumODLcAH.png

透过范本参考变数(Template variables)#tName 和表单控制项实例(ngModel)的配合,就可以很轻松地产出动态检核栏位。明天会完成这个英雄资讯表单,并优化它的画面。

今天的程序码已推上 Github


<<:  【Day 16】混合云 x AWS Outposts 开箱文

>>:  Day 10 JavaScript CSS in JS

Day13. 有了Blue Prism,谁说办公室恋情影响工作-BP的用途

经历过一连串的Blue Prism实作,今天想让大家与自己都松口气, 看到上面的图案,是否有种机械...

[Day - 30] 不完美的结束

最後,还是到了这最後一天,这第 30 天不完美的完赛,有时候时常都会想,上班就很忙了,开的 Tick...

23 - 建立结构化的 Log (1/4) - Elastic Common Schema 结构化 Log 的规范

建立结构化的 Log 系列文章 (1/4) - Elastic Common Schema 结构化 ...

[Day28] Security

在网路世界中,安全永远是最最重要的事情,而云端安全当然也不例外。任何的安全问题都来自於人为的疏忽,部...

环境配置(node/golang)(Day3)

接续上篇提到的内容,这篇提到的主要会是golang与react会需要的环境配置 小提醒 在下面会有提...