angular - 表单

(1)模板驱动表单

参考:

简单表单

​ngForm​ 固定的,需要提交的值 ngModel​

 <form #sub="ngForm" (submit)="onSubmit(sub)">
        <input type="text" name="username" ngModel>  
        <input type="id" name="userId" ngModel>
        <button>提交</button>
      </form>
  onSubmit(form:NgForm){
    console.log(form.value);
    console.log(form.valid);

  }

分组

​ngModelGroup​ : 会创建 FormGroup 的实例并把它绑定到 DOM 元素中。

<form #sub="ngForm" (submit)="onSubmit(sub)">
        <ng-container ngModelGroup="user">
          <input type="text" name="username" ngModel>  
        </ng-container>
        <ng-container ngModelGroup="Guid">
        <input type="id" name="Id" ngModel>
      </ng-container>
        <button>提交</button>
      </form>

(2)模板驱动表单验证

Angular 内置表单验证器:

  1. required:检查输入字段是否为空。
  2. minlength:检查输入字段的长度是否符合最小长度要求。
  3. maxlength:检查输入字段的长度是否符合最大长度要求。
  4. min:检查输入字段的值是否符合最小值要求。
  5. max:检查输入字段的值是否符合最大值要求。
  6. pattern:使用正则表达式来验证输入字段的值是否符合特定的模式。
  7. email:检查输入字段的值是否符合电子邮件格式。
  8. url:检查输入字段的值是否符合 URL 格式。
  9. date:检查输入字段的值是否符合日期格式。
  10. checkbox:检查是否选择了复选框。
  11. number:检查输入字段的值是否为数字。
  12. ​requiredTrue()​: 这个验证器是 Angular 中的一个内置验证器,它用于检查输入字段是否被选中和/或填写。如果输入字段的值是 true​ 或者用户已经填写了该字段,那么这个验证就会通过。如果输入字段为空或者未被选中,那么这个验证就会失败。
  13. ​compose()​: 这个函数是用来组合多个验证器的。你可以将多个验证器传递给 compose()​ 函数,然后它将返回一个新的验证器,这个新的验证器将按照你指定的顺序执行这些验证器。你可以使用 pipe()​ 函数来将多个验证器串联起来,但是 compose()​ 更加灵活,因为它允许你在不同的上下文中重复使用相同的验证器。s

Angular 中文文档-内置验证器,可用于各种表单控件

显示未通过验证的信息

要启用验证功能:必须有一个模板引用变量 #username="ngModel"​,这里的 ngModel​指令提供的功能,用于验证状态。

验证对象属性

名称 描述
path 这个属性返回元素的名称
valid 通过验证规则定义的条件 (样式 ng-valid)
invalid 不能通过验证规则定义的条件 (样式 ng-invalid)
pristine 内容没修改,未被用户编辑(样式 ng-pristine)
dirty 内容被修改,被用户编辑 (样式 ng-drity)
touched 元素被用户访问 (样式 ng-touched)(一般通过制表符 TAB​选择表单域)
untouched 元素未被用户访问, (样式 ng-untouched)(一般通过制表符 TAB​选择表单域)
errors 返回一个 ValidationErrors 键值对对象
​export declare type ValidationErrors = {

​ [key: string]: any;

​};
value 返回 value 值,用于 自定义表单验证规则​
      <form #sub="ngForm" (submit)="onSubmit(sub)">
        <input type="text" name="username" required pattern="\d"  #username="ngModel" as ngModel>
        <div *ngIf="username.touched && username.invalid && username.errors">
          <div *ngIf="username.errors['required']">必须填写</div>
          <div *ngIf="username.errors['pattern']">必须为数字</div>
        </div>

        <input type="id" name="userId" ngModel>
        <button  [disabled]="sub.invalid">提交</button>
      </form>

这里 angular 15 使用的 errors["XXXX"]​ 的方式(ValidationErrors​),angular 12 使用的 errors.requered 的方式。

表单验证错误描述属性

名称 描述
minlength.requiredLength 所需的字符数
minlength.actualLength 输入的字符数
pattern.requiredPattern 返回指定正则
pattern.actualValue 元素的内容

使用组件显示验证消息,验证整个表单

formSubmitted 指定表单是否已经提交,并在提交前阻止验证。

component.ts

import { ApplicationRef, Component } from '@angular/core';
import { Model } from './repository.model';
import { Product } from './product.model';
import { NgForm } from '@angular/forms';

@Component({
  selector: 'app',
  templateUrl: 'template.html',
})
export class ProductComponent {
  model: Model = new Model();
  //是否已经提交
  formSubmitted: boolean = false;

  submitForm(form: NgForm) {
    this.formSubmitted = true;
    if (form.valid) {
      this.addProject(this.newProduct);
      this.newProduct = new Product();
      form.reset();
      this.formSubmitted = false;
    }
  }

