威尼斯网址开户网站Angular:自定义表单控件

本文主要给大家介绍如何判断验证器的结果。在这里,我们就来看看怎样实现一个自定义的验证器。

Angular2表单自定义验证器的实现,angular2表单

本文主要给大家介绍如何判断验证器的结果。在这里,我们就来看看怎样实现一个自定义的验证器。

目标

我们要实现一个验证手机号的验证器,使用的实例还是基于之前的文章里面的实例,也就是用户信息输入的表单页面。我们在手机号的元素上添加一个验证手机号的验证器。然后,如果手机号验证失败,就显示一个错误,页面如下:

威尼斯网址开户网站 1

这部分教程的代码可以从github获取:

git clone

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

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

实现验证器

在Angular2中,实现一个验证器非常简单,就是一个方法就可以,该方法的参数是一个FormControl,结果是一个错误对象或者null。用TypeScript接口表示,就是这样:

interface Validator<T extends FormControl> {
(c:T): {[error: string]:any};
}

如果是对类似Java这样的面向对象语言比较了解的话,上面的接口定义就很容易理解。其中<T
extends
FormControl>是指这个方法中用到一个泛型T,它是一个继承自FormControl的对象。(c:T):
{[error:
string]:any};这是一个lambda表达式的方法定义,参数c的类型为T,这个方法返回一个对象。

我们创建一个名为mobile.validator.ts的文件,它的内容如下:

import { FormControl } from '@angular/forms';
export function validateMobile(c: FormControl) {
let MOBILE_REGEXP = /^1[0-9]{10,10}$/;
return MOBILE_REGEXP.test(c.value) ? null : {
validateMobile: {valid: false}
}
}

在这个验证方法里,参数c的类型为FormControl,也就是表单控件,他有一个value属性,存放当前的值。我们使用正则表达式,来判断这个值是否合法。如果不合法,就返回一个对象。
在之前的教程中,我们对验证器的验证结果是这样获得的:

<p *ngIf="userForm.controls.mobile?.errors?.required">必须输入电话</p>

userForm.controls.mobile就是表单中手机号这个控件,required是required验证器对应的key,当required验证器验证失败时,就会在errors里面添加一个值:

{
required: {valid: false}
}

所以,我们实现的自定义的验证器,也要把验证结果用验证器的名字作为key,放到errors里面,就是这样:

{
validateMobile: {valid: false}
}

这样,我们就能够在页面中用跟之前同样的方式来获得这个验证器的验证结果。

在模型驱动的表单里添加验证器

接下来,我们把我们实现的验证器添加到我们的表单里,先加到模型驱动的表单里:

import { validateMobile } from '../validators/mobile.validator';
export class ReactiveFormsComponent implements OnInit {
this.userForm = this.formBuilder.group({
// ... 省略其他控件
mobile: [13800138001, [Validators.required, Validators.minLength(11), Validators.maxLength(11), validateMobile]]
});
...
}

上面的代码省略了其他的部分,完整的代码,请参考github。

在上面的代码中,我们引入了之前实现的自定义的验证器,然后在表单控件创建代码中,对mobile控件加了一个validateMobile。

这样,我们在页面上添加相应的验证结果信息:

<p *ngIf="userForm.controls.mobile.errors?.validateMobile">电话号码格式不正确</p>

这样就完成了验证器,以及在页面显示验证结果,就这么简单。

在模板驱动的表单里添加验证器

但是,如果我们的表单不是在组件里用模型驱动的方式创建的,而是在页面上用html元素创建的,那么使用自定义的验证器就稍微麻烦一点。

在一个模板驱动的表单里,我们是这样使用验证器的:

<input type="text" name="mobile" [(ngModel)]="user.mobile" #mobile="ngModel" required minlength="11" maxlength="11">
有效
<div [hidden]="mobile.valid || mobile.pristine">
<p *ngIf="mobile.errors?.minlength || mobile.errors?.maxlength">电话长度必须为11</p>
<p *ngIf="mobile.errors?.required">必须输入姓名</p>
</div>

也就是在input输入元素的属性中添加验证器。那么,我们要实现自己的验证器在表单里面使用,除了上面的验证器方法里面,还需要2件事情:

我们需要将这个验证器定义成一个指令Directive,这样Angular在解析这段html的时候,会识别我们自定义的验证器指令。
我们还需要Angular的验证器调用我们的验证方法。
所以,在之前的mobile.validator.ts文件里,添加下面的指令定义:

@Directive({
selector: '[validateMobile][ngModel]'
})
export class MobileValidator {}

这段代码很简单,就是用@Directive标签定义了一个指令MobileValidator,它作用的元素是同时具有validateMobile和ngModel属性的元素。这样,我们就可以在手机号的元素上添加一个属性,来使这个验证器指令起作用。
然后,我们还需要Angular的验证器框架能够调用我们的验证方法,这就需要NG_VALIDATORS。我们修改上面的验证器的指令定义如下:

@Directive({
selector: '[validateMobile][ngModel]',
providers: [
{ provide: NG_VALIDATORS, useValue: validateMobile, multi: true }
]
})
export class MobileValidator {}

这样Angular的验证器就能够将validateMobile方法应用在这个指令上。

最后,我们再把这个新的指令,添加到AppModule的declarations里面,就可以在页面上使用这个验证器了。

最后,页面上使用验证器的代码如下:

<input type="text" name="mobile" [(ngModel)]="user.mobile" #mobile="ngModel" required minlength="11" maxlength="11" validateMobile>
有效
<div [hidden]="mobile.valid || mobile.pristine">
<p *ngIf="mobile.errors?.minlength || mobile.errors?.maxlength">电话长度必须为11</p>
<p *ngIf="mobile.errors?.required">必须输入姓名</p>
<p *ngIf="mobile.errors?.validateMobile">电话号码格式不正确</p>
</div>

以上所述是小编给大家介绍的Angular2表单自定义验证器,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对帮客之家网站的支持!

本文主要给大家介绍如何判断验证器的结果。在这里,我们就来看看怎样实现一个自定义的验…

当我们打算自定义表单控件前,我们应该先考虑一下以下问题:

分享一个最近写的支持表单验证的时间选择组件。

我们在构建企业级应用时,通常会遇到各种各样的定制化功能,因为每个企业都有自己独特的流程、思维方式和行为习惯。有很多时候,软件企业是不太理解这种情况,习惯性的会给出一个诊断,『你这么做不对,按逻辑应该这样这样』。但企业往往不会接受这种说法,习惯的力量是强大的,我们一定要尊重这种事实。所以在构建企业应用的时候,我们不仅仅要了解对方的基本需求,也要了解他们习惯于怎么处理流程,在设计的时候需要予以充分重视。当然这也不是说客户说怎么改我们就怎么改,而是要了解到对方真正的诉求和背后的原因,在产品规划设计的时候,将这种因素考虑进去,才能在维持产品统一的框架下满足不同用户的需求。

目标

  • 是否已经有相同语义的 native (本机) 元素?如:<input type="number">
  • 如果有,我们就应该考虑能否依赖该元素,仅使用 CSS
    或渐进增强的方式来改变其外观/行为就能满足我们的需求?
  • 如果没有,自定义控件会是什么样的?
  • 我们如何让它可以访问 (accessible)?
  • 在不同平台上自定义控件的行为是否有所不同?
  • 自定义控件如何实现数据验证功能?
