Angular 4 Template-Driven Forms

Angular 4.x 中有两种表单:

  • Template-Driven Forms - 模板驱动式表单 (类似于 Angular 1.x 中的表单 )
  • Reactive Forms (Model-Driven Forms) - 响应式表单

本文主要介绍 Template-Driven Forms (模板驱动式表单) ,将涉及 ngFormngModelngModelGroup、表单提交事件、表单验证和异常信息输出等内容。

Contents

  • ngModule and template-driven forms
  • Binding ngForm and ngModel
  • ngModel,[ngModel] and [(ngModel)]
  • ngModels and ngModelGroup
  • Template-driven submit
  • Template-driven error validation

Form base and interface

Form base

<form novalidate>
  <label>
    <span>Full name</span>
    <input
      type="text"
      name="name"
      placeholder="Your full name">
  </label>
  <div>
    <label>
      <span>Email address</span>
      <input
        type="email"
        name="email"
        placeholder="Your email address">
    </label>
    <label>
      <span>Confirm address</span>
      <input
        type="email"
        name="confirm"
        placeholder="Confirm your email address">
    </label>
  </div>
  <button type="submit">Sign up</button>
</form>

接下来我们要实现的功能如下:

  • 绑定 name、email、confirm 输入框的值
  • 为所有输入框添加表单验证功能
  • 显示验证异常信息
  • 表单验证失败时,不允许进行表单提交
  • 表单提交功能

User interface

// signup.interface.ts
export interface User {
  name: string;
  account: {
    email: string;
    confirm: string;
  }
}

ngModule and template-driven forms

在我们继续深入介绍 template-driven 表单前,我们必须在 @NgModule 中导入 @angular/forms 库中的 FormModule

import { FormsModule } from '@angular/forms';

@NgModule({
  imports: [
    ...,
    FormsModule
  ],
  declarations: [...],
  bootstrap: [...]
})
export class AppModule {}

友情提示:若使用 template-driven 表单,则导入 FormsModule;若使用 reactive forms,则导入 ReactiveFormsModule。

Template-driven approach

使用模板驱动的表单,我们基本上可以将组件类留空,直到我们需要读取/写入值 (例如提交和设置初始值)。我们将基于上面的定义的基础表单,创建 SignupFormComponent

signup-form.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'signup-form',
  template: `
    <form novalidate>...</form>
  `
})
export class SignupFormComponent {
  constructor() {}
}

这是一个很基础的组件,接下来我们导入之前定义的 User 接口,具体如下:

import { User } from './signup.interface';

@Component({...})
export class SignupFormComponent {
  public user: User = {
    name: '',
    account: {
      email: '',
      confirm: ''
    }
  };
}

初始化 SignupFormComponent 组件类中的用户模型后,我们开始实现第一个功能点:即绑定 name、email、confirm 输入框的值。

Binding ngForm and ngModel

我们从 ngForm 开始,更新后的模板如下:

<form novalidate #f="ngForm">
  <label>
    <span>Full name</span>
    <input type="text" placeholder="Your full name">
  </label>
</form>

上面代码中,我们把 ngForm 的值赋值给 #f 变量,通过该变量我们可以方便的获取表单的值。

友情提示:#f 变量的值,是 ngForm 指令的导出对象。

@Directive({
  selector: 'form:not([ngNoForm]):not([formGroup]),ngForm,[ngForm]',
  providers: [formDirectiveProvider],
  host: {'(submit)': 'onSubmit($event)', '(reset)': 'onReset()'},
  outputs: ['ngSubmit'],
  exportAs: 'ngForm'
})
export class NgForm extends ControlContainer implements Form {}

在模板中,我们可以通过以下方式查看表单的值:

{{ f.value | json }} // {}

上面示例 f.value 输出 {},因为此时我们表单中还未绑定任何值。在 Angular 1.x 中我们可以使用 ng-model 指令进行表单数据的双向绑定,接下来我们来看一下 Angular 4.x 中怎么实现数据绑定。

ngModel,[ngModel] and [(ngModel)]

在 Angular 4.x 中 ngModel 有三种不同的语法:

  • ngModel - 直接使用 ngModel 指令,没有使用绑定或关联任何值。此时,ngModel 将自动关联表单控件的 name 属性,并使用该值作为 ngForm 对象的属性名。