  getValidationMessages(state: any, thingName?: string) {
    let thing: string = state.path || thingName;
    let messages: string[] = [];
    if (state.errors) {
      for (const errorName in state.errors) {
        switch (errorName) {
          case 'required':
            messages.push(`you must enter a ${thing}`);
            break;
          case 'minlength':
            messages.push(
              `a ${thing} must be at least ${state.errors['minlength'].requiredLength}`
            );
            break;
          case 'pattern':
            messages.push(`The ${thing} contains illegal chracters`);
            break;
        }
      }
    }

    return messages;
  } 

//......other method
}

html

form定义一个引用变量 form​,ngForm​赋值给它:#form="ngForm"​,ngSubmit 绑定表达式调用控制器 submitForm​

​name="name"​这里的字符串name就是 #username​的 path​

<!-- 14.4.3 验证整个表单 -->
<style>
  input.ng-dirty.ng-invalid {
    border: 2px solid #ff0000
  }

  input.ng-dirty.ng-valid {
    border: 2px solid #6bc502
  }
</style>

<div class="m-2">
  <div class="bg-info text-white mb-2 p-2">Model Data:{{jsonProduct}}</div>

  <form #form="ngForm" (ngSubmit)="submitForm(form)">
    <div class="bg-danger text-white p-2 mb-2" *ngIf="formSubmitted && form.invalid">
      There are problems with the form
    </div>
    <div class="form-group">
      <label>Name</label>
     <input class="form-control" name="name" [(ngModel)]="newProduct.name" #username="ngModel" required minlength="5"
        pattern="^[A-Za-z ]+$" />
      <ul class="text-danger list-unstlyed"
        *ngIf="(formSubmitted || username.dirty) && username.invalid && username.errors">
        <li *ngFor="let error of getValidationMessages(username)">
          <span>{{error}}</span>
        </li>
      </ul>
    </div>

    <button class="btn btn-primary" type="submit">Create</button>
  </form>

</div>

1 显示摘要信息

component.ts

通过 NgForm​ 对象的 controls​ 属性访问各个元素

  getFormValidationMessages(form: NgForm): string[] {
    let messages: string[] = [];
    Object.keys(form.controls).forEach((k) => {
      this.getValidationMessages(form.controls[k], k).forEach((m) =>
        messages.push(m)
      );
    });
    return messages;
  }

html

<!-- 14.4.3-1 显示验证摘要信息 -->
<style>
  input.ng-dirty.ng-invalid {
    border: 2px solid #ff0000
  }

  input.ng-dirty.ng-valid {
    border: 2px solid #6bc502
  }
</style>

<div class="m-2">
  <div class="bg-info text-white mb-2 p-2">Model Data:{{jsonProduct}}</div>

  <form #form="ngForm" (ngSubmit)="submitForm(form)">
    <div class="bg-danger text-white p-2 mb-2" *ngIf="formSubmitted && form.invalid">
      There are problems with the form
      <ul>
        <li *ngFor="let error of getFormValidationMessages(form)">
          {{error}}
        </li>
      </ul>
    </div>

    <div class="form-group">
      <label>Name</label>
      <input class="form-control" name="name" [(ngModel)]="newProduct.name" #username="ngModel" required minlength="5"
        pattern="^[A-Za-z ]+$" />
      <ul class="text-danger list-unstlyed"
        *ngIf="(formSubmitted || username.dirty) && username.invalid && username.errors">
        <li *ngFor="let error of getValidationMessages(username)">
          <span>{{error}}</span>
        </li>
      </ul>
    </div>

    <button class="btn btn-primary" type="submit">Create</button>
  </form>

</div>

2 禁用提交按钮

<button class="btn btn-primary" type="submit" [disabled]="formSubmitted && form.invalid"
      [class.btn-secondary]="formSubmitted && form.invalid">
      Create
    </button>

(3)模型驱动表单

3.1) 响应式表单

3.1.1) 创建基础表单

1 在你的应用中注册响应式表单模块。该模块声明了一些你要用在响应式表单中的指令。

要使用响应式表单控件,就要从 @angular/forms 包中导入 ReactiveFormsModule,并把它添加到你的 NgModule 的 imports 数组中。

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

@NgModule({
  imports: [
    // other imports ...
    ReactiveFormsModule
  ],
})
export class AppModule { }

2 生成一个新的 FormControl 实例,并把它保存在组件中。

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

@Component({
  selector: 'app-name-editor',
  templateUrl: './name-editor.component.html',
  styleUrls: ['./name-editor.component.css']
})
export class NameEditorComponent {
  name = new FormControl('');
}

3 在模板中注册这个 FormControl。