import {AfterViewInit, Component, forwardRef, Input, OnInit, Renderer} from "@angular/core";
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from "@angular/forms";
@Component({
    selector: 'time-picker',
    template: `
        <div id="container">
            <input id="input_box" readonly
                   [(ngModel)]="_value"
                   [ngStyle]="{'width.px': inputWidth, 'height.px': inputHeight}"
                   (click)="onInputClick($event)"
                   (focus)="onInputFocus($event)"
                   (blur)="onInputBlur()">

            <div id="panel" *ngIf="showBox">
                <div class="ui-g">
                    <div class="ui-g-4 title">小时</div>
                    <div class="ui-g-8 title">分钟</div>
                </div>
                <div class="ui-g">
                    <div class="ui-g-4">
                        {{hour}}
                    </div>
                    <div class="ui-g-8">
                        {{minute}}
                    </div>
                </div>
            </div>
        </div>
    `,
    styles: [`
        :host{
            display: inline-block;
        }
        #container{
            position: relative;
        }
        #input_box{
            outline: none;
            box-sizing: border-box;
            padding: 0 3px;
        }
        #panel{
            position: absolute;
            width: 400px;
            background-color: white;
            box-shadow: 0 2px 8px 4px rgba(0,0,0,0.2);
            z-index: 2000;
        }
        .title{
            text-align: center;
        }
        .item{
            display: inline-block;
            width: 30px;
            height: 30px;
            line-height: 30px;
            text-align: center;
            cursor: pointer;
        }
        .item:hover, .item.selected{
            background-color: #0b7dd8;
            color: white;
        }
    `],
    providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: forwardRef(() => TimePicker), // 此时TimePicker未声明,因此需要使用forwardRef
            multi: true
        }
    ]
})
export class TimePicker implements ControlValueAccessor, OnInit, AfterViewInit{ // ControlValueAccessor,一座表单控件和原生元素或自定义输入组件之间的桥梁
    @Input() inputWidth = 100; // 输入框宽度
    @Input() inputHeight = 30; // 输入框高度
    _value: string;
    showBox = false; // 控制选择面板显示与否,true为显示
    hours = []; // 小时数组
    minutes = []; // 分钟数组
    hourIsSelect = false; // 打开选择面板后,标记小时是否被点击
    minIsSelect = false; // 打开选择面板后,标记分钟是否被点击

    selectedHour; // 当前小时
    selectedMinute; // 当前分钟

    bodyClickListener: any;

    constructor(
        public renderer: Renderer
    ){}

    ngOnInit(){
        for(let i = 0; i < 24; i++){
            let h;
            if(i < 10){
                h = '0' + i;
            }else{
                h = '' + i;
            }
            this.hours.push(h);
        }
        for(let j = 0; j < 60; j++){
            let m;
            if(j < 10){
                m = '0' + j;
            }else{
                m = '' + j;
            }
            this.minutes.push(m);
        }
    }

    ngAfterViewInit(){
        this.bodyClickListener = this.renderer.listenGlobal('body','click', () => { this.hide() });
    }

    onChange = (time: string) => {};
    onTouched = () => {};

    get value(): string{
        return this._value;
    }

    set value(val: string){
        if(val !== this._value){
            this._value = val;
            this.onChange(val);
        }
    }

    /**
     * 实现ControlValueAccessor中的方法,用于将model显示到view
     * @param val
     */
    writeValue(val: string): void{
        if(val !== this._value){
            this._value = val;
        }
        if(this._value){
            let time = this._value.split(':');
            this.selectedHour = time[0];
            this.selectedMinute = time[1];
        }
    }

    /**
     * 实现ControlValueAccessor中的方法,用于通知Angular值已被修改
     * @param fn
     */
    registerOnChange(fn: (time: string) => void): void{
        this.onChange = fn;
    }

    /**
     * 实现ControlValueAccessor中的方法,用于通知Angular输入框被鼠标聚焦过
     * @param fn
     */
    registerOnTouched(fn: () => void): void{
        this.onTouched = fn;
    }

    /**
     * 隐藏选择面板
     */
    hide(){
        this.showBox = false;
        this.hourIsSelect = false;
        this.minIsSelect = false;
    }

    /**
     * 显示选择面板
     */
    show(){
        this.showBox = true;
    }

    /**
     * 输入框获得焦点
     * @param event
     */
    onInputFocus(event){
        event.stopPropagation();
        this.show();
    }

    /**
     * 输入框失去焦点
     */
    onInputBlur(){
        this.onTouched();
    }

    /**
     * 输入框点击
     * @param event
     */
    onInputClick(event){
        event.stopPropagation();
    }

    /**
     * 选择小时
     * @param h
     * @param event
     */
    onHourClick(h, event){
        event.stopPropagation();

        this.selectedHour = h;
        this.value = this.selectedHour + ':' + (this.selectedMinute ? this.selectedMinute : '00');

        this.hourIsSelect = true;
        if(this.hourIsSelect && this.minIsSelect){
            this.hide();
        }
    }

    /**
     * 选择分钟
     * @param min
     * @param event
     */
    onMinuteClick(min, event){
        event.stopPropagation();

        this.selectedMinute = min;
        this.value = (this.selectedHour ? this.selectedHour : '00') + ':' + this.selectedMinute;

        this.minIsSelect = true;
        if(this.hourIsSelect && this.minIsSelect){
            this.hide();
        }
    }
}

那么这里我们举一个例子,比如我们正在开发一个医疗卫生领域的企业软件,客户要求提供一个出生日期的控件,但这个控件不光可以输入年月日,而且可以输入年龄数值以及选择年龄单位。客户的希望是:

我们要实现一个验证手机号的验证器,使用的实例还是基于之前的文章里面的实例,也就是用户信息输入的表单页面。我们在手机号的元素上添加一个验证手机号的验证器。然后,如果手机号验证失败,就显示一个错误,页面如下:

可能还有很多事情需要考虑,但如果我们决定使用 Angular
创建自定义控件,就需要考虑以下问题:

  1. 填写日期时,年龄和年龄单位随之变化
  2. 填写年龄和选择年龄单位时出生日期也随之变化

威尼斯网址开户网站 2

  • 如何实现 model -> view 的数据绑定?
  • 如何实现 view -> model 的数据同步?
  • 若需要自定义验证,应该如何实现?
  • 如何向DOM元素添加有效性状态,便于设置不同样式?
  • 如何让控件可以访问 (accessible)?
  • 该控件能应用于 template-driven 表单?
  • 该控件能应用于 model-driven 表单?

看起来好像很无用的一个需求,这个在面向互联网的应用中确实如此。但在特定领域,其实有其背景原因,比如客户提出这个需求是由于很多人,尤其是小城镇的,是不记公历生日的,这样会导致出生日期不是很准确,另外还会有一些人的身份证日期和真实年龄是不一致的。这种情况对于成人来说还好,但对于儿童来说就偏差很大,但一般人会记得孩子现在是多少天或多少个月大。这样的话是不是觉得这个需求还有些道理?

这部分教程的代码可以从github获取:

(备注:主要浏览器上 HTML 5 当前辅助功能支持状态,可以参看 – HTML5
Accessibility)

那么我们就接着来看一下这个需求应该怎样实现,首先分析一下:

git clone

Creating a custom counter

现在我们从最简单的 Counter 组件开始,具体代码如下:

counter.component.ts

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

@Component({
    selector: 'exe-counter',
    template: `
    <div>
      <p>当前值: {{ count }}</p>
      <button (click)="increment()"> + </button>
      <button (click)="decrement()"> - </button>
    </div>
    `
})
export class CounterComponent {
    @Input() count: number = 0;

    increment() {
        this.count++;
    }

    decrement() {
        this.count--;
    }
}

app.component.ts

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

@Component({
  selector: 'exe-app',
  template: `
    <exe-counter></exe-counter>
  `,
})
export class AppComponent { }

app.module.ts

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

import { CounterComponent } from './couter.component';
import { AppComponent } from './app.component';

