• Angular2表单模型驱动的表单(Model-Driven Forms)



    http://codin.im/2016/09/29/angular2-form-model-driven/


    在上一篇Angular2表单-模板驱动的表单文章中,介绍了模板驱动的表单,这里就不再赘述。虽然模板驱动的表单使用起来很方便,但是,当你的表单变得越来越复杂,特别是控件之间存在很多数据的交互,例如很常见的购物车,购物车里面会有很多商品,如果是类似淘宝这样的网站,这些商品还需要按照店铺分组;每个商品有单价和数量,每个店铺甚至每个商品可能有一些优惠券可以使用,甚至会有淘宝平台的减满券;当每个商品的单价或数量改变的时候,每个店铺的商品总金额、和总金额都会发生改变。像这种复杂的表单,数据之间的交互非常多,对开发和测试都会非常不方便。如果使用模板驱动的表单,测试是基于浏览器的端对端测试,测试用例也很不好写。对于这种情况,使用Angular2的另一种表单,也就是模型驱动的表单(Model-Driven Forms)会更加方便。

    但是,需要说明的是,使用模型驱动的表单,并不是说就不需要写页面,页面模板上的表单也不会自动生成。只是说,我们不需要像模板驱动的表单一样,在页面上设置model、验证器等。

    实例

    下图是这篇文章使用的实例的界面,跟上一篇介绍模板驱动的表单使用的实例一样:
    screen.png

    项目源码可以从github获取,这个项目包含了几个Angular2表单相关的实例,可以使用下面的命令获取本文所对应的代码:

    1
    git clone https://github.com/Mavlarn/angular2-forms-tutorial

    然后进入项目目录,运行下面的命令安装依赖然后运行测试服务器:

    1
    2
    3
    4
    cd angular2-forms-tutorial
    git checkout model-driven # 检出该文所使用的tag
    npm install
    npm start

    form

    我们还是用上一篇文章使用的实例,来创建一个简单的修改个人信息的表单,表单页面如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <form>
    <label>姓名:</label>
    <input type="text">
    <label>电话:</label>
    <input type="text">
    <fieldset>
    <label>城市:</label>
    <input type="text">
    <label>街道:</label>
    <input type="text">
    </fieldset>
    <button type="submit">保存</button>
    </form>

    同样,为了节省页面篇幅,省略了很多样式,在实例中可以看到使用的bootstrap的样式,例如姓名字段实际上是这样:

    1
    2
    3
    4
    5
    6
    <div class="form-group">
    <label class="col-sm-2 control-label">姓名:</label>
    <div class="col-sm-10">
    <input class="form-control" type="text">
    </div>
    </div>

    组件

    然后,我们的组件是这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import { Component } from '@angular/core';
    import { FormGroup, FormControl } from '@angular/forms';
    @Component({
    selector: 'reactive-form',
    templateUrl: 'app/reactive-forms/reactive-forms.component.html',
    styleUrls: ['app/reactive-forms/reactive-forms.component.css']
    })
    export class ReactiveFormsComponent {
    userForm = new FormGroup({
    name: new FormControl(),
    mobile: new FormControl(),
    address: new FormGroup({
    city: new FormControl(),
    street: new FormControl()
    })
    });
    }

    我们在组件里面,创建了一个FormGroup类型的对象,他就是对应的页面上的表单数据,其中,address也是一个group,里面又有2个属性。

    绑定组件和表单元素

    我们在组件中手动创建了这个表单控件组,里面包含所有的组件,对应页面上的表单元素。但是,我们需要把这个组件中的数据绑定到页面上。我们除了用Angular2的数据绑定的方式绑定这个数据到模板上以外,我们还需要针对表单控件做映射:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <form [formGroup]="userForm">
    <label>姓名:</label>
    <input type="text" formControlName="name">
    <label>电话:</label>
    <input type="text" formControlName="mobile">
    <fieldset formGroupName="address">
    <label>城市:</label>
    <input type="text" formControlName="city">
    <label>街道:</label>
    <input type="text" formControlName="street">
    </fieldset>
    <button type="submit">保存</button>
    </form>

    首先我们用[formGroup]="userForm"将页面上的表单的formGroup(单向)绑定到组件中的userForm变量上。
    然后,通过<input type="text" formControlName="name">,将里面的表单的输入组件绑定到userForm里面的name控件上。在上面的ReactiveFormsComponent组件中,我们创建了userForm控件组,里面有一个FormControl,叫name
    使用这种绑定方式,我们把页面上的表单元素和组件中代码创建的表单控件关联起来。

    简化表单控件创建

    在上面的ReactiveFormsComponent组件中我们创建表单的方式其实也可以使用FormBuilder简化。同时还可以在新建的FormControl的时候可以设置初始值已经验证器等,具体看下面的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import { Component, OnInit } from '@angular/core';
    import { FormGroup, FormControl, FormBuilder, Validators } from '@angular/forms';
    export class ReactiveFormsComponent implements OnInit {
    userForm: FormGroup;
    constructor(private formBuilder: FormBuilder) {}
    ngOnInit() {
    this.userForm = this.formBuilder.group({
    name: ['张三', [Validators.required, Validators.minLength(3)]],
    mobile: [13800138001, [Validators.required, Validators.minLength(11), Validators.maxLength(11)]],
    address: this.formBuilder.group({
    city: ['北京', Validators.required],
    street: ['朝阳望京...', Validators.required]
    })
    });
    }
    }

    在这个控件中,姓名控件设置了初始值’张三’和2个验证器(Validators.required, Validators.minLength(3)),手机号的控件也设置了初始值和3个验证器。

    页面上显示控件状态

    最后,跟’模板驱动的表单’类似,我们还可以在页面上添加验证器和表单控件的状态。只不过,在’模板驱动的表单’中,我们可以使用模板引用变量#name="ngModel"来创建一个针对姓名控件的引用变量。但是在这里,我们没有一个ngModel在模板里,所以就不能用这种方式,我们只能使用表单控件的引用来获得所有控件的状态,例如userForm.controls.name.dirty这样。下面就是针对姓名控件,添加的根据状态显示各种信息的实例:

    1
    2
    3
    4
    5
    6
    7
    8
    <input type="text" formControlName="name">
    <span *ngIf="userForm.controls.name.pristine">未修改</span>
    <span *ngIf="userForm.controls.name.dirty">已修改</span>
    <span *ngIf="userForm.controls.name.valid">有效</span>
    <div [hidden]="userForm.controls.name.valid||userForm.controls.name.pristine">
    <p *ngIf="userForm.controls.name.errors?.minlength">姓名最小长度为3</p>
    <p *ngIf="userForm.controls.name.errors?.required">必须输入姓名</p>
    </div>

    当然,跟模板驱动的表单一样,这种表单也会根据状态在html元素上添加各种class。当一个控件的值通过验证器验证有效以后,在这个html元素上就会添加一个ng-valid的class;如果验证失败,就会添加一个ng-invalid。我们就可以使用css样式来显示不同的状态:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    .ng-valid[required], .ng-valid.required {
    border-left: 5px solid #42A948; /* green */
    }
    .ng-invalid:not(form).ng-invalid:not(fieldset) {
    border-left: 5px solid #a94442; /* red */
    }
    .error-msg {
    color: red;
    }

    在上面的定义中,我们对验证类型为required、状态为valid的控件添加了一个左边绿色显示的样式。但是,实际上这一个样式不会起作用,因为我们的验证器不是在页面模版中添加的,例如在’姓名’是这样:

    1
    <input type="text" formControlName="name">

    我们没有添加一个required的属性给他,所以ng-valid[required]这个选择器不会起作用。

    响应式处理表单数据

    模型驱动表单,之所以又叫响应式表单(Reactive Forms),最重要的原因就是我们可以使用响应式编程来处理表单中的数据。响应式表单的控件提供一个Observables类型的数据更新对象,我们可以使用Observables的各种特性来处理表单中数据的改变。
    有关Observables的特性和如何利用它的特性处理表单数据,可以阅读这篇文章《利用Angular2的Observables实现交互控制》
    在这里,我们直接使用它来处理数据更新:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    export class ReactiveFormsComponent implements OnInit {
    userForm: FormGroup;
    msg: String;
    changeMsg: any;
    constructor(private formBuilder: FormBuilder) {}
    ngOnInit() {
    this.userForm = this.formBuilder.group({
    name: ['张三', [Validators.required, Validators.minLength(3)]],
    mobile: [13800138001, [Validators.required, Validators.minLength(11), Validators.maxLength(11)]],
    address: this.formBuilder.group({
    city: ['北京', Validators.required],
    street: ['朝阳望京...', Validators.required]
    })
    });
    // 从表单控件中获得地址、城市、街道控件的引用
    // 地址控件也是一个FormGroup,需要将它转型成FormGroup类型。
    const addr$ = <FormGroup>this.userForm.controls['address'];
    const city$ = addr$.controls['city'];
    const street$ = addr$.controls['street'];
    city$.valueChanges.debounceTime(1000).distinctUntilChanged().subscribe(cityValue => {
    this.msg = cityValue + ' 欢迎你!';
    street$.setValue(cityValue);
    });
    this.userForm.valueChanges.subscribe(x => this.changeMsg = { event: 'Form DATA CHANGED', object: x });
    }
    reset() {
    // 我们同样可以使用reset方法来重置数据
    this.userForm.reset();
    }
    }

    上面的debounceTime(1000)是指当用户修改输入的值以后,过1000毫秒才会触发后面的处理方法。
    distinctUntilChanged()是指如果用户如果在1000毫秒内,输入了一个字符,又删掉了,那么输入的值应该没有改变,这种情况下,后面的处理方法就不会被触发。
    subscribe()就是注册一个处理方法,就是当有数据发生改变的时候,需要出发的方法。在这个方法里,当用户输入了一个新的城市名以后,就会更新街道的值也为这个城市名,同时下面的显示信息也相应改变。

    实际上,对于模板驱动的表单的数据更新,我们也可以使用这种响应式的数理方法。只是在之前的实例中,没有使用这种处理方式而已。

    引入Observables的操作符

    在添加上面的debounceTime(1000)方法以后,实际上编辑器会报编译错误,是因为我们还需要手动的引入相关的操作符。我们可以把这个引入的定义添加到这个ReactiveFormsComponent所在的文件中,也可以在这个组件的任意父组件中引入。区别就是,在父组件中引入的话,那么在其他组件中也能够使用。所以,我们就在app.module.ts中引入,这样在所有的组件中都可以使用这些方法。

    1
    2
    import 'rxjs/add/operator/debounceTime';
    import 'rxjs/add/operator/distinctUntilChanged';

    为什么我们需要一个一个方法的引入呢?因为这样,我们就只需要引入我们用到的方法,不需要的部分就不会被引入。这样在最后打包的时候,不需要的部分不会被打包,就能够减少代码量。

    模板驱动和模型驱动表单的区别

    看到这里,其实就会发现,这两种表单创建方式,主要有2个区别:

    1. 数据模型、验证器的定义
      使用模型驱动的表单,我们的页面可以很整洁,没有验证器,没有数据绑定,没有onChange, onBlur等事件绑定。我们可以在组件代码里订阅数据更新的事件,实现数据之间的交互。
    2. 是否可以单元测试
      目前我们还没有使用任何测试方法,但是,在Angular的最佳实践里,测试是很重要的,它包括2中测试,单元测试以及端到端测试,单元测试可以不依赖浏览器,而端到端测试的执行必须在浏览器里执行。而模型驱动的表单,由于他的数据模型、验证器、交互等都是在组件中,我们可以完全不依赖浏览器就在单元测试里面测试表单的各种逻辑。

    所以,当你有一个表单,考虑用哪种表单创建方式的时候,如果这个表单比较简单,甚至不需要测试,那就用模板驱动的表单就可以。如果表单比较复杂,就应该考虑使用模型驱动的表单方式。


  • 相关阅读:
    c#+linux+mono+Redis集群(解决无法连接Redis的问题)
    实验楼----奇妙的音乐
    实验楼----PHP大法
    实验楼----PHP代码审计(sha1、md5)
    实验楼----变异凯撒
    storm安装
    storm问题汇总
    windows下linux子系统安装
    mongoDB学习记录
    excel vba 不可查看
  • 原文地址:https://www.cnblogs.com/ztguang/p/12645284.html
Copyright © 2020-2023  润新知