<label for="name">Name: </label>
<input id="name" type="text" [formControl]="name">

使用这种模板绑定语法,把该表单控件注册给了模板中名为 name 的输入元素。这样,表单控件和 DOM 元素就可以互相通讯了:视图会反映模型的变化,模型也会反映视图中的变化。

a) 显示表单值

  • 通过可观察对象 valueChanges,你可以在模板中使用 AsyncPipe或在组件类中使用 subscribe() 方法来监听表单值的变化。

  • 使用 value 属性。它能让你获得当前值的一份快照。

<p>Value: {{ name1.value }}</p>

<label for="name">Name: </label>
<input id="name" type="text" [formControl]="name1">

一旦你修改了表单控件所关联的元素,这里显示的值也跟着变化了。

b) 替换表单控件的值

html

<p>Value: {{ name1.value }}</p>

<label for="name">Name: </label>
<input id="name" type="text" [formControl]="name1">
<button type="button" (click)="updateName()">Update Name</button>

FormControl 实例提供了一个 setValue() 方法,它会修改这个表单控件的值

  updateName() {
    this.name1.setValue('Nancy');
  }

3.1.2) 把表单分组 FormGroup 和 FromArray

FormGroup

定义了一个带有一组控件的表单,你可以把它们放在一起管理。表单组的基础知识将在本节中讨论。你也可以通过嵌套表单组来创建更复杂的表单。

FormArray

定义了一个动态表单,你可以在运行时添加和删除控件。你也可以通过嵌套表单数组来创建更复杂的表单。欲知详情,参阅下面的创建动态表单

a) FormGroup 创建确定子控件数量的动态表单

1.创建一个 FormGroup 实例。

要初始化这个 FormGroup,请为构造函数提供一个由控件组成的对象,对象中的每个名字都要和表单控件的名字一一对应

import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-ProfileEditor',
  templateUrl: './ProfileEditor.component.html',
  styleUrls: ['./ProfileEditor.component.css']
})
export class ProfileEditorComponent implements OnInit {
  profileForm = new FormGroup({
    firstName: new FormControl(''),
    lastName: new FormControl(''),
  });
  constructor() { }

  ngOnInit() {
  }
}

2 把这个 FormGroup 模型关联到视图

由 FormControlName 指令提供的 formControlName 属性把每个输入框和 FormGroup 中定义的表单控件绑定起来。这些表单控件会和相应的元素通讯,它们还把更改传给 FormGroup,这个 FormGroup 是模型值的事实之源。

<form [formGroup]="profileForm">

  <label for="first-name">First Name: </label>
  <input id="first-name" type="text" formControlName="firstName">

  <label for="last-name">Last Name: </label>
  <input id="last-name" type="text" formControlName="lastName">

</form>

3 保存表单数据。

ProfileEditor 组件从用户那里获得输入,但在真实的场景中,你可能想要先捕获表单的值,等将来在组件外部进行处理。FormGroup 指令会监听 form 元素发出的 submit 事件,并发出一个 ngSubmit 事件,让你可以绑定一个回调函数。把 onSubmit() 回调方法添加为 form 标签上的 ngSubmit 事件监听器。

<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">

ProfileEditor 组件上的 onSubmit() 方法会捕获 profileForm 的当前值。要保持该表单的封装性,就要使用 EventEmitter 向组件外部提供该表单的值。下面的例子会使用 console.warn 把这个值记录到浏览器的控制台中。

onSubmit() {
  // TODO: Use EventEmitter with form value
  console.warn(this.profileForm.value);
}

form 标签所发出的 submit 事件是内置 DOM 事件,通过点击类型为 submit 的按钮可以触发本事件。这还让用户可以用回车键来提交填完的表单。往表单的底部添加一个 button,用于触发表单提交。

  <p>Complete the form to enable button.</p>
  <button type="submit" [disabled]="!profileForm.valid">Submit</button>

b) 嵌套表单组

1 创建一个嵌套的表单组

在这个例子中,address group 把现有的 firstNamelastName 控件和新的 streetcitystate 和 zip 控件组合在一起。虽然 address 这个 FormGroup 是 profileForm 这个整体 FormGroup 的一个子控件,但是仍然适用同样的值和状态的变更规则。来自内嵌控件组的状态和值的变更将会冒泡到它的父控件组,以维护整体模型的一致性

import { Component } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';

@Component({
  selector: 'app-profile-editor',
  templateUrl: './profile-editor.component.html',
  styleUrls: ['./profile-editor.component.css']
})
export class ProfileEditorComponent {
  profileForm = new FormGroup({
    firstName: new FormControl(''),
    lastName: new FormControl(''),
    address: new FormGroup({
      street: new FormControl(''),
      city: new FormControl(''),
      state: new FormControl(''),
      zip: new FormControl('')
    })
  });
}

