Angular10教程–6.0 表单 响应式表单 模板驱动 动态表单

Angular10教程--6.0 表单 响应式表单 模板驱动 动态表单

Angular 中提供了两种形式:表单。

理论准备

选择适合你的表单形式:
  • 提供对底层表单对象模型直接、显式的访问。它们与表单相比,更加健壮:它们的可扩展性、可复用性和可测试性都更高。适用于比较复杂的表单。
  • 模板驱动表单依赖模板中的指令来创建和操作底层的对象模型。它们很容易添加到应用中,但在扩展性方面不如响应式表单。适用于简单的表单。

无论哪种形式的表单,都会有下面4个常用基础类:

  • FormControl 实例用于追踪单个表单控件的值和验证状态;
  • FormGroup 用于追踪一个表单控件组的值和状态;
  • FormArray 用于追踪表单控件数组的值和状态;
  • ControlValueAccessor 用于在 Angular 的 FormControl 实例和原生 DOM 元素之间创建一个桥梁。

响应式表单

按照实际开发的操作程序,我们会单独创建一个模块来演示:
ng g m forms-study/forms-study --flat
将模块引入到你想要放的模块中。再创建一个今天练习的组件并调用:
ng g c forms-study/reactive-forms -s -c OnPush
一切准备就绪后,我们来创建一个最简单的响应式表单:
  1.  注册响应式表单模块,该模块声明了一些用在响应式表单中的指令:
// forms-study.module.ts
import { ReactiveFormsModule } from '@/forms';

@NgModule({
  imports: [
    // ...
    ReactiveFormsModule
  ],
})

2. 生成一个 FormControl 实例,并把它保存在组件中:

// reactive-forms.component.ts
export class ReactiveFormsComponent implements OnInit {
  name = new FormControl(''); // 默认值为''
}

3. 在模板中使用 FormControl:

<!-- reactive-forms.component.html -->
<label>
  Name: <input type="text" [formControl]="name">
</label>

这样,我们就创建了一个最简单的响应式表单。

使用 value 获取表单的值:

<p>value: {{name.value}}</p>

Angular10教程--6.0 表单 响应式表单 模板驱动 动态表单

表单分组

FormGroup 的实例跟踪一组 FormControl 实例(比如一个表单)的表单状态。当创建 FormGroup 时,其中的每个控件都会根据其名字进行跟踪。

使用步骤跟上面类似:

1. 创建一个 FormGroup 实例:

// reactive-forms.component.ts
export class ReactiveFormsComponent implements OnInit {
  profileForm = new FormGroup({
    firstName: new FormControl(''),
    lastName: new FormControl(''),
  });
}

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

<!-- reactive-forms.component.html -->
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
  <div class="form-group">
    <label>
      First Name: <input type="text" formControlName="firstName" class="form-control form-control-sm">
    </label>
  </div>
  <div class="form-group">
    <label>
      Last Name: <input type="text" formControlName="lastName" class="form-control form-control-sm">
    </label>
  </div>
  <button class="btn btn-primary btn-sm"  type="submit">提交</button>
</form>

3.保存表单数据:

// reactive-forms.component.ts
onSubmit(): void {
  console.log(this.profileForm.value); // {firstName: "abc", lastName: "def"}
}

嵌套表单组

表单组可以同时接受单个表单控件实例和其它表单组实例作为其子控件。这可以让复杂的表单模型更容易维护,并在逻辑上把它们分组到一起。

比如我们增加一个 address 信息:
// reactive-forms.component.ts
export class ReactiveFormsComponent implements OnInit {
  profileForm = new FormGroup({
    firstName: new FormControl(''),
    lastName: new FormControl(''),
    address: new FormGroup({ // 新增,并设置默认值
      street: new FormControl('建设路32号'),
      city: new FormControl('成都市')
    })
  });
}
当我们使用嵌套的表单组时,我们模板对应的 html 代码也应该具有相同的结构,并且设置 FormGroupName 值为表单组名:
<!-- reactive-forms.component.html -->
<!-- 其他代码 -->
<div formGroupName="address">
  <div class="form-group">
    <label>
      street: <input type="text" formControlName="street" class="form-control form-control-sm">
    </label>
  </div>
  <div class="form-group">
    <label>
      city: <input type="text" formControlName="city" class="form-control form-control-sm">
    </label>
  </div>
