更新: 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, 能夠看下面這個連接
關鍵就是 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
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 的監聽與廣播
兩種監聽方式
// 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() parent: ControlContainer,
配合 @Input formControlName 就能夠獲取到 formControl
最後提一點, 當 control invalid 時, control.value 並不會被更新爲 null. 我記得 angular1 會自動更新成 null. 這在 angular2 是不一樣的。