2 在模板中对这个嵌套表单分组

内嵌的使用formGroupName

<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">

  <label for="first-name">First Name: </label>
  <input id="first-name" type="text" formControlName="firstName">

  <label for="last-name">Last Name: </label>
  <input id="last-name" type="text" formControlName="lastName">

  <div formGroupName="address">
    <h2>Address</h2>

    <label for="street">Street: </label>
    <input id="street" type="text" formControlName="street">

    <label for="city">City: </label>
    <input id="city" type="text" formControlName="city">

    <label for="state">State: </label>
    <input id="state" type="text" formControlName="state">

    <label for="zip">Zip Code: </label>
    <input id="zip" type="text" formControlName="zip">
  </div>

  <p>Complete the form to enable button.</p>
  <button type="submit" [disabled]="!profileForm.valid">Submit</button>
</form>

c) 更新部分数据模型

当修改包含多个 FormGroup 实例的值时,你可能只希望更新模型中的一部分,而不是完全替换掉

方法 详情
setValue() 使用 setValue() 方法来为单个控件设置新值。setValue() 方法会严格遵循表单组的结构,并整体性替换控件的值。
patchValue() 用此对象中定义的任意属性对表单模型进行替换。

setValue() 方法的严格检查可以帮助你捕获复杂表单嵌套中的错误,而 patchValue() 在遇到那些错误时可能会默默的失败。

在 ProfileEditorComponent 中,使用 updateProfile 方法传入下列数据可以更新用户的名字与街道住址。

  updateProfile() {
    const newFirstName = 'Alice'; // 新的first name值
    // setValue 更新单个
    this.profileForm.get('firstName')?.setValue(newFirstName);

    // patchValue 更新多个一部分
    // this.profileForm.patchValue({
    //   firstName: 'Nancy',
    //   address: {
    //     street: '123 Drew Street'
    //   }
    // });
  }

加个按钮来更新

<button type="button" (click)="updateProfile()">Update Profile</button>

3.1.3) FormBuilder 服务生成控件

a 导入 FormBuilder 类。

b 注入这个 FormBuilder 服务。

c 生成表单内容。

FormBuilder 服务有三个方法:control()group() 和 array()。这些方法都是工厂方法,用于在组件类中分别生成 FormControlFormGroup 和 FormArray

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-ProfileEditor',
  templateUrl: './ProfileEditor.component.html',
  styleUrls: ['./ProfileEditor.component.css']
})
export class ProfileEditorComponent implements OnInit {
  profileForm = this.fb.group({
    firstName: [''],
    lastName: [''],
    address: this.fb.group({
      street: [''],
      city: [''],
      state: [''],
      zip: ['']
    }),
  });

  constructor(private fb: FormBuilder) { }

  ngOnInit() {
  }
}
// ts{10-19,21}

在上面的例子中,你可以使用 group() 方法,用和前面一样的名字来定义这些属性。这里,每个控件名对应的值都是一个数组,这个数组中的第一项是其初始值。

3.1.4) 响应式表单的验证

3.1.4.1) 在表单组件中导入一个验证器函数。

从 @angular/forms 包中导入 Validators 类。

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

3.1.4.2) 把这个验证器添加到表单中的相应字段。

profileForm = this.fb.group({
  firstName: ['', Validators.required],
  lastName: [''],
  address: this.fb.group({
    street: [''],
    city: [''],
    state: [''],
    zip: ['']
  }),
});

3.1.4.3) 添加逻辑来处理验证状态。

<p>Form Status: {{ profileForm.status }}</p>

参考:

3.1.5) FormArray 创建不确定数量子控件的动态表单

FormArray 如果你事先不知道子控件的数量,这就是一个很好的选择。

FormArray 是 FormGroup 之外的另一个选择,用于管理任意数量的匿名控件。像 FormGroup 实例一样,你也可以往 FormArray 中动态插入和移除控件,并且 FormArray 实例的值和验证状态也是根据它的子控件计算得来的。不过,你不需要为每个控件定义一个名字作为 key.

a) 导入 FormArray 类。

b) 定义一个 FormArray 控件。

使用 FormBuilder.array() 方法来定义该数组,并用 FormBuilder.control() 方法来往该数组中添加一个初始控件。

FormGroup 中的这个 aliases 控件现在管理着一个控件,将来还可以动态添加多个。

profileForm = this.fb.group({
  firstName: ['', Validators.required],
  lastName: [''],
  address: this.fb.group({
    street: [''],
    city: [''],
    state: [''],
    zip: ['']
  }),
  aliases: this.fb.array([
    this.fb.control('')
  ])
});

c) 使用 getter 方法访问 FormArray 控件。