</div>
tips:如果不提供 FormGroupName 参数,在下面的 formControlName 也不能使用 address.street 。获取到的数据也是嵌套形式:Angular10教程--6.0 表单 响应式表单 模板驱动 动态表单

更新表单数据

更新表单数据有两种方式:

  • 使用 setValue() 方法来为单个控件设置新值。 setValue() 方法会严格遵循表单组的结构,并整体性替换控件的值。
  • 使用 patchValue() 方法可以用对象中所定义的任何属性为表单模型进行替换。
我们添加两个方法来演示 setValue 及 patchValue 两个方法来修改 lastName :
// reactive-forms.component.ts
onSetValue(): void {
  this.profileForm.setValue({
    address: {
      city: '北京市'
    }
  });
}
onPatchValue(): void {
  this.profileForm.patchValue({
    address: {
      city: '北京市'
    }
  });
}
patchValue()方法修改成功,而 setValue() 方法则报错了:

Angular10教程--6.0 表单 响应式表单 模板驱动 动态表单

因为 setValue() 方法严格遵循表单组的结构,所以需要将所有的表单控件的值都加上才能修改成功。

得出结论:修改表单部分值的时候使用 patchValue() 方法,否则使用 setValue() 方法。

使用 FormBuilder 服务生成控件

当表单中有很多控件时,为每个控件实例化 FormControl 显得很麻烦。FormBuilder 服务提供了一些便捷方法来生成表单控件。

class FormBuilder {
  group(controlsConfig: { [key: string]: any; }, options: AbstractControlOptions | { [key: string]: any; } = null): FormGroup
  control(formState: any, validatorOrOpts?: ValidatorFn | AbstractControlOptions | ValidatorFn[], asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[]): FormControl
  array(controlsConfig: any[], validatorOrOpts?: ValidatorFn | AbstractControlOptions | ValidatorFn[], asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[]): FormArray
}

FormBuilder 有三个方法:

  • group() 构建一个新的 FormGroup 实例。
  • control() 构建一个新的 FormControl 实例。
  • array() 构造一个新的 FormArray 实例。
使用 FormBuilder 的 group() 方法修改上面的表单创建形式:
// reactive-forms.component.ts
// 注入服务
constructor(private fb: FormBuilder) { }
// 构建表单
profileForm = this.fb.group({
  firstName: [''],
  lastName: [''],
  address: this.fb.group({
    street: ['建设路32号'],
    city: ['成都市']
  })
});
这样创建是不是清爽很多呢?效果完全一样。

创建

有的时候,我们不知道要创建的表单具体有多少个。比如给一个商品增加 size 属性,我们根本不知道它有几个尺寸,所以就需要动态创建表单。

使用 FormBuilder.array() 方法来定义 FormArray 实例数组:
// reactive-forms.component.ts
profileForm = this.fb.group({
  ...
  sizes: this.fb.array([
    this.fb.control('')
  ])
});
使用 getter 语法创建类属性 sizes,以便我们在模板中使用:
// reactive-forms.component.ts
import {FormArray} from '@/forms';
get sizes() {
  return this.profileForm.get('sizes') as FormArray; // 断言为FormArray类型
}
自定义 addSize() 方法,用 FormArray.push() 方法为 sizes 控件添加新属性:
// reactive-forms.component.ts
import {FormArray} from '@angular/forms';
addSize(): void {
  this.sizes.push(this.fb.control(''));
}
模板中渲染 sizes :
<!-- reactive-forms.component.html -->
<!-- 其他代码 -->
<div formArrayName="sizes">
  <div class="form-group" *ngFor="let size of sizes.controls; let i = index">
    <label>
      Size:
      <input type="text" [formControlName]="i" class="form-control form-control-sm">
    </label>
  </div>
</div>
tips:

  • 跟 FormGroup 一样,也需要先提供一个 formArrayName 属性;
  • 通过 sizes.controls 获取添加的动态控件。

Angular10教程--6.0 表单 响应式表单 模板驱动 动态表单

获取数据是一个数组的形式:

Angular10教程--6.0 表单 响应式表单 模板驱动 动态表单