@NgModule({
  imports: [BrowserModule],
  declarations: [AppComponent, CounterComponent],
  bootstrap: [AppComponent],
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AppModule { }

很好,CounterComponent 组件很快就实现了。但现在我们想在
Template-DrivenReactive 表单中使用该组件,具体如下:

<!-- this doesn't work YET -->
<form #form="ngForm">
  <exe-counter name="counter" ngModel></exe-counter>
  <button type="submit">Submit</button>
</form>

现在我们还不能直接这么使用,要实现该功能。我们要先搞清楚
ControlValueAccessor,因为它是表单模型和DOM 元素之间的桥梁。

  1. 无论是输入出生日期还是年龄,其实最终要得到一个日期,也就是说年龄只是得到日期的一个辅助手段。
  2. 年龄单位的转换我们需要有一个界定,否则切换起来没有规则的话会导致逻辑的混乱。那这里我们定义一下:以天为单位时的上限为:90,下限为
    0,也就是只有小于等于 90
    天的婴儿我们会使用天作为年龄单位。类似的,以月为单位的上限为
    24,下限为 1;以年为单位的上限为 150,下限为 1。
  3. 威尼斯网址开户网站,同样的出生日期的验证规则为:这个日期不能是未来的时间,一定是小于等于当前时间的,再有就是年龄的上限既然是
    150,那么出生日期也不能比当前日期减去 150 年更早,对吗?
  4. 联动的规则应该是调整出生日期时,会将日期按上面规则转换成年龄和单位,改变控件中的值;而调整年龄或者单位的时候,我们会根据年龄推算出出生日期,当然这里是估算,以当前日期减去年龄得出,然后更新出生日期输入框中的值。

Understanding ControlValueAccessor

当我们运行上面示例时,浏览器控制台中将输出以下异常信息:

Uncaught (in promise): Error: No value accessor for form control with name: 'counter'

那么,ControlValueAccessor
是什么?那么你们还记得我们之前提到的实现自定义控件需要确认的事情么?其中一个要确认的事情就是,要实现
Model -> View,View -> Model 之间的数据绑定,而这就是我们
ControlValueAccessor 要处理的问题。

ControlValueAccessor 是一个接口,它的作用是:

  • 把 form 模型中值映射到视图中
  • 当视图发生变化时,通知 form directives 或 form controls

Angular
引入这个接口的原因是,不同的输入控件数据更新方式是不一样的。例如,对于我们常用的文本输入框来说,我们是设置它的
value 值,而对于复选框 (checkbox) 我们是设置它的 checked
属性。实际上,不同类型的输入控件都有一个
ControlValueAccessor,用来更新视图。

Angular 中常见的 ControlValueAccessor 有:

  • DefaultValueAccessor – 用于 texttextarea 类型的输入控件
  • SelectControlValueAccessor – 用于 select 选择控件
  • CheckboxControlValueAccessor – 用于 checkbox 复选控件

接下来我们的 CounterComponent 组件需要实现 ControlValueAccessor
接口,这样我们才能更新组件中 count 的值,并通知外界该值已发生改变。

威尼斯网址开户网站 3

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

Implementing ControlValueAccessor

首先我们先看一下 ControlValueAccessor 接口,具体如下:

// angular2/packages/forms/src/directives/control_value_accessor.ts 
export interface ControlValueAccessor {
  writeValue(obj: any): void;
  registerOnChange(fn: any): void;
  registerOnTouched(fn: any): void;
  setDisabledState?(isDisabled: boolean): void;
}
  • writeValue(obj: any):该方法用于将模型中的新值写入视图或 DOM
    属性中。
  • registerOnChange(fn: any):设置当控件接收到 change
    事件后,调用的函数
  • registerOnTouched(fn: any):设置当控件接收到 touched
    事件后,调用的函数
  • setDisabledState?(isDisabled: boolean):当控件状态变成 DISABLED
    或从 DISABLED 状态变化成 ENABLE
    状态时,会调用该函数。该函数会根据参数值,启用或禁用指定的 DOM
    元素。

接下来我们先来实现 writeValue() 方法:

@Component(...)
class CounterComponent implements ControlValueAccessor {
  ...
  writeValue(value: any) {
    this.counterValue = value;
  }
}

当表单初始化的时候,将会使用表单模型中对应的初始值作为参数,调用
writeValue()
方法。这意味着,它会覆盖默认值0,一切看来都没问题。但我们回想一下在表单中
CounterComponent 组件预期的使用方式:

<form #form="ngForm">
  <exe-counter name="counter" ngModel></exe-counter>
  <button type="submit">Submit</button>
</form>

你会发现,我们没有为 CounterComponent 组件设置初始值,因此我们要调整一下
writeValue() 中的代码,具体如下:

writeValue(value: any) {
  if (value) {
    this.count = value;
  }
}

现在,只有当合法值 (非 undefined、null、””)
写入控件时,它才会覆盖默认值。接下来,我们来实现 registerOnChange()
registerOnTouched() 方法。registerOnChange()
可以用来通知外部,组件已经发生变化。registerOnChange() 方法接收一个 fn
参数,用于设置当控件接收到 change 事件后,调用的函数。而对于
registerOnTouched() 方法,它也支持一个 fn 参数,用于设置当控件接收到
touched 事件后,调用的函数。示例中我们不打算处理 touched 事件,因此
registerOnTouched() 我们设置为一个空函数。具体如下:

@Component(...)
class CounterComponent implements ControlValueAccessor {
  ...
  propagateChange = (_: any) => {};

  registerOnChange(fn: any) {
    this.propagateChange = fn;
  }

  registerOnTouched(fn: any) {}
}

很好,我们的 CounterComponent 组件已经实现了ControlValueAccessor
接口。接下来我们需要做的是在每次count 的值改变时,需要调用
propagateChange() 方法。换句话说,当用户点击了 +-
按钮时,我们希望将新值传递到外部。

@Component(...)
export class CounterComponent implements ControlValueAccessor {
    ...
    increment() {
        this.count++;
        this.propagateChange(this.count);
    }

    decrement() {
        this.count--;
        this.propagateChange(this.count);
    }
}

是不是感觉上面代码有点冗余,接下来我们来利用属性修改器,重构一下以上代码,具体如下:

counter.component.ts

import { Component, Input } from '@angular/core';
import { ControlValueAccessor } from '@angular/forms';

@Component({
    selector: 'exe-counter',
    template: `
      <p>当前值: {{ count }}</p>
      <button (click)="increment()"> + </button>
      <button (click)="decrement()"> - </button>
    `
})
export class CounterComponent implements ControlValueAccessor {
    @Input() _count: number = 0;

    get count() {
        return this._count;
    }

    set count(value: number) {
        this._count = value;
        this.propagateChange(this._count);
    }

    propagateChange = (_: any) => { };

    writeValue(value: any) {
        if (value !== undefined) {
            this.count = value;
        }
    }

    registerOnChange(fn: any) {
        this.propagateChange = fn;
    }

    registerOnTouched(fn: any) { }

    increment() {
        this.count++;
    }

    decrement() {
        this.count--;
    }
}

CounterComponent
组件已经基本开发好了,但要能正常使用的话,还需要执行注册操作。

一个定制化的日期选择控件

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

Registering the ControlValueAccessor

对于我们开发的 CounterComponent 组件来说,实现 ControlValueAccessor
接口只完成了一半工作。要让 Angular 能够正常识别我们自定义的
ControlValueAccessor,我们还需要执行注册操作。具体方式如下:

  • 步骤一:创建 EXE_COUNTER_VALUE_ACCESSOR

import { Component, Input, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

export const EXE_COUNTER_VALUE_ACCESSOR: any = {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => CounterComponent),
    multi: true
};

友情提示:想了解 forwardRef 和 multi 的详细信息,请参考 Angular 2
Forward
Reference
和 Angular 2 Multi
Providers
这两篇文章。

  • 步骤二:设置组件的 providers 信息

@Component({
    selector: 'exe-counter',
    ...
    providers: [EXE_COUNTER_VALUE_ACCESSOR]
})

万事俱备只欠东风,我们马上进入实战环节,实际检验一下我们开发的
CounterComponent 组件。完整代码如下:

counter.component.ts

import { Component, Input, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

export const EXE_COUNTER_VALUE_ACCESSOR: any = {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => CounterComponent),
    multi: true
};

@Component({
    selector: 'exe-counter',
    template: `
    <div>
      <p>当前值: {{ count }}</p>
      <button (click)="increment()"> + </button>
      <button (click)="decrement()"> - </button>
    </div>
    `,
    providers: [EXE_COUNTER_VALUE_ACCESSOR]
})
export class CounterComponent implements ControlValueAccessor {
    @Input() _count: number = 0;

    get count() {
        return this._count;
    }

    set count(value: number) {
        this._count = value;
        this.propagateChange(this._count);
    }

    propagateChange = (_: any) => { };

    writeValue(value: any) {
        if (value) {
            this.count = value;
        }
    }

    registerOnChange(fn: any) {
        this.propagateChange = fn;
    }

    registerOnTouched(fn: any) { }

    increment() {
        this.count++;
    }

    decrement() {
        this.count--;
    }
}

但这里面有几个值得注意的地方:

实现验证器

Using it inside template-driven forms

Angular 4.x 中有两种表单:

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

了解 Angular 4.x Template-Driven Forms 详细信息,请参考 – Angular 4.x
Template-Driven
Forms。接下来我们来看一下具体如何使用:

  1. 可能存在反复联动的问题,比如改变出生日期后,年龄和单位随之改变,这又引发了由年龄和单位的变化而导致的出生日期的重算。
  2. 如果输入非法的值,可能导致计算出现异常,因而控件状态出现不正确的状态值,进一步影响未来的计算。
  3. 如果每次输入改动都会引发重新计算,会带来大量的过程中无用计算,耗费资源,因此需要进行对输入事件的『整流』控制。

在Angular2中,实现一个验证器非常简单,就是一个方法就可以,该方法的参数是一个FormControl,结果是一个错误对象或者null。用TypeScript接口表示,就是这样:

1.导入 FormsModule 模块

app.module.ts

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

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

搭建自定义表单控件的框架

首先为什么要实现一个自定义表单控件?我们当然可以直接把这个逻辑放在表单中,但问题是表单真的需要关心这几个框的联动吗?

其实从表单的角度看,它只要一个值:那就是经过计算的出生日期。至于你是手动输入的还是按年龄和单位计算的,表单根本就不应该关心。另外一点是随着表单的复杂化,如果我们不把这些逻辑剥离出去的话,我们的表单本身的逻辑就会越来越复杂。最后是,封装成表单控件意味着我们以后可以复用这个控件了。

知道了 why,我们看看 how。在 Angular
中实现一个自定义的表单控件还是比较简单的,下面是一个表单控件的骨架。

import {ChangeDetectionStrategy, Component, forwardRef, OnInit, OnDestroy, Input} from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormControl, FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';

@Component({
  selector: 'app-age-input',
  template: `
    // 省略
    `,
  styles: [`
    // 省略
  `],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => AgeInputComponent),
      multi: true,
    },
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => AgeInputComponent),
      multi: true,
    }
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AgeInputComponent implements ControlValueAccessor {

  private propagateChange = (_: any) => {};

  constructor() { }

  // 提供值的写入方法
  public writeValue(obj: Date) 
  }

  // 当表单控件值改变时,函数 fn 会被调用
  // 这也是我们把变化 emit 回表单的机制
  public registerOnChange(fn: any) {
    this.propagateChange = fn;
  }

  // 这里没有使用,用于注册 touched 时的回调函数
  public registerOnTouched() {
  }

  // 验证表单,验证结果正确返回 null 否则返回一个验证结果对象
  validate(c: FormControl): {[key: string]: any} {
    // 省略
  }
}