相对于重复使用 profileForm.get() 方法获取每个实例的方式,getter 可以让你轻松访问表单数组各个实例中的别名。表单数组实例用一个数组来代表未定数量的控件。通过 getter 来访问控件很方便,这种方法还能很容易地重复处理更多控件。
使用 getter 语法创建类属性 aliases,以从父表单组中接收表示绰号的表单数组控件。

get aliases() {
  return this.profileForm.get('aliases') as FormArray;
}

注意
因为返回的控件的类型是 AbstractControl,所以你要为该方法提供一个显式的类型声明来访问 FormArray 特有的语法。

定义一个方法来把一个绰号控件动态插入到绰号 FormArray 中。用 FormArray.push() 方法把该控件添加为数组中的新条目。

addAlias() {
  this.aliases.push(this.fb.control(''));
}

在这个模板中,这些控件会被迭代,把每个控件都显示为一个独立的输入框。

d) 在模板中显示这个表单数组。

要想为表单模型添加 aliases,你必须把它加入到模板中供用户输入。和 FormGroupNameDirective 提供的 formGroupName 一样,FormArrayNameDirective 也使用 formArrayName 在这个 FormArray 实例和模板之间建立绑定。
在 formGroupName <div> 元素的结束标签下方,添加一段模板 HTML。

  <div formArrayName="aliases">
    <h2>Aliases</h2>
    <button type="button" (click)="addAlias()">+ Add another alias</button>

    <div *ngFor="let alias of aliases.controls; let i=index">
      <!-- The repeated alias template -->
      <label for="alias-{{ i }}">Alias:</label>
      <input id="alias-{{ i }}" type="text" [formControlName]="i">
    </div>
  </div>

3.2) 严格类型化表单

从 Angular 14 开始,响应式表单默认是严格类型的。

3.2.1) FormControl 入门

const email = new FormControl('angularrox@gmail.com');

此控件将被自动推断为 FormControl<string|null> 类型。TypeScript 会在整个FormControl API中自动强制执行此类型,例如 email.value 、 email.valueChanges 、 email.setValue(...) 等。

a) 可空性 nonNullable

你可能想知道:为什么此控件的类型包含 null ?这是因为控件可以随时通过调用 reset 变为 null

const email = new FormControl('angularrox@gmail.com');
email.reset();
console.log(email.value); // null

TypeScript 将强制你始终处理控件已变为 null 的可能性。如果要使此控件不可为空,可以用 nonNullable 选项。这将导致控件重置为其初始值,而不是 null

const email = new FormControl('angularrox@gmail.com', {nonNullable: true});
email.reset();
console.log(email.value); // angularrox@gmail.com

3.2.2) FormArray:动态的、同质的集合

FormArray 包含一个开放式控件列表。type 参数对应于每个内部控件的类型:

const names = new FormArray([new FormControl('Alex')]);
names.push(new FormControl('Jess'));

此 FormArray 将具有内部控件类型 FormControl<string|null>

如果你想在数组中有多个不同的元素类型,则必须使用 UntypedFormArray,因为 TypeScript 无法推断哪种元素类型将出现在哪个位置。

3.2.3) FormGroup 和 FormRecord

Angular 为具有枚举键集的表单提供了 FormGroup 类型,并为开放式或动态组提供了一种名为 FormRecord 的类型。

a) 部分值

const login = new FormGroup({
    email: new FormControl('', {nonNullable: true}),
    password: new FormControl('', {nonNullable: true}),
});

在任何 FormGroup 上,都可以禁用控件。任何禁用的控件都不会出现在组的值中。

因此,login.value 的类型是 Partial<{email: string, password: string}>。这种类型的 Partial 意味着每个成员可能是未定义的。

更具体地说,login.value.email 的类型是 string|undefined,TypeScript 将强制你处理可能 undefined 的值(如果你启用了 strictNullChecks)。

如果你想访问包括禁用控件的值,从而绕过可能的 undefined 字段,可以用 login.getRawValue()

b) 可选控件和动态组

某些表单的控件可能存在也可能不存在,可以在运行时添加和删除。你可以用可选字段来表示这些控件:

interface LoginForm {
    email: FormControl<string>;
    password?: FormControl<string>;
}

const login = new FormGroup<LoginForm>({
    email: new FormControl('', {nonNullable: true}),
    password: new FormControl('', {nonNullable: true}),
});

login.removeControl('password');

在这个表单中,我们明确地指定了类型,这使我们可以将 password 控件设为可选的。TypeScript 会强制只有可选控件才能被添加或删除。

c) FormRecord

某些 FormGroup 的用法不符合上述模式,因为键是无法提前知道的。FormRecord 类就是为这种情况设计的:

const addresses = new FormRecord<FormControl<string|null>>({});
addresses.addControl('Andrew', new FormControl('2340 Folsom St'));

