angular2 学习笔记 ( Form 表单 )

更新: 2019-08-17 css

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

由于 material 也用了 custom value accssor ... html5

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

另外说一下 number validation, github

ng 的 input type=number 是没法验证的 ajax

由于原生的 input type=number 在 invalid 的时候 value 是 empty string api

而 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 才能够。angular2

 

 

更新: 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
});
}
相关文章
相关标签/搜索