我们可以看到要实现一个表单控件的话,要实现 ControlValueAccessor
这样一个接口。这个接口顾名思义是用于写入控件值的,它是一个控件和原生 DOM
元素之间的桥梁,通过实现这个接口,我们可以对原生 DOM
元素写入值。而这个接口需要实现三个必选方法: writeValue(obj: any)
registerOnChange(fn: any)registerOnTouched(fn: any)

  • writeValue(obj: any):用于向元素中写入值
  • registerOnChange(fn: any):设置一个当控件接受到改变的事件时所要调用的函数。
  • registerOnTouched(fn: any):设置一个当控件接受到 touch
    事件时所要调用的函数。

另外的一个 validate(c: FormControl): {[key: string]: any}
是控件的验证器函数。除了这些函数,你应该也注意到,我们注册了两个
provider,一个的 token 是 NG_VALUE_ACCESSOR 这是将控件本身注册到 DI
框架成为一个可以让表单访问其值的控件。但问题来了,如果在元数据中注册了控件本身,而此时控件仍为创建,这怎么破?这就得用到
forwardRef 了,这个函数允许我们引用一个尚未定义的对象。另外一个
NG_VALIDATORS 是让控件注册成为一个可以让表单得到其验证状态的控件
。当然这里还有一个奇怪的东西,就是那个 multi: true,,这是声明这个
token 对应的类很多,分散在各处。

interface Validator<T extends FormControl> {
(c:T): {[error: string]:any};
}

2.更新 AppComponent

控件的界面

我们这里使用了 @angular/materialinputdatepicker
button-toggle
控件来分别实现日期输入、年龄输入和年龄单位的选择。注意到我们在里面使用了响应式表单,这感觉好像有点怪,我们本身不是一个表单控件吗?怎么自己的模板还是一个表单?这个其实没啥问题,因为
Angular 中的组件是和外界隔离的,所以组件自身的模板其实想怎么使用都可以。

<div [formGroup]="form" class="age-input">
  <div>
    <md-input-container>
      <input mdInput [mdDatepicker]="birthPicker" type="text" placeholder="出生日期" formControlName="birthday" >
      <button mdSuffix [mdDatepickerToggle]="birthPicker" type="button"></button>
      <md-error>日期不正确</md-error>
    </md-input-container>
    <md-datepicker touchUi="true" #birthPicker></md-datepicker>
  </div>
  <ng-container formGroupName="age">
    <div class="age-num">
      <md-input-container>
        <input mdInput type="number" placeholder="年龄" formControlName="ageNum">
      </md-input-container>
    </div>
    <div>
      <md-button-toggle-group formControlName="ageUnit" [(ngModel)]="selectedUnit">
        <md-button-toggle *ngFor="let unit of ageUnits" [value]="unit.value">
          {{ unit.label }}
        </md-button-toggle>
      </md-button-toggle-group>
    </div>
    <md-error class="mat-body-2" *ngIf="form.get('age').hasError('ageInvalid')">年龄或单位不正确</md-error>
  </ng-container>
</div>

上面这个模板中值得注意的一点是,我们把年龄的数值和单位放在了一个
FormGroup
里面,这是由于这两个值组合在一起才有意义,而且后面的表单验证也是这两个值在一起组合后验证。

如果是对类似Java这样的面向对象语言比较了解的话,上面的接口定义就很容易理解。其中<T
extends
FormControl>是指这个方法中用到一个泛型T,它是一个继承自FormControl的对象。(c:T):
{[error:
string]:any};这是一个lambda表达式的方法定义,参数c的类型为T,这个方法返回一个对象。

2.1 未设置 CounterComponent 组件初始值

app.component.ts

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

@Component({
  selector: 'exe-app',
  template: `
    <form #form="ngForm">
      <exe-counter name="counter" ngModel></exe-counter>
    </form>
    <pre>{{ form.value | json }}</pre>
  `,
})
export class AppComponent { }

友情提示:上面示例代码中,form.value 用于获取表单中的值,json 是
Angular 内置管道,用于执行对象序列化操作 (内部实现 –
JSON.stringify(value, null, 2))。若想了解 Angular 管道详细信息,请参考

  • Angular 2
    Pipe。

使用 Rx 的事件流来重新梳理逻辑

私以为 Rx 的两大优点:

  1. 由于在 Rx
    世界里,一切都是事件流,所以这『逼迫』开发者将时间维度纳入设计的考量
  2. 提供的各种强大的操作符可以将逻辑非常轻松的组合

那么从 Rx
的角度看的话,这个控件会产生三个事件流:出生日期、年龄数值和年龄单位:

出生日期:-------d----------d---------------d--------------
年龄数值:----------num----------num----------------num----
年龄单位:----unit-------------unit-------------unit-------

写成代码的话就是下面的样子,Angular
的响应式表单为我们提供了非常便利的方法可以得到这些变化的事件流,FormControl
valueChanges 属性就是一个 Observable

