angular2 学习笔记 ( Form 表单 )

更新: 2021-01-14 

之前有写过 formControl 的监听与广播, 很久一下了. 而且之前写的有点乱, 最近有用到

记入多一次

我们可以直接看源码 input 是怎样监听的 

accessor 有 write value 和 onChange

write value 表示 model to view 

onChange 则是 view to model 

write value 什么时候会被调用呢 ? 

 原来是监听了 form control 的 registerOnChange, 只要 onChange 被调用就触发

那什么时候 form control onChange 会被调用呢 ? 

 当 setValue 的时候, 只要 emitModelToViewChange 没有设置成 false 就会触发, 也就是说 true or undeifned (没有设置) 都是会触发的 

所以记得,触发很明确表明不要 model to view, 不然它就是 model to view 然后 onchange 调用 然后 accessor 的 write value 被调用.

好,清楚了 model to view, 现在看 view to model 

当 accessor 调用 onChange 的时候发生了什么呢 ?

 调用了 updateControl 

 然后里面调用了 control.setValue, 明确表明了 model to view 是 false, 也就是上面我们说的不会调用 write value 了. 这样就不会有循环的感觉了咯.

所以记得,如果我们做 accessor 又没有实现接口的话,也是可以直接调用 setValue 和 registerOnChange 来达到一样的效果的.

model to view : control.registerOnChange 然后实现 write value 的逻辑

view to model : 调用 control.setvalue 明确表示 emitModelToViewChange : false 就可以了

更新: 2020-12-16

form dirty 只有在 accessor change 才算哦, programmatically 比如 set value 是不会导致 form control dirty 的. 

array push 和 remove 也不会 dirty 因为是 ControlValueAccessor 让它触发的,而 array 并不实现 ControlValueAccessor 

所以我们要自己调用 markAsDirty 才行哦

https://github.com/angular/angular/issues/16370

更新: 2020-09-28

form 在 async validation 的时候所有的 errors 会变成 null, 然后 state pending.

更新: 2020-09-20

当 form.value 遇上 disabled 

看 kara 女神的回复.

https://github.com/angular/angular/issues/15982

当 form group/array enable 的时候, 它只会把 enable 的内容显示出来 (html native form 的规则来的)

当它 disabled 的时候则把所有内容显示出来

但是有一个奇葩现象就是,当一个 form group 所有 control 都是 disabled 的时候, form group 会自动变成 disabled 

所以它的行为是显示所有 disabled 的 control 而不是 hide all. 它的这个自动 disabled form group 就容易引发不必要的 bug 了

更新: 2020-08-29

做动态表单的时候,我们通常需要 wrap material 组件

比如 wrap 整个 form filed

wrap 这个概念在 ng 是很危险的,因为很多时候 on push detech change 会坏掉.

许多 material 都是监听 docheck 在干活的, 非常诡异,但是真的就是这样

以往的经验是 wrap 了之后 material error 就不会出现了. 那个就是要靠 docheck 来 digest 的

所以 form filed 之上的组件就不要使用 onpush 比较好. 

另外提一个也是 wrap 遇到的问题. textarea auto resize.

当 parent 是 display none 的时候, auto size 会算不对, 在 display block 之后, 需要 docheck 才会 re-auto resize

所以 wrap textarea 也是很危险的.

更新: 2020-06-09

setValue('test') 是不会导致 formControl dirty 的哦

dirty 属于 ui 操作. 一般上如果我们的组件实现了 controlAccessor 在 view to model publish value 的时候

ng 是会帮我们 set dirty 的

但是如果我们是通过调用 formControl.setValue 来模拟 ui 操作更新 value 的话, 那要记得自己调用一下 ctrl.markAsDirty 咯

比如用 datepicker monthYear mode 的时候, 官网的例子就没有写 mark dirty, 然后我的表就不 submit 了... 

更新 : 2020-04-06