任何 string|null 类型的控件都可以添加到此 FormRecord

如果你需要一个动态(开放式)和异构(控件是不同类型)的 FormGroup,则无法提升为类型安全的,这时你应该使用 UntypedFormGroup

FormRecord 也可以用 FormBuilder 构建:

const addresses = fb.record({'Andrew': '2340 Folsom St'});

如果你需要一个动态(开放式)和异构(控件是不同类型)的 FormGroup,则无法提高类型安全,你应该使用 UntypedFormGroup

3.2.4) FormBuilder 和 NonNullableFormBuilder

FormBuilder 类已升级为支持新增的类型的版本,方式与上面的示例相同。

此外,还有一个额外的构建器:NonNullableFormBuilder。它是在所有控件都上指定 {nonNullable: true} 的简写,用来在大型非空表单中消除主要的样板代码。你可以用 FormBuilder 上的 nonNullable 属性访问它:

const fb = new FormBuilder();
const login = fb.nonNullable.group({
    email: '',
    password: '',
});

在上面的示例中,两个内部控件都将不可为空(即将设置 nonNullable)。

你还可以用名称 NonNullableFormBuilder 注入它。

3.3) 构建动态表单

3.4) 例子

ReactiveFormsModule 的使用

app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

// import { AppComponent } from './app.component';
import { ProductComponent } from './component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

@NgModule({
  declarations: [ProductComponent], //列出应用的类,告诉Angular哪些类构成了这个模块。
  imports: [BrowserModule, FormsModule, ReactiveFormsModule],
  providers: [], //提供服务
  bootstrap: [ProductComponent], //根组件
})
export class AppModule {}

基于模型的功能在 ReactiveFormsModule 模块定义中。

FormControl 和 FormGroup 的使用

FormControl: 表单中的表单项

FormGroup: 表单组,表单至少是一个 FormGroup

// form.module.ts
import { FormControl, FormGroup, Validators } from '@angular/forms';

export class ProductFormControl extends FormControl {   //FormControl表单中的单个元素
  lable: string;  
  modelProperty: string;

  /**
   * 构造函数用于初始化一个带有label、模型属性、值和验证器的 FormControl。
   * @param {string} lable - 表示控件的标签或名称的字符串。它用于在用户界面中显示控件的label。
   * @param {string} modelProperty - 表示绑定到此表单控件的模型对象的属性名称的字符串。它用于将表单控件的值映射到模型对象的相应属性上。
   * @param {any} value - 代表控件当前值的参数。对于 FormControl,它是当前值。对于已启用的 FormGroup,它是组内所有已启用控件的值,以键值对的形式呈现为对象。对于已禁用的 FormGroup,它是组内所有控件的值。
   * @param {any} validator - 用于确定控件有效性的函数。它可以是一个单独的验证器函数,也可以是使用 Validators.compose() 函数组合多个验证器函数的结果。验证器用于对控件的值执行验证检查,并在值无效时返回错误对象。
   */
  constructor(
    lable: string,
    modelProperty: string,
    value: any,
    validator: any
  ) {

/**
 * 例子:const fc = new FormControl('foo');
 *  
 * value: 控件的当前值。
    对于 FormControl,是当前值。
    对于已启用的 FormGroup,是组内所有已启用控件的值,以对象形式呈现,每个组成员都有一个键值对。
    对于已禁用的 FormGroup,是组内所有控件的值,以对象形式呈现,每个组成员都有一个键值对。
    对于 FormArray,是组内所有已启用控件的值,以数组形式呈现。

    validator: 返回用于同步确定此控件有效性的函数。如果已添加多个验证器,则这将是一个组合函数。有关更多信息,请参阅 Validators.compose()。
 */  
    super(value, validator);  //实现 FormControl 的构造函数
    this.lable = lable;
    this.modelProperty = modelProperty;
  }
}

export class ProductFormGroup extends FormGroup {   //FormGroup 管理form 元素
  constructor() {
    super({
      name: new ProductFormControl('Name', 'name', '', Validators.required),
      category: new ProductFormControl(
        'Category',
        'category',
        '',
        Validators.compose([
          Validators.required,
          Validators.pattern('^[A-Za-z ]+$'),
          Validators.minLength(3),
          Validators.maxLength(10),
        ])
      ),
      price: new ProductFormControl(
        'Price',
        'price',
        '',
        Validators.compose([
          Validators.required,
          Validators.pattern('^[0-9\.]+$'),
        ])
      ),
    });
  }
}

​FormGroup​​的构造函数接收一个对象,该对象各个属性与各个属性的名称与模板各个 input 元素的名称一一对应,每个属性都赋予一个用来表示该 input 元素的 ProductFormControl​​ 对象,该对象同时指定 input 的验证要求。