// 得到出生日期的值的变化流
const birthday$ = this.form.get('birthday').valueChanges;
// 得到年龄数值的变化流
const ageNum$ = this.form.get('age').get('ageNum').valueChanges;
// 得到年龄单位的变化流
const ageUnit$ = this.form.get('age').get('ageUnit').valueChanges;

由于年龄数值和年龄单位需要合并在一起才有意义,所以这两个流需要做一个合并操作,而且不管是数值变化还是单位变化,我们都要在新的合并流中有一个反映:

年龄数值:----------n1----------------n2------------------n3-------
年龄单位:----u1-------------u2------------------u3----------------
合并后:  ------(n1,u1)--(n1,u2)--(n2,u2)----(n2,u3)---(n3,u3)---

仔细观察一下,你可能会发现这个合并流还有一个特点就是只有在参与合并的两个流都有事件产生后才会有合并的事件发生,在这之后就是任何一个参与合并的流有新的事件,合并流就会产生一个事件,这个合并的值会取刚刚发生的那个事件和另一个参与合并的流中的『最新』事件。这种合并方法在
Rx 中叫做 combineLatest

const age$ = Observable
      .combineLatest(ageNum$, ageUnit$, (_num, _unit) => this.toDate({age: _num, unit: _unit}));

上面的代码中,我们将年龄数值的事件流(ageNum$)以及年龄单位的事件流(ageUnit$)做了合并,而且通过一个
this.toDate 的工具函数将年龄和单位计算出了一个估算的出生日期。

出生日期:-------d----------d---------------d--------------
年龄合并:---d^----d^----d^---d^--------d^------d^---------
// 年龄合并后产生的出生日期用 d^ 来标识

现在看起来这两个流都产生日期,只不过是不同的控件变化引起的。那么我们应该可以把它们也做一个合并,这个合并就比较简单,可以想象成按照各自流中的位置把两个流做投影。

最终合并:---d^--d--d^----d^--d-d^-------d^--d----d^-------

而这种合并在 Rx 中叫做 merge

const merge$ = Observable.merge(birthday$, age$);

但为了要能区分这个日期是来自于出生日期那个输入框还是来自于年龄和单位的输入变化,我们得标识出这个日期的来源。所以我们需要对
birthday$age$
做一个变换处理,不在单纯的发射日期,而是将日期和来源组合成一个新的对象
{date: string; from: string} 发射。

const birthday$ = this.form.get('birthday').valueChanges
      .map(d => ({date: d, from: 'birthday'}));
const age$ = Observable
      .combineLatest(ageNum$, ageUnit$, (_num, _unit) => this.toDate({age: _num, unit: _unit}))
      .map(d => ({date: d, from: 'age'}));

这样处理之后,我们就可以根据不同情况,根据日期设置年龄和单位,或者反之,由年龄和单位的变化设置出生日期。

this.subBirth = merged$.subscribe(date => {
  const age = this.toAge(date.date);
  const ageNum = this.form.get('age').get('ageNum');
  const ageUnit = this.form.get('age').get('ageUnit');
  if(date.from === 'birthday') {
    if(age.age === ageNum.value && age.unit === ageUnit.value) {
      return;
    }
    ageUnit.patchValue(age.unit, {emitEvent: false, emitModelToViewChange: true, emitViewToModelChange: true});
    ageNum.patchValue(age.age, {emitEvent: false});
    this.selectedUnit = age.unit;
    this.propagateChange(date.date);

  } else {
    const ageToCompare = this.toAge(this.form.get('birthday').value);
    // 如果要设置的日期换算成年龄和单位,如果这两个值和现有控件的值是一样的,那就没有必要更新日期的值了
    if(age.age !== ageToCompare.age || age.unit !== ageToCompare.unit) {
      this.form.get('birthday').patchValue(date.date, {emitEvent: false});
      this.propagateChange(date.date);
    }
  }
});

大致的逻辑就是这样了,但我们还有几个问题需要解决

  1. 现在的情况是不管你以多快的速度输入日期,或者输错了按 backspace
    都会产生新的事件,也因此会有计算。但显然这样做一方面浪费了性能,另一方面会导致一些不合法的值大量出现(比如本来要输入
    2000-12-11 , 但事实上现在当你刚刚敲了 2
    ,事件就已经产生了,但显然年份 2
    不是一个合理的出生年份,我们毕竟不是在做一个考古信息系统)。
  2. 当你和上一次输入相同的值时,现在的系统仍然会发射事件,但这其实是在做无用功。
  3. 我们现在的事件流没有经过一个验证就会把数据发射出来,但一个没有验证成功的值其实对我们来说是没有意义的。
  4. 年龄和单位的合并流只有在年龄和单位都产生变化的时候才开始发射,但一开始的初始状态,这两个控件并没有值,这显然不是我们希望的(比如你可能不想填完年龄,例如
    30,然后还得点一下『天』,再点回『岁』来得到合并计算的值)。

const birthday$ = this.form.get('birthday').valueChanges
  .map(d => ({date: d, from: 'birthday'}))
  .debounceTime(300)
  .distinctUntilChanged()
  .filter(date => this.form.get('birthday').valid);
const ageNum$ = this.form.get('age').get('ageNum').valueChanges
  .startWith(this.form.get('age').get('ageNum').value)
  .debounceTime(300)
  .distinctUntilChanged();
const ageUnit$ = this.form.get('age').get('ageUnit').valueChanges
  .startWith(this.form.get('age').get('ageUnit').value)
  .debounceTime(300)
  .distinctUntilChanged();
const age$ = Observable
  .combineLatest(ageNum$, ageUnit$, (_num, _unit) => this.toDate({age: _num, unit: _unit}))
  .map(d => ({date: d, from: 'age'}))
  .filter(_ => this.form.get('age').valid);
const merged$ = Observable
  .merge(birthday$, age$)
  .filter(_ => this.form.valid);

上面的代码中,我们使用 debounceTime
过滤掉了短时间内的输入,等待用户略有停顿或输入完成时才发射新的事件。我们还使用了
distinctUntilChanged 来过滤掉和之前一样的输入。而 startWith
其实是在帮事件流拼接一个初始值,使得合并流按我们想像中那样运行。那么
filter 则是屏蔽掉验证未通过的数据。

这样简单的通过几个 Rx
的操作符我们就完成了核心逻辑,而且在核心逻辑不变的前提下对数据验证、事件的『整流』、筛选等进行了调整。

我们创建一个名为mobile.validator.ts的文件,它的内容如下:

2.2 设置 CounterComponent 组件初始值 – 使用 [ngModel] 语法

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

@Component({
  selector: 'exe-app',
  template: `
    <form #form="ngForm">
      <exe-counter name="counter" [ngModel]="outerCounterValue"></exe-counter>
    </form>
    <pre>{{ form.value | json }}</pre>
  `,
})
export class AppComponent { 
  outerCounterValue: number = 5;  
}

总结和思考

针对复杂的表单,我们通常应该使用『复杂问题简单化』的方法将一个复杂问题拆分成多个简单问题。对于较复杂的表单来讲,自定义表单控件是一个很有用的可以简单化表单逻辑,封装局部逻辑的一种方法。

而使用 Rx 进行逻辑的组装、转换、拼接以及合并是非常容易的事情,而且 Rx
的事件流特点会让你把逻辑梳理的非常清晰,以时间维度把业务逻辑的先后和组装的次序考虑周全。

import { FormControl } from '@angular/forms';
export function validateMobile(c: FormControl) {
let MOBILE_REGEXP = /^1[0-9]{10,10}$/;
return MOBILE_REGEXP.test(c.value) ? null : {
validateMobile: {valid: false}
}
}

2.3 设置数据双向绑定 – 使用 [(ngModel)] 语法

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

@Component({
  selector: 'exe-app',
  template: `
    <form #form="ngForm">
      <p>outerCounterValue value: {{outerCounterValue}}</p>
      <exe-counter name="counter" [(ngModel)]="outerCounterValue"></exe-counter>
    </form>
    <pre>{{ form.value | json }}</pre>
  `,
})
export class AppComponent { 
  outerCounterValue: number = 5;  
}

源码