<form novalidate #f="ngForm">
  ...
    <input
     type="text"
     placeholder="Your full name"
     name="name"
     ngModel>
  ...
</form>

友情提示:上面示例中,如果 input 输入框若未设置 name 属性,应用将会抛出异常。ngModel 指令基于输入框的 name 属性,进行绑定。

运行以上代码,f.value 的输入值如下:

{{ f.value | json }} // { name: '' }

非常好,我们已经绑定了 name 输入框的值。但我们应该怎么为输入框设置初始值?

  • [ngModel] = one-way binding syntax (单向绑定语法)

为了设置输入框初始值,我们先要更新一下 SignupFormComponent 组件类的用户模型:

...
user: User = {
  name: 'Semlinker',
  account: {
    email: '',
    confirm: ''
  }
};
...

更新完用户模型,我们需要同步更新组件模板,具体如下:

<form #f="ngForm">
  ...
    <input
      type="text"
      placeholder="Your full name"
      name="name"
      [ngModel]="user.name">
  ...
</form>

代码重新运行后,f.value 的输出如下:

{{ f.value | json }} // { name: 'Semlinker' }

从上面示例可以看出,使用 [ngModel] 允许我们通过 this.user.name 设置 name 输入框的初始值,而且该值会自动绑定到 f.value 对象上。

友情提示:[ngModel] 是单向绑定,当表单中 name 输入框的值改变时,不会同步更新 this.user.name

如果想在 name 输入框值变化时,自动同步更新 this.user.name 的值,我们需要使用双向绑定。

  • [(ngModel)] = two-way binding syntax (双向绑定),具体示例如下:
<form #f="ngForm">
  ...
    <input
      type="text"
      placeholder="Your full name"
      name="name"
      [(ngModel)]="user.name">
  ...
</form>

上面示例成功运行后,我们可以在模板中新增以下代码,然后观察 user 模型的值:

{{ user | json }} // { name: 'Semlinker' }

需要注意的是,以下两种方式是等价的:

<input [(ngModel)]="user.name">
<input [ngModel]="user.name" (ngModelChange)="user.name = $event">

其中 [(ngModel)] 是简写的语法糖。

ngModels and ngModelGroup

我们已经介绍了 ngFormngModel 的基础用法,现在我们来完善剩下的内容。SignupFormComponent 组件类的用户模型中,包含了一个嵌套属性 account ,account 对象中包含 emailconfirm 属性,分为表示邮件地址和重复确认的邮件地址。针对这种场景,Angular 4.x 为我们提供了 ngModelGroup 指令,具体示例如下:

<form novalidate #f="ngForm">
  <label>
    <span>Full name</span>
    <input
      type="text"
      placeholder="Your full name"
      name="name"
      ngModel>
  </label>
  <div ngModelGroup="account">
    <label>
      <span>Email address</span>
      <input
        type="email"
        placeholder="Your email address"
        name="email"
        ngModel>
    </label>
    <label>
      <span>Confirm address</span>
      <input
        type="email"
        placeholder="Confirm your email address"
        name="confirm"
        ngModel>
    </label>
  </div>
  <button type="submit">Sign up</button>
</form>

使用 ngModelGroup 指令后,我们的 DOM 结构将更加合理:

ngForm -> '#f'
    ngModel -> 'name'
    ngModelGroup -> 'account'
                 -> ngModel -> 'email'
                 -> ngModel -> 'confirm'

以上代码成功运行后,浏览器中页面显示的结果:

// { name: 'Semlinker', account: { email: '', confirm: '' } }
{{ f.value | json }}

此时我们已经完成了表单数据绑定,接下来我们来为表单增加提交逻辑。

Template-driven submit

Angular 表单中提供了 ngSubmit 输出属性,用于监听表单的提交事件:

<form novalidate (ngSubmit)="onSubmit(f)" #f="ngForm">
  ...
</form>

当用户提交表单时,我们将会把 f 作为参数,调用 ngSubmit 关联的 onSubmit() 方法。onSubmit() 方法的具体实现如下:

export class SignupFormComponent {
  user: User = {...};
  onSubmit({ value, valid }: { value: User, valid: boolean }) {
    console.log(value, valid);
  }
}