传给超类构造函数对象第一个属性最简单:

​name: new ProductFormControl('Name', 'name', '', Validators.required),​​

属性名为 name​​,告诉angular 属性对应模板中名为 name 的input元素。ProductFormControl 构造函数实参指定:

与 input 元素相关的 label 元素内容(Name),

input 元素绑定的 Product 类的某个属性名称(name),

数据绑定的初始值(空字符串)

所需的验证(Validators.required)

可以使用 Validators.compose​​ 将多个验证器组合起来。

FormArray: 复杂表单, 动态添加表单项和表单组,表单验证时,FormArray 有一项没通过,整体不通过。

(4)模型驱动的表单验证

错误消息生成从组件移动到表单模型类,让组件尽可能简单。ProductFormControl​​ 类的 getValidationMessages​​ 方法中尉 maxLength​​ 添加的验证消息。

// form.module.ts
import { FormControl, FormGroup, Validators } from '@angular/forms';

export class ProductFormControl extends FormControl {
  //FormControl表单中的单个元素
  lable: string;
  modelProperty: string;

  /**
   * 构造函数用于初始化一个带有lable、模型属性、值和验证器的 FormControl。
   * @param {string} lable - 表示控件的标签或名称的字符串。它用于在用户界面中显示控件的 lable。
   * @param {string} modelProperty - 表示绑定到此表单控件的模型对象的属性名称的字符串。它用于将表单控件的值映射到模型对象的相应属性上。
   * @param {any} value - 代表控件当前值的参数。对于 FormControl,它是当前值。对于已启用的 FormGroup,它是组内所有已启用控件的值,以键值对的形式呈现为对象。对于已禁用的 FormGroup,它是组内所有控件的值。
   * @param {any} validator - 用于确定控件有效性的函数。它可以是一个单独的验证器函数,也可以是使用 Validators.compose() 函数组合多个验证器函数的结果。验证器用于对控件的值执行验证检查,并在值无效时返回错误对象。
   */
  constructor(
    lable: string,
    modelProperty: string,
    value: any,
    validator: any
  ) {
    /**
 * 例子:const fc = new FormControl('foo');
 *  
 * value: 控件的当前值。
    对于 FormControl,是当前值。
    对于已启用的 FormGroup,是组内所有已启用控件的值,以对象形式呈现,每个组成员都有一个键值对。
    对于已禁用的 FormGroup,是组内所有控件的值,以对象形式呈现,每个组成员都有一个键值对。
    对于 FormArray,是组内所有已启用控件的值,以数组形式呈现。

    validator: 返回用于同步确定此控件有效性的函数。如果已添加多个验证器,则这将是一个组合函数。有关更多信息,请参阅 Validators.compose()。
 */
    super(value, validator); //实现 FormControl 的构造函数
    this.lable = lable;
    this.modelProperty = modelProperty;
  }

  //14.5.2 定义表单模型
  getValidationMessages() {
    let messages: string[] = [];
    if (this.errors) {
      for (const errorName in this.errors) {
        switch (errorName) {
          case 'required':
            messages.push(`you must enter a ${this.lable}`);
            break;
          case 'minlength':
            messages.push(
              `a ${this.lable} must be at least ${this.errors['minlength'].requiredLength}`
            );
            break;
          case 'maxlength':
            messages.push(
              `a ${this.lable} must be no more than ${this.errors['minlength'].requiredLength}`
            );
            break;
          case 'pattern':
            messages.push(`The ${this.lable} contains illegal chracters`);
            break;
        }
      }
    }
    return messages;
  }
}

export class ProductFormGroup extends FormGroup {
  //FormGroup 管理form 元素
  constructor() {
    super({
      name: new ProductFormControl('Name', 'username', '', Validators.required),
      category: new ProductFormControl(
        'Category',
        'category',
        '',
        Validators.compose([
          Validators.required,
          Validators.pattern('^[A-Za-z ]+$'),
          Validators.minLength(3),
          Validators.maxLength(10),
        ])
      ),
      price: new ProductFormControl(
        'Price',
        'price',
        '',
        Validators.compose([
          Validators.required,
          Validators.pattern('^[0-9.]+$'),
        ])
      ),
    });
  }

  get productControls(): ProductFormControl[] {
    return Object.keys(this.controls).map(
      (k) => this.controls[k] as ProductFormControl
    );
  }

  getValidationMessages(name: string): string[] {
    return (
      this.controls[name] as ProductFormControl
    ).getValidationMessages();
  }

  getFormValidationMessages(): string[] {
    let messages: string[] = [];
    Object.values(this.controls).forEach((c) =>
      messages.push(...(c as ProductFormControl).getValidationMessages())
    );
    return messages;
  }
}

删除了用于生成错误消息的方法,它们被移动到 form.model​​