valueChanges 和 statusChanges 触发多次 
很多事情会导致上面这 2 个事件触发, 比如 updateValueAndValidity()
哪怕你只是要 update validity 没有更新 value
所以, 如果只是想关心 "value 真的 change 了" 那么需要 rxjs 过滤一下
this.formGroup.get('title')!.statusChanges
  .pipe(
    startWith(this.formGroup.get('title')!.status),
    distinctUntilChanged(),
    skip(1)
  )
  .subscribe(v => {
    console.log('status change', v);
  });

加 distinct 就可以了, 如果你不希望第一次触发, 那么就 skip 1

 

更新: 2020-01-26

formControl.valueChanges + startWith or defer 

formControl 是一个 Subject 而不是 BehaviorSubject 所以当我们 subscrube 它以后, 即便它有初始值, 也不会马上触发.

const formControl = new FormControl('dada');
setTimeout(() => {
  formControl.valueChanges.subscribe(v => console.log(v)); // 不触发
}, 1000);

碰到这样的情况一般上我们会用 startWith 来解决. 

const formControl = new FormControl('dada');
const o = formControl.valueChanges.pipe(startWith(formControl.value));
setTimeout(() => {
  o.subscribe(v => console.log(v)); // dada
}, 1000);

但是如果间中, 值改变的话,这招就不灵了

const formControl = new FormControl('dada');
const o = formControl.valueChanges.pipe(startWith(formControl.value));
formControl.setValue('tata');
setTimeout(() => {
  o.subscribe(v => console.log(v)); // dada
}, 1000);

依然是 dada, 而不是 tata. 这是因为 startwith 的值是一开始执行时就存进去了的. 

一个解决方式是使用 defer 

const formControl = new FormControl('dada');
// const o = formControl.valueChanges.pipe(startWith(formControl.value));
const o = defer(() => {
  return formControl.valueChanges.pipe(startWith(formControl.value));
});
formControl.setValue('tata');
setTimeout(() => {
  o.subscribe(v => console.log(v)); // tata
}, 1000);

更新: 2020-01-01 

number 触发 valuechange 2 次 + validation 的 坑

https://github.com/angular/angular/issues/12540

input type = number 在 blur 的时候会触发一次 value changes 这个 bug 已经很久了. 

本来嘛也不在乎. 直到今天做 dynamic validation, 手动监听 valuechange 然后去 set error 

发现在 blur 以后 errors 就被 reset 了. 后来才明白。

value change 触发, ng 就会去 clear all error 然后重新去跑 validation. 

而我自己写的 validation 却没有这个概念就被 ng 给 clear 了. 

当然还有一个原因是我写了 distinct until change, 而 number fire 得时候 value 是一样得, 所以我得 validation 没跑.

以防万一最好不要写 distinct 每次都跑呗。 

更新: 2019-12-12

async validation 什么时候 call 

当我们创建 formGroup 的时候, validation 会跑一次. 

当我们 binding [formGroup] 的时候, validation 也会跑一次. (这个有点意想不到 /. )

还有一般的 set, update, reset value 自然也是会跑咯, form reset 也算是 reset value 哦, 全部会跑

refer : https://github.com/angular/angular/issues/21500

kara 说最少 2 次, 我觉得也合理啦,只是感觉挺浪费的. 通常 init value 都是 valid 的丫

更新: 2019-08-17 

上次提到说通过 custom value accessor 来实现 uppercase 不过遇到 material autocomplete 就撞墙了. 

因为 material 也用了 custom value accssor ... 

那就不要那么麻烦了,绑定一个 blur 事件,在用于 blur 的时候才变 uppercase 呗. 

另外说一下 number validation, 

ng 的 input type=number 是无法验证的 

因为原生的 input type=number 在 invalid 的时候 value 是 empty string 

而 ng 遇到 empty string 会直接返回 null

所以如果我们想让用户知道 number invalid 我们得修改 value accessor 

registerOnChange(fn: (_: number | null) => void): void {
  this.onChange = (e: Event) => {
    const input = e.target as HTMLInputElement;
    let value: number | null;
    if (input.validity.badInput) {
      value = NaN;
    } else if (input.value === '') {
      value = null;
    } else {
      value = parseFloat(input.value);
    }
    console.log(value);
    fn(value);
  };
}