上面代码中,我们使用 Object destructuring (对象解构) 的方式,从#f 引用对象中获取 valuevalid 属性的值。其中 value 的值,就是 f.value 的值。表单的数据绑定方式和提交逻辑已经介绍完了,是该介绍表单实际应用中,一个重要的环节 — 表单验证。

Template-driven error validation

在为表单项添加验证规则前,我们先来更新一下 SignupFormComponent 组件中的 Sign up 按钮,确保在表单验证不通过时,不允许用户执行表单提交操作。更新后的代码如下:

<form novalidate (ngSubmit)="onSubmit(f)" #f="ngForm">
  ...
  <button type="submit" [disabled]="f.invalid">Sign up</button>
</form>

以上代码我们通过 f.invalid 获取表单当前的验证状态 (验证不通过时该值为true),来控制按钮的 disabled 属性。

接下来开始进入正题,为表单添加验证规则:

<form novalidate #f="ngForm">
  <label>
    ...
    <input
      ...
      ngModel
      required>
  </label>
  <div ngModelGroup="account">
    <label>
      ...
      <input
        ...
        name="email"
        ngModel
        required>
    </label>
    <label>
      ...
      <input
        ...
        name="confirm"
        ngModel
        required>
    </label>
  </div>
  <button type="submit">Sign up</button>
</form>

上面代码中,我们为每个 input 表单控件,添加了 required (必填项) 的验证规则。一切都那么简单,剩下的问题就是如何获取验证失败的异常消息。

皇上,您还记得当年大明湖畔的夏雨荷吗? — No,No,No !我只记得安谷拉 (angular) 湖畔的美女 (f)。

#f 引用对象中有一个 controls 属性,通过该属性,我们就可以获取表单控件的验证信息,下面示例演示了如何获取 name 表单控件验证的异常信息:

<form novalidate #f="ngForm">
  {{ f.controls.name?.errors | json }}
</form>

f.controls.name?.errors 的值是 nullundefined 时,表示验证成功。

友情提示:?.prop 称为安全导航操作符,用于告诉 Angular prop 的值可能不存在。

接下来为我们的 name 表单控件,添加显示异常信息的代码:

<div *ngIf="f.controls.name?.required" class="error">
  Name is required
</div>

虽然我们已经可以获取某个表单项的验证信息,但有没有觉得使用 f.controls.name?.errors 这种方式,太麻烦了。那么有没有更简单的方式呢?我的答案是 - Yes !废话不多说,马上看示例:

<label>
  ...
  <input
    ...
    #userName="ngModel"
    required>
</label>
<div *ngIf="userName.errors?.required" class="error">
  Name is required
</div>