导入 ProductFormGroup 类,并且定义一个 fromGroup 属性,这样就可以使用自定义表单模型类。

//14.5.3 使用模型验证
//component.ts
import { ApplicationRef, Component } from '@angular/core';
import { Model } from './repository.model';
import { Product } from './product.model';
import { NgForm } from '@angular/forms';
import { ProductFormGroup } from './form.module';

@Component({
  selector: 'app',
  templateUrl: 'template.html',
})
export class ProductComponent {
  model: Model = new Model();
  formGroup: ProductFormGroup = new ProductFormGroup();
  selectedProduct: string = '';

  //是否已经提交
  formSubmitted: boolean = false;

  submitForm() {
    Object.keys(this.formGroup.controls)
      .forEach(c=> this.newProduct[c] = this.formGroup.controls[c].value);//如果错误:No index signature with a parameter of type 'string' was found on type 'Product'. => Product 类定义 [key: string]: any;

    this.formSubmitted = true;
    if (this.formGroup.valid) {
      this.addProject(this.newProduct);
      this.newProduct = new Product();
      this.formGroup.reset();
      this.formSubmitted = false;
    }
  }

 newProduct: Product = new Product();

  get jsonProduct() {
    return JSON.stringify(this.newProduct);
  }

  addProject(p: Product) {
    console.log('new project: ' + this.newProduct);
  }

}

下面是HTML,

<!-- 14.5.3 使用模型验证 -->
<style>
  input.ng-dirty.ng-invalid {
    border: 2px solid #ff0000
  }

  input.ng-dirty.ng-valid {
    border: 2px solid #6bc502
  }
</style>

<div class="m-2">
  <div class="bg-info text-white mb-2 p-2">Model Data:{{jsonProduct}}</div>

  <form class="m-2" novalidate [formGroup]="formGroup" (ngSubmit)="submitForm()">
    <div class="bg-danger text-white p-2 mb-2" *ngIf="formSubmitted && formGroup.invalid">
      There are problems with the form
      <ul>
        <li *ngFor="let error of formGroup.getFormValidationMessages()">
          {{error}}
        </li>
      </ul>
    </div>

    <div class="form-group">
      <label>Name</label>
      <input class="form-control" name="name" formControlName="name" />
      <ul class="text-danger list-unstlyed"
        *ngIf="(formSubmitted || formGroup.controls['name'].dirty) && formGroup.controls['name'].invalid">
        <li *ngFor="let error of formGroup.getValidationMessages('name')">
          <span>{{error}}</span>
        </li>
      </ul>
    </div>

    <div class="form-group">
      <label>Category</label>
      <input class="form-control" name="price" formControlName="category" />
      <ul class="text-danger list-unstlyed"
        *ngIf="(formSubmitted || formGroup.controls['category'].dirty) && formGroup.controls['category'].invalid">
        <li *ngFor="let error of formGroup.getValidationMessages('category')">
          <span>{{error}}</span>
        </li>
      </ul>
    </div>

    <div class="form-group">
      <label>Price</label>
      <input class="form-control" name="price" formControlName="price" />
      <ul class="text-danger list-unstlyed"
        *ngIf="(formSubmitted || formGroup.controls['price'].dirty) && formGroup.controls['price'].invalid">
        <li *ngFor="let error of formGroup.getValidationMessages('price')">
          <span>{{error}}</span>
        </li>
      </ul>
    </div>

    <button class="btn btn-primary" type="submit" 
    [disabled]="formSubmitted && formGroup.invalid"
      [class.btn-secondary]="formSubmitted && formGroup.invalid">
      Create
    </button>
  </form>
</div>

第1个修改:form

<form class="m-2" novalidate [formGroup]="formGroup" (ngSubmit)="submitForm()">

​[formGroup]="formGroup"​​ 赋给 formGroup 指令的值是 form​​ 属性,这里是 formGroup,返回一个 ProductFormGroup​​ 对象。

第2个修改:input

​删除​​了单独的验证属性和被赋予特殊值 ngForm​​ 的模板变量。但是添加了 foprmControlName​​ 属性,模型表单这里使用 ProductFormGroup 使用名称来识别。

foprmControlName 让 angular 添加和移除 input元素的验证类。告诉 angular,用特定的验证器验证。

···
name: new ProductFormControl('Name', 'username', '', Validators.required),
···

第3个修改: ul

FormGroup 提供一个 controls 属性,返回自己管理的 FormControl 对象集合,按名称进行索引。

export class Product {
  constructor(
    public id?: number,
    public name?: string,
    public category?: string,
    public price?: number
  ) {}
  [key: string]: any;
}

posted @ 2024-05-19 22:11  【唐】三三  阅读(78)  评论(0编辑  收藏  举报