让它返回 nan 才可以。

更新: 2019-07-31

今天想做一个 input uppercase,就是用户输入 lowercase 自动变成 uppercase 的 input 

ng1 的做法是用 formatter 和 parser 来实现,但是 ng2 没有这个东西了。

https://github.com/angular/angular/issues/3009

2015 年就提了这个 feature issue,但是直到今天有没有好的 idea 实现。

formatter parser 对 validation 不利,而且难控制执行顺序. 

目前我们唯一能做的就是自己实现一个 value accessor 来处理了.

原生的 input, Angular 替我们实现了 DefaultValueAccessor,所以 input 天生就可以配合 FormControl 使用.

@Directive({
  selector:
      'input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]',
  // TODO: vsavkin replace the above selector with the one below it once
  // https://github.com/angular/angular/issues/3011 is implemented
  // selector: '[ngModel],[formControl],[formControlName]',
  host: {
    '(input)': '$any(this)._handleInput($event.target.value)',
    '(blur)': 'onTouched()',
    '(compositionstart)': '$any(this)._compositionStart()',
    '(compositionend)': '$any(this)._compositionEnd($event.target.value)'
  },
  providers: [DEFAULT_VALUE_ACCESSOR]
})
export class DefaultValueAccessor implements ControlValueAccessor {

源码里可以看到,Angular 监听的是 oninput 还有 compositionstart 这个是为了处理中文输入

上网找了一些方法,有人说监听 keydown or keypress 然后 set event.target.value 

有些人说监听 input. 

但其实这 2 个都是不对的. 

我们知道用户输入时, 事件触发的顺序时 keydown -> keypress -> input -> keyup. 如果用户长按不放,keydown, keypress, input 会一直触发, keyup 则只会触发一次. 

而 keydown, keypress 触发时,我们只能获取到 event.key 和 event.code 当时的 event.target.value 依然时旧的值. 

我们监听 oninput 也不行,因为 Angular 的指令会更快的执行. 

所以唯一我们能做的是写一个 CustomValueAccessor 去替换掉 DefaultValueAccessor 

每一个组件只能允许一个 value accessor. 选择的顺序是 custom 优先, builtin 第二, default 第三

下面这些是 build in 的, default 指的是 input text 和 textarea 

const BUILTIN_ACCESSORS = [
  CheckboxControlValueAccessor,
  RangeValueAccessor,
  NumberValueAccessor,
  SelectControlValueAccessor,
  SelectMultipleControlValueAccessor,
  RadioControlValueAccessor,
];

所以只要我们写一个 input directive 实现 custom value accessor 那么我们的就会优先选中执行. 而 default value accessor 就不会被执行了.

这样我们就实现我们要的东西了。

更新 : 2019-07-17

async validator 

终于有需求要用到了. 

async 的问题主要就是发的频率, 用户一直输入, 一直发请求去验证伤服务器性能. 

有 2 种做法, 一是用 updateOn: blur,这个很牛, 一行代码搞定, 但是未必满足体验. 

另一种是用返回 obserable + delay, 可以看下面这个链接 

https://stackoverflow.com/questions/36919011/how-to-add-debounce-time-to-an-async-validator-in-angular-2

关键就是 ng 监听到 input 事件后会调用我们提供的方法, 然后 ng 去 subscribe 它等待结果. 

而第二个 input 进来的时候 ng 会 unsubscribe 刚才那一次, 所以我们把请求写在 delay 之后, 一旦被 unsubscribe 后就不会发出去了. 

没有看过源码, 但是推测 ng 内部是用 switchmap 来操作的, 所以是这样的结果. 很方便哦.

另外, 只有当普通的 validation 都 pass 的情况下, ng 才会检查 async 的 validation 哦.

当 async validator 遇到 OnPush, view 的更新会失效. 

https://github.com/angular/angular/issues/12378

解决方法是加一个 tap markForCheck

asyncValidators: (formControl: FormControl): Observable<ValidationErrors | null> => {
  return of([]).pipe(delay(400), switchMap(() => {
    return from(this.customerService.checkCustomerNameUniqueAsync({ name: formControl.value })).pipe(map((isDuplicate) => {
        return isDuplicate ? { 'unique': true } : null;
    }), tap(() => this.cdr.markForCheck()));
  }));

或者使用 (statusChanges | async) === 'PENDING'

更新 : 2019-07-16 

动态验证简单例子 

ngOnInit() {
  this.formGroup = this.formBuilder.group({
    name: [''],
    email: ['']
  });
  this.formGroup.valueChanges.subscribe((value: { name: string, email: string }) => {
    if (value.name !== '' || value.email !== '') {
      this.formGroup.get('name').setValidators(Validators.required);
      this.formGroup.get('email').setValidators(Validators.required);

      // this.formGroup.updateValueAndValidity({ emitEvent: false }); // 调用 formGroup 是不足够的, 它并不会去检查 child control
      this.formGroup.get('name').updateValueAndValidity({ emitEvent: false }); // 这个就有效果, 但是记得要放 emitEvent false, 不然就死循环了 
      // 最后.. 这里不需要调用 ChangeDetectorRef.markForCheck() view 也会更新
    }
  });
}

更新 : 2019-05-25

disabled 的 control 不会被纳入 form.valid 和 form.value 里, 这个和 html5 行为是一致的. 

https://github.com/angular/angular/issues/11432   

更新 : 2018-02-13 

valueChanges and rxjs for filter first value 
需求是这样的 
let fc = new FormControl('');   
fc.valueChanges.subscribe(v => console.log(v));
fc.setValue(''); // '' to '' 没有必要触发
fc.setValue('a'); // '' to a 触发 

可是结果 2 个都触发了.

那这样试试看 : 

fc.valueChanges.pipe(distinctUntilChanged()).subscribe(v => console.log(v));

结果还是一样. 

问题在哪里呢 ? 

首先 ng 的 formControl 看上去想是 BehaviorSubject 因为它有 default 值, 但是行为却像是 Subject. 因为 

let fc = new FormControl('dada');   
fc.valueChanges.subscribe(v => console.log(v)); //并没有触发

虽然之前的代码问题出在, 没有初始值, 所以 distinctUntilChanged 就发挥不了作用了 

我们需要用 startWith 告诉它初始值

let fc = new FormControl('dada');
fc.valueChanges.pipe(startWith('dada'), distinctUntilChanged(), skip(1)).subscribe(v => console.log(v));
fc.setValue('dada'); // 不触发
fc.setValue('dada1'); //触发了

startWith 会马上输入一个值, 然后流入 distinct, distinct 会把值对比上一个(目前没有上一个), 然后记入这一个, 在把值流入 skip(1), 因为我们不要触发初始值, 所以使用了 skip, 如果没有 skip 这时 subscribe 会触发. (startWith 会触发 subscribe)

这样之后的值流入(不经过 startWith 了, startWith 只用一次罢了), distinc 就会和初始值对比就是我们要的结果了. 

如果要在加上一个 debounceTime, 我们必须加在最 startWith 之前. 

pipe(debounceTime(200), startWith(''), distinctUntilChanged(), skip(1))

一旦 subscribe startWith 输入值 -> distinct -> skip 

然后 setValue -> debounce -> distinc -> 触发 ( startWith 只在第一次有用, skip(1) 也是因为已经 skip 掉第一次了)

更新 : 2018-02-10

form.value 坑

let ff = new FormGroup({ 
    name : new FormControl('')
});
ff.get('name')!.valueChanges.subscribe(v => {
  console.log(v); // 'dada'
  console.log(ff.value); // { name : '' } 这时还没有更新到哦 
  console.log(ff.getRawValue())  // { name : 'dada' }
});
ff.get('name')!.setValue('dada');
console.log(ff.value); // { name : 'dada' }

更新 : 2017-10-19 

this.formControl.setValidators(null);
this.formControl.updateValueAndValidity();

reset validators 后记得调用从新验证哦,Ng 不会帮你做的.

更新 : 2017-10-18 

formControl 的监听与广播 

两种监听方式 

1. control.valueChanges.subscribe(v)  
2. control.registerOnChange((value, emitViewToModelChange)
通常我们是想监听来自 view 的更新, 当 accessor.publishMethod(v) 的时候, 上面第一种会被广播, 第二种则收不到. 所以想监听 view -> model 使用第一种 
那么如果我们要监听来自 control.setValue 的话, model -> view or just model change, 我们使用第 2 种, 
setvalue 允许我们广播时声明要不要 让第一种和第二种触发
emitEvent = false 第一种不触发
emitModelToViewChange = false 第 2 种不触发 
emitViewToModelChange = false 第 2 种触发, 然后第二个参数是 就是 emitViewToModelChange 
对了,虽然两种叫 changes 但是值一样也是会触发的,如果不想重复值触发的话,自己写过滤呗.
总结: 
在做 view accessor 时, 我们监听 formControl model to view 所以使用 registerOnChange
// view accessor 
this.viewValue = this.formControl.value; // first time
this.formControl.registerOnChange((v, isViewToModel) => { // model to view
  console.log('should be false', isViewToModel);
  this.viewValue = v;
});

然后通过 formControl view to model 更新

viewToModel(value: any) {
  this.formControl.setValue(value, {
    emitEvent: true,
    emitModelToViewChange: false,
    emitViewToModelChange: true
  });
}

然后呢在外部,我们使用 valueChanges 监听 view to model 的变化

this.formControl.valueChanges.subscribe(v => console.log('view to model', v)); // view to model

再然后呢, 使用 setValue model to view 

modelToView(value: any) {
  this.formControl.setValue(value, {
    emitEvent: false,
    emitModelToViewChange: true,
    emitViewToModelChange: false
  });
}

 最关键的是在做 view accessor 时, 不要依赖 valueChanges 应该只使用 registerOnChange, 这好比你实现 angular ControlvalueAccessor 的时候,我们只依赖 writeValue 去修改 view.

对于 model to view 的时候是否允许 emitEvent 完全可以看你自己有没有对其依赖,但 view accessor 肯定是不依赖的,所以即使 emitEvent false, model to view 依然把 view 处理的很好才对。

更新 : 2017-08-06 

formControlName and [formControl] 的注入

 <form [formGroup]="form">
  <div formGroupName="obj">
    <input formControlName="name" type="text">
    <input sURLTitle="name" formControlName="URLTitle" type="text">
  </div>
</form> 

<form [formGroup]="form">
  <div [formGroup]="form.get('obj')">
    <input [formControl]="form.get('obj.name')" type="text">
    <input [sURLTitle]="form.get('obj.name')" [formControl]="form.get('obj.URLTitle')" type="text">
  </div>
</form>

这 2 种写法出来的结果是一样的. 

如果我们的指令是 sURLTitle

那么在 sURLTitle 可以通过注入获取到 formControl & formGroup

@Directive({
  selector: '[sURLTitle]'
})
export class URLTitleDirective implements OnInit, OnDestroy {

  constructor(
    // 注意 : 不雅直接注入 FormGroupDirective | FormGroupName, 注入 ControlContainer 才对.
    // @Optional() private formGroupDirective: FormGroupDirective,
    // @Optional() private formGroupName: FormGroupName,
    private closestControl: ControlContainer, // 通过抽象的 ControlContainer 可以获取到上一层 formGroup
    @Optional() private formControlDirective: FormControlDirective,
    @Optional() private FormControlName: FormControlName,
  ) { }

  @Input('sURLTitle')
  URLTitle: string | FormControl

  private sub: ISubscription
  ngOnInit() {
    let watchControl = (typeof (this.URLTitle) === 'string') ? this.closestControl.control.get(this.URLTitle) as FormControl : this.URLTitle;
    let sub = watchControl.valueChanges.subscribe(v => {
      (this.formControlDirective || this.FormControlName).control.setValue(s.toURLTitle(v));
    });
  }

  ngOnDestroy() {
    this.sub.unsubscribe();
  }
}

更新 : 2017-04-21 

form 是不能嵌套的, 但是 formGroup / formGroupDirective 可以 

submit button 只对 <form> 有效果. 如果是 <div [formGroup] > 的话需要自己写 trigger 

child form submit 并不会让 parent form submit, 分的很开. 

更新 : 2017-03-22

小提示 : 

我们最好把表单里面的 button 都写上 type.

因为 ng 会依据 type 来做处理. 比如 reset 

要注意的是, 如果你不写 type, 默认是 type="submit".

<form [formGroup]="form" #formComponent >
    <button type="button" ></button>
    <button type="submit" ></button>
    <button type="reset" ></button>
</form>

另外, ng 把 formGroup 指令和 formGroup 对象区分的很明显,我们可不要搞混哦. 

上面 formComponent 是有 submitted 属性的, form 则没有 

formComponent.reset() 会把 submitted set to false, form.reset() 则不会. 

formComponent.reset() 会间接调用 form.reset(), 所以数据会清空.

<button type="reset"> 虽然方便不过不支持 window.confirm

我们要自己实现 reset 的话,就必须使用 @Viewchild 来注入 formGroup 指令. 

2016-08-30

refer : 

ng2 的表单和 ng1 类似, 也是用 control 概念来做操作, 当然也有一些地方不同 

最大的特点是它把表单区分为 template drive and model drive 

template drive 和 ng1 很像, 就是通过指令来创建表单的 control 来加以操作. 

model drive 则是直接在 component 内生成 control 然后再绑定到模板上去. 

template drive 的好处是写得少,简单, 适合用于简单的表单

简单的定义是 :

-没有使用 FormArray,

-没有 async valid,

-没有 dynamic | condition validation  

-总之就是要很简单很静态就对了啦.

当然如果你打算自己写各做复杂指令去让 template drive 无所不能, 也是可以办到的. 有心铁棒磨成针嘛.. 你爱磨就去啦..

model drive 的好处就是方便写测试, 不需要依赖 view. 

模板驱动 (template drive):

<form novalidate #form="ngForm" (ngSubmit)="submit(form)">
       
</form>

没能嵌套表单了哦! 

通过 #form="ngForm" 我们可以获取到 ngForm 指令, 并且操作它, 比如 form.valid, form.value 等 

ngSubmit 配合 button type=submit 使用 

<input type="text" placeholder="name"
        [(ngModel)]="person.name" name="name" #name="ngModel" required minlength="5" maxlength="10" />
<p>name ok : {{ name.valid }}</p>  

[(ngModel)] 实现双向绑定和 get set value for input 

name="name" 实现 create control to form 

#name 引用 ngModel 指令,可以获取 name.valid 等 

required, minlength, maxlength 是原生提供的验证. ng2 给的原生验证指令很少,连 email,number 都没有哦. 

<fieldset ngModelGroup="address">
    <legend>Address</legend>
    <div>
        Street: <input type="text" [(ngModel)]="person.address.country" name="country" #country="ngModel" required />
    </div> 
    <div>country ok : {{ country.valid }}</div>
</fieldset>

如果值是对象的话,请开多一个 ngModelGroup, 这有点像表单嵌套了.

<div>
    <div *ngFor="let data of person.datas; let i=index;">
        <div ngModelGroup="{{ 'datas['+ i +'].data' }}">
            <input type="text" [(ngModel)]="data.key" name="key" #key="ngModel" required />
        </div>
    </div>
</div>  

遇到数组的话,建议不适用 template drive, 改用 model drive 比较适合. 上面这个有点牵强...

在自定义组件上使用 ngModel 

ng1 是通过 require ngModelControl 来实现 

ng2 有点不同 

@Component({ 
    selector: "my-input",
    template: `
        <input type="text" [(ngModel)]="value"  />
    `,
    providers: [{
        provide: NG_VALUE_ACCESSOR,
        useExisting: forwardRef(() => MyInputComponent),
        multi: true
    }]
})

首先写一个 provide 扩展 NG_VALUE_ACCESSOR 让 ng 认识它 .

export class MyInputComponent implements OnInit, ControlValueAccessor {}

实现 ControlValueAccessor 接口 

//outside to inside     
writeValue(outsideValue: any): void {
    this._value = outsideValue;
};

//inside to outside
//注册一个方法, 当 inside value updated then need call it : fn(newValue)
registerOnChange(fn: (newValue : any) => void): void {
    this.publichValue = fn;
}

//inside to outside
registerOnTouched(fn: any): void {
    this.publichTouched = fn;
}  

主要 3 个方法 

writeValue 是当外部数据修改时被调用来更新内部的。 

registerOnChange(fn) 把这个 fn 注册到内部方法上, 当内部值更新时调用它 this.publishValue(newValue); 

registerOnTouched(fn) 也是一样注册然后调用当 touched 

使用时这样的 : 

<my-input [(ngModel)]="person.like" name="like" email #like="ngModel" ></my-input>
value : {{ like.value }}

执行的顺序是 ngOnInit-> writeValue-> registerOnChange -> registerOnTouched -> ngAfterContentInit -> ngAfterViewInit

如果内部也是使用 formControl 来维护 value 的话, 通常在写入时我们可以关掉 emitEvent, 不然又触发 onChange 去 publish value (但即使你这样做,也不会造成死循环 error 哦, ng 好厉害呢)

writeValue(value: any): void { 
    this.formControl.setValue(value,{ emitEvent : false });        
};

Model drive

自定义 validator 指令 

angular 只提供了 4 种 validation : required, minlength, maxlength, pattern

好吝啬 ! 

class MyValidators {
    static email(value: string): ValidatorFn {
        return function (c: AbstractControl) {            
            return (c.value == value) ? null : { "email": false };            
        };
    }  
}

this.registerForm = this.formBuilder.group({
    firstname: ['', MyValidators.email("abc")] 
}); 

如果验证通过返回 null, 如果失败返回一个对象 { email : false }; 

还有 async 的,不过我们有找到比较可靠的教程,以后才讲吧.

上面这个是 model drive 的,如果你希望支持 template drive 可以参考这个 : 

http://blog.thoughtram.io/angular/2016/03/14/custom-validators-in-angular-2.html

在大部分情况下, model drive 是更好的选择, 因为它把逻辑才开了, 不要依赖模板是 angular2 一个很重要的思想, 我们要尽量少依赖模板来写业务逻辑, 因为在多设备开发情况下模板是不能复用的.

而且不依赖模板也更容易测试. 

我们看看整个 form 的核心是什么 ? 

就是对一堆有结构的数据, 附上验证逻辑, 然后绑定到各个组件上去与用户互动. 

所以 model drive 的开发流程是 : 定义出有结构的数据 -> 绑定验证 -> 绑定到组件 -> 用户操作 (我们监听并且反应)

这就是有结构的数据 : 

export class AppComponent {
    constructor(private formBuilder: FormBuilder) { console.clear(); }
    registerForm: FormGroup;
    ngOnInit() {
        this.registerForm = this.formBuilder.group({
            firstname: ['', Validators.required],
            address: this.formBuilder.group({
                text : ['']
            }),
            array: this.formBuilder.array([this.formBuilder.group({
                abc : ['']
            })], Validators.required)
        }); 
    }
}

angular 提供了一个 control api, 让我们去创建数据结构, 对象, 数组, 嵌套等等. 

this.formBuilder.group 创建对象

this.formBuilder.array 创建数组 ( angular 还有添加删除数组的 api 哦 )

firlstname : [ '', Validators.required, Validators.requiredAsync ] 这是一个简单的验证绑定, 如果要动态绑定的话也是通过 control api 

control api 还有很多种对数据, 结构, 验证, 监听的操作, 等实际开发之后我才补上吧. 

template drive 其实也是用同一个方式来实现的, 只是 template drive 是通过指令去创建了这些 control, 并且隐藏了起来, 所以其实看穿了也没什么, 我们也可以自己写指令去让 template drive 实现所有的功能.

接下来是绑定到 template 上.

<form [formGroup]="registerForm">
    <input type="text" formControlName="firstname"/> 
    <fieldset formGroupName="address"> 
        <input type="text" formControlName="text"> 
    </fieldset> 
    <div formArrayName="array">
            <div *ngFor="let obj of registerForm.controls.array.controls; let i=index">
            <fieldset [formGroupName]="i"> 
                <input type="text" formControlName="abc"> 
            </fieldset> 
            </div>
    </div>              
</form> 

值得注意的是 array 的绑定, 使用了 i 

特别附上这 2 篇 : 

https://scotch.io/tutorials/how-to-build-nested-model-driven-forms-in-angular-2

https://scotch.io/tutorials/how-to-deal-with-different-form-controls-in-angular-2

async validator 

static async(): AsyncValidatorFn { 
    let timer : NodeJS.Timer;  
    return (control: AbstractControl) => { 
        return new Promise((resolve, reject) => {
            clearTimeout(timer);
            timer = setTimeout(() => {
                console.log("value is " + control.value);
                console.log("ajaxing");
                let isOk = control.value == "keatkeat";
         //假设是一个 ajax 啦 setTimeout(()
=> { if (isOk) { return resolve(null); } else { resolve({ sync: true }); } }, 5000); }, 300); }); }; } this.form = this.formBuilder.group({ name: ["abcde", MyValidators.sync(), MyValidators.async()] });

angular 会一直调用 async valid 所以最好是写一个 timer, 不然一直 ajax 很浪费.

常用的手动调用 : 

this.form.controls["name"].markAsPending(); //async valid 时会是 pending 状态, 然后 setErrors 会自动把 pending set to false 哦
this.form.controls["name"].setErrors({ required : true }); 
this.form.controls["name"].setErrors(null); // null 表示 valid 了   
this.form.controls["name"].markAsTouched();
this.form.controls['name'].updateValueAndValidity(); //trigger 验证 (比如做 confirmPassword match 的时候用到) 
this.form.controls['name'].root.get("age"); //获取 sibling 属性, 验证的时候经常用到, 还支持 path 哦 .get("address.text")
this.form.controls["confirmPassword"].valueChanges.subscribe(v => v); //监听任何变化

这些方法都会按照逻辑去修改更多相关值, 比如 setErrors(null); errors = null 同时修改 valid = true, invalid = false; 

特别说一下 AbstractControl.get('path'),

-当 path 有包含 "." 时 (例如 address.text), ng 会把 address 当成对象然后获取 address 的 text 属性. 但是如果你有一个属性名就叫 'address.text' : "value" 那么算是你自己挖坑跳哦.  

-如果要拿 array 的话是 get('products.0') 而不是 get('products[0]') 哦.

更新 : 2016-12-23 

touch and dirty 的区别 

touch 表示被"动"过 ( 比如 input unblur 就算 touch 了 )

dirty 是说值曾经被修改过 ( 改了即使你改回去同一个值也时 dirty 了哦 )

更新 : 2017-02-16

概念 : 

当我们自己写 accessor 的时候, 我们也应该 follow angular style 

比如自定义 upload file 的时候, 当 ajax upload 时, 我们应该把 control 的 status set to "PENDING" 通过 control.markAsPending()

pending 的意思是用户正在进行操作, 可能是正在 upload or 正在做验证. 总之必须通过 control 表达出去给 form 知道, form 则可能阻止 submitt 或则是其它特定处理. 

要在 accessor 里调用 formContril 我们需要注入

@Optional() @Host() @SkipSelf() parentControlContainer, 

配合 @Input formControlName 就可以获取到 formControl

最后提一点, 当 control invalid 时, control.value 并不会被更新为 null. 我记得 angular1 会自动更新成 null. 这在 angular2 是不同的。 

modelToView(value: any) {
this.formControl.setValue(value, {
emitEvent: false,
emitModelToViewChange: true,
emitViewToModelChange: false
});
}
原文地址:https://www.cnblogs.com/keatkeat/p/5821974.html