import {ChangeDetectionStrategy, Component, forwardRef, OnInit, OnDestroy, Input} from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormControl, FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import {
  subYears,
  subMonths,
  subDays,
  isBefore,
  differenceInDays,
  differenceInMonths,
  differenceInYears,
  parse
} from 'date-fns';
import {Observable} from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { toDate, isValidDate } from '../../utils/date.util';
export enum AgeUnit {
  Year = 0,
  Month,
  Day
}

export interface Age {
  age: number;
  unit: AgeUnit;
}

@Component({
  selector: 'app-age-input',
  template: `
    <div [formGroup]="form" class="age-input">
      <div>
        <md-input-container>
          <input mdInput [mdDatepicker]="birthPicker" type="text" placeholder="出生日期" formControlName="birthday" >
          <button mdSuffix [mdDatepickerToggle]="birthPicker" type="button"></button>
          <md-error>日期不正确</md-error>
        </md-input-container>
        <md-datepicker touchUi="true" #birthPicker></md-datepicker>
      </div>
      <ng-container formGroupName="age">
        <div class="age-num">
          <md-input-container>
            <input mdInput type="number" placeholder="年龄" formControlName="ageNum">
          </md-input-container>
        </div>
        <div>
          <md-button-toggle-group formControlName="ageUnit" [(ngModel)]="selectedUnit">
            <md-button-toggle *ngFor="let unit of ageUnits" [value]="unit.value">
              {{ unit.label }}
            </md-button-toggle>
          </md-button-toggle-group>
        </div>
        <md-error class="mat-body-2" *ngIf="form.get('age').hasError('ageInvalid')">年龄或单位不正确</md-error>
      </ng-container>
    </div>
    `,
  styles: [`
    .age-num{
      width: 50px;
    }
    .age-input{
      display: flex;
      flex-wrap: nowrap;
      flex-direction: row;
      align-items: baseline;
    }
  `],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => AgeInputComponent),
      multi: true,
    },
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => AgeInputComponent),
      multi: true,
    }
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AgeInputComponent implements ControlValueAccessor, OnInit, OnDestroy {

  selectedUnit = AgeUnit.Year;
  form: FormGroup;
  ageUnits = [
    {value: AgeUnit.Year, label: '岁'},
    {value: AgeUnit.Month, label: '月'},
    {value: AgeUnit.Day, label: '天'}
  ];
  dateOfBirth;
  @Input() daysTop = 90;
  @Input() daysBottom = 0;
  @Input() monthsTop = 24;
  @Input() monthsBottom = 1;
  @Input() yearsBottom = 1;
  @Input() yearsTop = 150;
  @Input() debounceTime = 300;
  private subBirth: Subscription;
  private propagateChange = (_: any) => {};

  constructor(private fb: FormBuilder) { }

  ngOnInit() {
    const initDate = this.dateOfBirth ? this.dateOfBirth : toDate(subYears(Date.now(), 30));
    const initAge = this.toAge(initDate);
    this.form = this.fb.group({
      birthday: [initDate, this.validateDate],
      age:  this.fb.group({
        ageNum: [initAge.age],
        ageUnit: [initAge.unit]
      }, {validator: this.validateAge('ageNum', 'ageUnit')})
    });
    const birthday = this.form.get('birthday');
    const ageNum = this.form.get('age').get('ageNum');
    const ageUnit = this.form.get('age').get('ageUnit');

    const birthday$ = birthday.valueChanges
      .map(d => ({date: d, from: 'birthday'}))
      .debounceTime(this.debounceTime)
      .distinctUntilChanged()
      .filter(date => birthday.valid);
    const ageNum$ = ageNum.valueChanges
      .startWith(ageNum.value)
      .debounceTime(this.debounceTime)
      .distinctUntilChanged();
    const ageUnit$ = ageUnit.valueChanges
      .startWith(ageUnit.value)
      .debounceTime(this.debounceTime)
      .distinctUntilChanged();
    const age$ = Observable
      .combineLatest(ageNum$, ageUnit$, (_num, _unit) => this.toDate({age: _num, unit: _unit}))
      .map(d => ({date: d, from: 'age'}))
      .filter(_ => this.form.get('age').valid);
    const merged$ = Observable
      .merge(birthday$, age$)
      .filter(_ => this.form.valid)
      .debug('[Age-Input][Merged]:');
    this.subBirth = merged$.subscribe(date => {
      const age = this.toAge(date.date);
      if(date.from === 'birthday') {
        if(age.age === ageNum.value && age.unit === ageUnit.value) {
          return;
        }
        ageUnit.patchValue(age.unit, {emitEvent: false, emitModelToViewChange: true, emitViewToModelChange: true});
        ageNum.patchValue(age.age, {emitEvent: false});
        this.selectedUnit = age.unit;
        this.propagateChange(date.date);

      } else {
        const ageToCompare = this.toAge(this.form.get('birthday').value);
        if(age.age !== ageToCompare.age || age.unit !== ageToCompare.unit) {
          this.form.get('birthday').patchValue(date.date, {emitEvent: false});
          this.propagateChange(date.date);
        }
      }
    });
  }

  ngOnDestroy() {
    if(this.subBirth) {
      this.subBirth.unsubscribe();
    }
  }

  public writeValue(obj: Date) {
    if (obj) {
      const date = toDate(obj);
      this.form.get('birthday').patchValue(date, {emitEvent: false});
    }
  }

  public registerOnChange(fn: any) {
    this.propagateChange = fn;
  }

  public registerOnTouched() {
  }

  validate(c: FormControl): {[key: string]: any} {
    const val = c.value;
    if (!val) {
      return null;
    }
    if (isValidDate(val)) {
      return null;
    }
    return {
      ageInvalid: true
    };
  }

  validateDate(c: FormControl): {[key: string]: any} {
    const val = c.value;
    return isValidDate(val) ? null : {
      birthdayInvalid: true
    }
  }

  validateAge(ageNumKey: string, ageUnitKey:string): {[key: string]: any} {
    return (group: FormGroup): {[key: string]: any} => {
      const ageNum = group.controls[ageNumKey];
      const ageUnit = group.controls[ageUnitKey];
      let result = false;
      const ageNumVal = ageNum.value;

      switch (ageUnit.value) {
        case AgeUnit.Year: {
          result = ageNumVal >= this.yearsBottom && ageNumVal <= this.yearsTop
          break;
        }
        case AgeUnit.Month: {
          result = ageNumVal >= this.monthsBottom && ageNumVal <= this.monthsTop
          break;
        }
        case AgeUnit.Day: {
          result = ageNumVal >= this.daysBottom && ageNumVal <= this.daysTop
          break;
        }
        default:
          result = false;
      }
      return result ? null : {
        ageInvalid: true
      }
    }
  }

  private toAge(dateStr: string): Age {
    const date = parse(dateStr);
    const now = new Date();
    if (isBefore(subDays(now, this.daysTop), date)) {
      return {
        age: differenceInDays(now, date),
        unit: AgeUnit.Day
      };
    } else if (isBefore(subMonths(now, this.monthsTop), date)) {
      return {
        age: differenceInMonths(now, date),
        unit: AgeUnit.Month
      };
    } else {
      return {
        age: differenceInYears(now, date),
        unit: AgeUnit.Year
      };
    }
  }

  private toDate(age: Age): string {
    const now = new Date();
    switch (age.unit) {
      case AgeUnit.Year: {
        return toDate(subYears(now, age.age));
      }
      case AgeUnit.Month: {
        return toDate(subMonths(now, age.age));
      }
      case AgeUnit.Day: {
        return toDate(subDays(now, age.age));
      }
      default: {
        return this.dateOfBirth;
      }
    }
  }
}

慕课网 Angular 视频课上线:
http://coding.imooc.com/class/123.html?mc\_marking=1fdb7649e8a8143e8b81e221f9621c4a&mc\_channel=banner

在这个验证方法里,参数c的类型为FormControl,也就是表单控件,他有一个value属性,存放当前的值。我们使用正则表达式,来判断这个值是否合法。如果不合法,就返回一个对象。
在之前的教程中,我们对验证器的验证结果是这样获得的:

Using it inside reactive forms

了解 Angular 4.x Reactive (Model-Driven) Forms 详细信息,请参考 –
Angular 4.x Reactive
Forms。接下来我们来看一下具体如何使用:

<p *ngIf="userForm.controls.mobile?.errors?.required">必须输入电话</p>

1.导入 ReactiveFormsModule

app.module.ts

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

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

userForm.controls.mobile就是表单中手机号这个控件,required是required验证器对应的key,当required验证器验证失败时,就会在errors里面添加一个值:

2.更新 AppComponent

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

@Component({
  selector: 'exe-app',
  template: `
    <form [formGroup]="form">
      <exe-counter formControlName="counter"></exe-counter>
    </form>
    <pre>{{ form.value | json }}</pre>
  `,
})
export class AppComponent {
  form: FormGroup;

  constructor(private fb: FormBuilder) { }

  ngOnInit() {
    this.form = this.fb.group({
      counter: 5 // 设置初始值
    });
  }
}

友情提示:上面代码中我们移除了 Template-Driven 表单中的 ngModel 和
name 属性,取而代之是使用 formControlName 属性。此外我们通过
FormBuilder 对象提供的 group() 方法,创建 FromGroup
对象,然后在模板中通过 [formGroup]="form" 的方式实现模型与 DOM
元素的绑定。关于 Reactive Forms 的详细信息,请参考 Angular 4.x
Reactive
Forms

最后我们在来看一下,如何为我们的自定义控件,添加验证规则。

{
required: {valid: false}
}

Adding custom validation

在 Angular 4.x
基于AbstractControl自定义表单验证
这篇文章中,我们介绍了如何自定义表单验证。而对于我们自定义控件来说,添加自定义验证功能
(限制控件值的有效范围:0 <= value <=10),也很方便。具体示例如下:

所以,我们实现的自定义的验证器,也要把验证结果用验证器的名字作为key,放到errors里面,就是这样:

1.自定义 VALIDATOR

{
validateMobile: {valid: false}
}

1.1 定义验证函数

export const validateCounterRange: ValidatorFn = (control: AbstractControl): 
  ValidationErrors => {
    return (control.value > 10 || control.value < 0) ?
        { 'rangeError': { current: control.value, max: 10, min: 0 } } : null;
};

这样,我们就能够在页面中用跟之前同样的方式来获得这个验证器的验证结果。

1.2 注册自定义验证器

export const EXE_COUNTER_VALIDATOR = {
    provide: NG_VALIDATORS,
    useValue: validateCounterRange,
    multi: true
};

在模型驱动的表单里添加验证器

2.更新 AppComponent

接下来我们更新一下 AppComponent 组件,在组件模板中显示异常信息:

@Component({
  selector: 'exe-app',
  template: `
    <form [formGroup]="form">
      <exe-counter formControlName="counter"></exe-counter>
    </form>
    <p *ngIf="!form.valid">Counter is invalid!</p>
    <pre>{{ form.get('counter').errors | json }}</pre>
  `,
})

CounterComponent 组件的完整代码如下:

counter.component.ts

import { Component, Input, forwardRef } from '@angular/core';
import {
    ControlValueAccessor, NG_VALUE_ACCESSOR, NG_VALIDATORS,
    AbstractControl, ValidatorFn, ValidationErrors, FormControl
} from '@angular/forms';

export const EXE_COUNTER_VALUE_ACCESSOR: any = {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => CounterComponent),
    multi: true
};

export const validateCounterRange: ValidatorFn = (control: AbstractControl): 
  ValidationErrors => {
    return (control.value > 10 || control.value < 0) ?
        { 'rangeError': { current: control.value, max: 10, min: 0 } } : null;
};

export const EXE_COUNTER_VALIDATOR = {
    provide: NG_VALIDATORS,
    useValue: validateCounterRange,
    multi: true
};

@Component({
    selector: 'exe-counter',
    template: `
    <div>
      <p>当前值: {{ count }}</p>
      <button (click)="increment()"> + </button>
      <button (click)="decrement()"> - </button>
    </div>
    `,
    providers: [EXE_COUNTER_VALUE_ACCESSOR, EXE_COUNTER_VALIDATOR]
})
export class CounterComponent implements ControlValueAccessor {
    @Input() _count: number = 0;

    get count() {
        return this._count;
    }

    set count(value: number) {
        this._count = value;
        this.propagateChange(this._count);
    }

    propagateChange = (_: any) => { };

    writeValue(value: any) {
        if (value) {
            this.count = value;
        }
    }

    registerOnChange(fn: any) {
        this.propagateChange = fn;
    }

    registerOnTouched(fn: any) { }

    increment() {
        this.count++;
    }

    decrement() {
        this.count--;
    }
}

除了在 CounterComponent 组件的 Metadata
配置自定义验证器之外,我们也可以在创建 FormGroup 对象时,设置每个控件
(FormControl) 对象的验证规则。需调整的代码如下:

counter.component.ts

@Component({
    selector: 'exe-counter',
    ...,
    providers: [EXE_COUNTER_VALUE_ACCESSOR] // 移除自定义EXE_COUNTER_VALIDATOR
})

app.component.ts

import { validateCounterRange } from './couter.component';
...

export class AppComponent {
  ...
  ngOnInit() {
    this.form = this.fb.group({
      counter: [5, validateCounterRange] // 设置validateCounterRange验证器
    });
  }
}

自定义验证功能我们已经实现了,但验证规则即数据的有效范围是固定 (0 <=
value
<=10),实际上更好的方式是让用户能够灵活地配置数据的有效范围。接下来我们就来优化一下现有的功能,使得我们开发的组件更为灵活。

接下来,我们把我们实现的验证器添加到我们的表单里,先加到模型驱动的表单里:

Making the validation configurable

我们自定义 CounterComponent 组件的预期使用方式如下:

<exe-counter
  formControlName="counter"
  counterRangeMax="10"
  counterRangeMin="0">
</exe-counter>

首先我们需要更新一下 CounterComponent 组件,增量 counterRangeMax 和
counterRangeMin 输入属性:

@Component(...)
class CounterInputComponent implements ControlValueAccessor {
  ...
  @Input() counterRangeMin: number;

  @Input() counterRangeMax: number;
  ...
}

接着我们需要新增一个 createCounterRangeValidator()
工厂函数,用于根据设置的最大值 (maxValue) 和最小值 (minValue) 动态的创建
validateCounterRange() 函数。具体示例如下:

export function createCounterRangeValidator(maxValue: number, minValue: number) {
    return (control: AbstractControl): ValidationErrors => {
        return (control.value > +maxValue || control.value < +minValue) ?
          { 'rangeError': { current: control.value, max: maxValue, 
               min: minValue }} : null;
    }
}

在 Angular 4.x
自定义验证指令
文章中,我们介绍了如何自定义验证指令。要实现指令的自定义验证功能,我们需要实现
Validator 接口:

export interface Validator {
  validate(c: AbstractControl): ValidationErrors|null;
  registerOnValidatorChange?(fn: () => void): void;
}

另外我们应该在检测到 counterRangeMincounterRangeMax
输入属性时,就需要调用 createCounterRangeValidator() 方法,动态创建
validateCounterRange() 函数,然后在 validate()
方法中调用验证函数,并返回函数调用后的返回值。是不是有点绕,我们马上看一下具体代码:

import { Component, Input, OnChanges, SimpleChanges, forwardRef } from '@angular/core';
import {
    ControlValueAccessor, NG_VALUE_ACCESSOR, NG_VALIDATORS, Validator,
    AbstractControl, ValidatorFn, ValidationErrors, FormControl
} from '@angular/forms';

...

export const EXE_COUNTER_VALIDATOR = {
    provide: NG_VALIDATORS,
    useExisting: forwardRef(() => CounterComponent),
    multi: true
};

export function createCounterRangeValidator(maxValue: number, minValue: number) {
    return (control: AbstractControl): ValidationErrors => {
        return (control.value > +maxValue || control.value < +minValue) ?
            { 'rangeError': { current: control.value, max: maxValue, min: minValue } } 
              : null;
    }
}