模板驱动表单

我们也将创建一个 template-forms 组件来演示:
ng g c forms-study/template-forms -s -c OnPush
所谓模板驱动表单,通俗的讲就是利用 ngModel 等指令实现的表单。要使用 ngModel 指令,必须要做的第一件事就是引入 FormsModule :
// forms-study.module.ts
@NgModule({
  imports: [
    FormsModule,
    ...
  ]
})

 

接下来,我们将通过模板驱动表单的形式创建前面的表单。

  1. 构建数据
        为了使我们的数据有一个严谨的结构,我们使用一个类来定义数据格式:
// template-forms.component.ts
export class Person {
  constructor(
    public firstName: string,
    public lastName: string,
    public street: string,
    public city: string,
) {
  }
}
实例化数据并设置默认值:
// template-forms.component.ts
export class TemplateFormsComponent implements OnInit {
  model = new Person('', '', '建设路32号', '成都市');
}
    2. 在模板中绑定数据:
<!-- template-forms.component.html -->
<form>
  <div class="form-group">
    <label>
      First Name: <input type="text" [(ngModel)]="model.firstName" name="firstName" class="form-control form-control-sm">
    </label>
  </div>
  <div class="form-group">
    <label>
      Last Name: <input type="text" [(ngModel)]="model.lastName" name="lastName" class="form-control form-control-sm">
    </label>
  </div>
  <div class="form-group">
    <label>
      street: <input type="text" [(ngModel)]="model.street" name="street" class="form-control form-control-sm">
    </label>
  </div>
  <div class="form-group">
    <label>
      city: <input type="text" [(ngModel)]="model.city" name="city" class="form-control form-control-sm">
    </label>
  </div>
  <div class="btn-group">
    <button class="btn btn-primary btn-sm" type="submit">提交</button>
  </div>
</form>
tips:如果要在 <form> 标签中使用 ngModel 指令,应该提供 name 属性(详见2.3 双向绑定)。这就相当于响应式模板中提供的 formControlName 属性。

Angular10教程--6.0 表单 响应式表单 模板驱动 动态表单

这样,我们实现了数据的双向绑定,也仅仅是双向绑定。如果想用表单的方法,就跟上面一样,还需要在 <form> 标签上提供一个类似 FormGroup 的属性:
<!-- template-forms.component.html -->
<form #profileForm="ngForm">
  ...
</form>
如果引入了 FormsModule , <form> 标签上就可以使用 ngForm 指令。 这里的 profileForm 就是 ngForm 类型, profileForm.form 就是 FormGroup 实例。
将表单自己作为参数传入:
<!-- template-forms.component.html -->
<form #profileForm="ngForm" (ngSubmit)="onSubmit(profileForm)">
  ...
</form>
通过 FormGroup 实例拿到表单数据:
// template-forms.component.ts
onSubmit(f: NgForm): void {
  console.log(f.form.value); // {firstName: "aa", lastName: "bb", street: "建设路32号", city: "成都市"}
}
使用 FormGroup 的 reset() 方法重置表单:
// template-forms.component.ts
resetForm(f: NgForm): void {
  // reset(value: any = {}, options: { onlySelf?: boolean; emitEvent?: boolean; } = {}): void
  
  f.reset({firstName: 'Jack', lastName: 'Bob'});
}
Angular10教程--6.0 表单 响应式表单 模板驱动 动态表单

跟踪控件状态

Angular 会跟踪表单组件的状态,会告诉你用户是否接触过该控件、该值是否发生了变化,或者该值是否无效。并且会在控件上以特殊的 CSS 类来反映其状态。

Angular10教程--6.0 表单 响应式表单 模板驱动 动态表单

Angular10教程--6.0 表单 响应式表单 模板驱动 动态表单

我们可以用这些 CSS 类来根据控件的状态定义其样式。这也将为后面我们介绍表单验证做一点准备。

总结

  1. 响应式表单主要是通过操作表单对象来定义表单,用于较复杂结构的表单,模版驱动表单则相反;
  2. 修改表单部分值的时候使用 patchValue() 方法,否则使用 setValue() 方法;
  3. 使用 FormArray.push() 方法可以创建动态表单;
  4. 可以根据 angular 跟踪的表单状态来定制样式。