(备注:此处一定要使用 #userName="ngModel")

以上代码成功运行后,我们在浏览器中看到了异常信息,为了避免一开始就显示异常信息,我们可以更新一下 *ngIf 表达式的验证逻辑:

<div *ngIf="userName.errors?.required && userName.touched" 
     class="error">
  Name is required
</div>

除了使用 required 验证规则之外,我们还可以使用 minlength (最小长度)、maxlength (最大长度) 等验证规则,下面我们继续来完善 SignupFormComponent 组件的功能,即为其它的表单控件添加显示异常信息的功能:

<!-- name -->
<div *ngIf="userName.errors?.required && userName.touched" 
     class="error">
  Name is required
</div>
<div *ngIf="userName.errors?.minlength && userName.touched" 
     class="error">
  Minimum of 2 characters
</div>

<!-- account: { email, confirm } -->
<div *ngIf="userEmail.errors?.required && userEmail.touched" 
     class="error">
  Email is required
</div>
<div *ngIf="userConfirm.errors?.required && userConfirm.touched" 
     class="error">
  Confirming email is required
</div>

我们通过使用模板变量的方式,为 account 表单组添加了显示验证异常信息的功能。但有没有其它更好的方式呢?有没有办法去掉 userEmailuserConfirm 引用对象呢?答案是肯定的,具体示例如下:

<div ngModelGroup="account" #userAccount="ngModelGroup">
  <label>
    <span>Email address</span>
    <input
      type="email"
      placeholder="Your email address"
      name="email"
      ngModel
      required>
  </label>
  <label>
    <span>Confirm address</span>
    <input
      type="email"
      placeholder="Confirm your email address"
      name="confirm"
      ngModel
      required>
  </label>
  <div *ngIf="userAccount.invalid && userAccount.touched" class="error">
    Both emails are required
  </div>
</div>

(备注:记得移除模板上的 #userEmail#userConfirm 引用哈)

我有话说

表单控件的状态除了 touched 外,还包含其它几种状态?

表单控件有以下 6 种状态,我们可以通过 #userName="ngModel" 方式获取对应的状态值。具体状态如下:

  • valid - 表单控件有效
  • invalid - 表单控件无效
  • pristine - 表单控件值未改变
  • dirty - 表单控件值已改变
  • touched - 表单控件已被访问过
  • untouched - 表单控件未被访问过

表单控件上 #userName#userName="ngModel" 这两种方式有什么区别?

  • #userName - 指向 input 表单控件
  • #userName="ngModel" - 指向 NgModel 实例

ngModel 指令

// angular2/packages/forms/src/directives/ng_model.ts 片段
@Directive({
  selector: '[ngModel]:not([formControlName]):not([formControl])',
  providers: [formControlBinding],
  exportAs: 'ngModel' // // 导出指令实例,使得可以在模板中调用
})
export class NgModel extends NgControl implements OnChanges, OnDestroy {
}

NgControl 抽象类

// angular2/packages/forms/src/directives/ng_control.ts 片段
export abstract class NgControl extends AbstractControlDirective {
  /** @internal */
  _parent: ControlContainer = null;
  name: string = null;
  valueAccessor: ControlValueAccessor = null;
  ...
  abstract viewToModelUpdate(newValue: any): void;
}

AbstractControlDirective 抽象类

// angular2/packages/forms/src/directives/abstract_control_directive.ts 片段
export abstract class AbstractControlDirective {
  get valid(): boolean { return this.control ? this.control.valid : null; }

  get invalid(): boolean { return this.control ? this.control.invalid : null; }

  get errors(): ValidationErrors | null { return this.control ? 
      this.control.errors : null; }

  get pristine(): boolean { return this.control ? this.control.pristine : null; }

  get dirty(): boolean { return this.control ? this.control.dirty : null; }

  get touched(): boolean { return this.control ? this.control.touched : null; }

  get untouched(): boolean { return this.control ? this.control.untouched : null; }

  get valueChanges(): Observable<any> { return this.control ? 
      this.control.valueChanges : null; }

  hasError(errorCode: string, path: string[] = null): boolean {
    return this.control ? this.control.hasError(errorCode, path) : false;
  }

  getError(errorCode: string, path: string[] = null): any {
    return this.control ? this.control.getError(errorCode, path) : null;
  }
}

ngModelGroup 有什么作用?

ngModelGroup 指令是 Angular 提供的另一特殊指令,可以对表单输入内容进行分组,方便我们在语义上区分不同性质的输入。例如联系人的信息包括姓名及住址,现在需对姓名和住址进行精细化信息收集,姓名可精细化成姓和名字,地址可精细化成城市、区、街等。此时就可以将姓名及住址进行分组收集,具体如下:

<form #concatForm = "ngForm">
    <fieldset ngModelGroup="nameGroup" #nameGroup="ngModelGroup">
        <label>姓:</label>
        <input type="text" name="firstname" [(ngModel)]="curContact.firstname" 
               required> 
        <label>名字:</label>
        <input type="text" name="lastname" [(ngModel)]="curContact.lastname"    
               required>
    </fieldset>
    <fieldset ngModelGroup="addressGroup" #addressGroup ="ngModelGroup">
        <label>街:</label>
        <input type="text" name="street" [(ngModel)]="curContact.street" required>          <label>区:</label>
        <input type="text" name="zip" [(ngModel)]="curContact.zip" required> 
        <label>城市:</label>
        <input type="text" name="city" [(ngModel)]="curContact.city" required>
    </fieldset>
</form>

上述例子分别对联系人的姓名和住址进行分组, ngModelGroup 将姓和名字的表单内容进行包裹组成姓名分组,将城市、区和街道的表单内容进行包裹组成住址分组。此时concatForm.value值为:

{
  nameGroup: {
    firstname: '',
    lastname: '',
  },
  addressGroup: { 
    street: '', 
    zip: '', 
    city: ''
  } 
}

参考资源

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容