@Component({
    selector: 'exe-counter',
    template: `
    <div>
      <p>当前值: {{ count }}</p>
      <button (click)="increment()"> + </button>
      <button (click)="decrement()"> - </button>
    </div>
    `,
    providers: [EXE_COUNTER_VALUE_ACCESSOR, EXE_COUNTER_VALIDATOR]
})
export class CounterComponent implements ControlValueAccessor, Validator,
    OnChanges {
    ...
    private _validator: ValidatorFn;
    private _onChange: () => void;

    @Input() counterRangeMin: number; // 设置数据有效范围的最大值

    @Input() counterRangeMax: number; // 设置数据有效范围的最小值

    // 监听输入属性变化,调用内部的_createValidator()方法,创建RangeValidator
    ngOnChanges(changes: SimpleChanges): void {
        if ('counterRangeMin' in changes || 'counterRangeMax' in changes) {
            this._createValidator();
        }
    }

    // 动态创建RangeValidator
    private _createValidator(): void {
        this._validator = createCounterRangeValidator(this.counterRangeMax,
           this.counterRangeMin);
    }

    // 执行控件验证
    validate(c: AbstractControl): ValidationErrors | null {
        return this.counterRangeMin == null || this.counterRangeMax == null ? 
            null : this._validator(c);
    }

  ...
}

上面的代码很长,我们来分解一下:

import { validateMobile } from '../validators/mobile.validator';
export class ReactiveFormsComponent implements OnInit {
this.userForm = this.formBuilder.group({
// ... 省略其他控件
mobile: [13800138001, [Validators.required, Validators.minLength(11), Validators.maxLength(11), validateMobile]]
});
...
}

注册 Validator

export const EXE_COUNTER_VALIDATOR = {
    provide: NG_VALIDATORS,
    useExisting: forwardRef(() => CounterComponent),
    multi: true
};

@Component({
    selector: 'exe-counter',
    ...,
    providers: [EXE_COUNTER_VALUE_ACCESSOR, EXE_COUNTER_VALIDATOR]
})

上面的代码省略了其他的部分,完整的代码,请参考github。

创建 createCounterRangeValidator() 工厂函数

export function createCounterRangeValidator(maxValue: number, minValue: number) {
    return (control: AbstractControl): ValidationErrors => {
        return (control.value > +maxValue || control.value < +minValue) ?
            { 'rangeError': { current: control.value, max: maxValue, min: minValue } } 
              : null;
    }
}

在上面的代码中,我们引入了之前实现的自定义的验证器,然后在表单控件创建代码中,对mobile控件加了一个validateMobile。

实现 OnChanges 接口,监听输入属性变化创建RangeValidator

export class CounterComponent implements ControlValueAccessor, Validator,
    OnChanges {
    ...
    @Input() counterRangeMin: number; // 设置数据有效范围的最大值
    @Input() counterRangeMax: number; // 设置数据有效范围的最小值

    // 监听输入属性变化,调用内部的_createValidator()方法,创建RangeValidator
    ngOnChanges(changes: SimpleChanges): void {
        if ('counterRangeMin' in changes || 'counterRangeMax' in changes) {
            this._createValidator();
        }
    }
  ...
}

这样,我们在页面上添加相应的验证结果信息:

调用 _createValidator() 方法创建RangeValidator

export class CounterComponent implements ControlValueAccessor, Validator,
    OnChanges {
    ...
    // 动态创建RangeValidator
    private _createValidator(): void {
        this._validator = createCounterRangeValidator(this.counterRangeMax,
           this.counterRangeMin);
    }
  ...
}
<p *ngIf="userForm.controls.mobile.errors?.validateMobile">电话号码格式不正确</p>

实现 Validator 接口,实现控件验证功能

export class CounterComponent implements ControlValueAccessor, Validator,
    OnChanges {
    ...
    // 执行控件验证
    validate(c: AbstractControl): ValidationErrors | null {
        return this.counterRangeMin == null || this.counterRangeMax == null ? 
            null : this._validator(c);
    }
   ...
}

此时我们自定义 CounterComponent
组件终于开发完成了,就差功能验证了。具体的使用示例如下:

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

@Component({
  selector: 'exe-app',
  template: `
    <form [formGroup]="form">
      <exe-counter formControlName="counter" 
        counterRangeMin="5" 
        counterRangeMax="8">
      </exe-counter>
    </form>
    <p *ngIf="!form.valid">Counter is invalid!</p>
    <pre>{{ form.get('counter').errors | json }}</pre>
  `,
})
export class AppComponent {
  form: FormGroup;

  constructor(private fb: FormBuilder) { }

  ngOnInit() {
    this.form = this.fb.group({
      counter: 5
    });
  }
}

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

威尼斯网址开户网站 4

angular-custom-form-control

这样就完成了验证器,以及在页面显示验证结果,就这么简单。

参考资源

  • thoughtram.io – CUSTOM FORM CONTROLS IN
    ANGULAR

在模板驱动的表单里添加验证器

但是,如果我们的表单不是在组件里用模型驱动的方式创建的,而是在页面上用html元素创建的,那么使用自定义的验证器就稍微麻烦一点。

在一个模板驱动的表单里,我们是这样使用验证器的:

<input type="text" name="mobile" [(ngModel)]="user.mobile" #mobile="ngModel" required minlength="11" maxlength="11">
有效
<div [hidden]="mobile.valid || mobile.pristine">
<p *ngIf="mobile.errors?.minlength || mobile.errors?.maxlength">电话长度必须为11</p>
<p *ngIf="mobile.errors?.required">必须输入姓名</p>
</div>

也就是在input输入元素的属性中添加验证器。那么,我们要实现自己的验证器在表单里面使用,除了上面的验证器方法里面,还需要2件事情:

我们需要将这个验证器定义成一个指令Directive,这样Angular在解析这段html的时候,会识别我们自定义的验证器指令。
我们还需要Angular的验证器调用我们的验证方法。
所以,在之前的mobile.validator.ts文件里,添加下面的指令定义:

@Directive({
selector: '[validateMobile][ngModel]'
})
export class MobileValidator {}

这段代码很简单,就是用@Directive标签定义了一个指令MobileValidator,它作用的元素是同时具有validateMobile和ngModel属性的元素。这样,我们就可以在手机号的元素上添加一个属性,来使这个验证器指令起作用。
然后,我们还需要Angular的验证器框架能够调用我们的验证方法,这就需要NG_VALIDATORS。我们修改上面的验证器的指令定义如下:

@Directive({
selector: '[validateMobile][ngModel]',
providers: [
{ provide: NG_VALIDATORS, useValue: validateMobile, multi: true }
]
})
export class MobileValidator {}

这样Angular的验证器就能够将validateMobile方法应用在这个指令上。

最后,我们再把这个新的指令,添加到AppModule的declarations里面,就可以在页面上使用这个验证器了。

最后,页面上使用验证器的代码如下:

<input type="text" name="mobile" [(ngModel)]="user.mobile" #mobile="ngModel" required minlength="11" maxlength="11" validateMobile>
有效
<div [hidden]="mobile.valid || mobile.pristine">
<p *ngIf="mobile.errors?.minlength || mobile.errors?.maxlength">电话长度必须为11</p>
<p *ngIf="mobile.errors?.required">必须输入姓名</p>
<p *ngIf="mobile.errors?.validateMobile">电话号码格式不正确</p>
</div>

以上所述是小编给大家介绍的Angular2表单自定义验证器,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对脚本之家网站的支持!

您可能感兴趣的文章:

  • AngularJS实现表单验证
  • 详解Angular开发中的登陆与身份验证
  • AngularJS使用ngMessages进行表单验证
  • AngularJS实现表单手动验证和表单自动验证
  • angular实现表单验证及提交功能
  • AngularJs验证重复密码的方法(两种)
  • Ionic+AngularJS实现登录和注册带验证功能
  • AngularJS身份验证的方法
  • AngularJS手动表单验证
  • Angular
    输入框实现自定义验证功能
  • Angular简单验证功能示例

相关文章