angular2 學習筆記 ( Form 表單 )


更新: 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 自然也是會跑咯

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
});
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM