Skip to content

Latest commit

 

History

History
667 lines (588 loc) · 21.7 KB

11. Reactive Form.md

File metadata and controls

667 lines (588 loc) · 21.7 KB

Reactive Form

FormGroupFormControl

使用 Template-Driven Form 是將驗證的部分寫在 Template 中,而 Reactive Form 把驗證的地方寫在 TypeScript 中,來看看需要更改的檔案 :

|--app
    |--app.component.html // 更動
    |--app.component.ts // 更動
    |--app.module.ts // 更動
<div class="container">
  <div class="row">
    <div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
      <form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
        <div class="form-group">
          <label for="username">Username</label>
          <input
            type="text"
            id="username"
            class="form-control"
            formControlName="username">
        </div>
        <div class="form-group">
          <label for="email">email</label>
          <input
            type="text"
            id="email"
            class="form-control"
            formControlName="email">
        </div>
        <div class="radio" *ngFor="let gender of genders">
          <label>
            <input
              type="radio"
              [value]="gender"
              formControlName="gender">{{ gender }}
          </label>
        </div>
        <button class="btn btn-primary" type="submit">Submit</button>
      </form>
    </div>
  </div>
</div>
export class AppComponent implements OnInit{
  genders = ['male', 'female'];
  signupForm: FormGroup;

  ngOnInit(): void {
    this.signupForm = new FormGroup({
      username: new FormControl(),
      email: new FormControl(),
      gender: new FormControl()
    });
  }

  onSubmit() {
    console.log(this.signupForm)
  }
}
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    ReactiveFormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

要在 AppModule 中 import ReactiveFormsModule。TypeScript 的部分,class 內部宣告型別為 FormGroup 的物件並在 ngOnInit 中初始化它,FormGroup 建構子的參數是放入物件,物件的 key 會對應到 Template 的設定 ( <input> 中的 formControlName ),物件的 value 則是 FormControl 物件 ( 建構子可以寫入驗證的方式以及預設值,目前先留空及可 )。Template 的部分,在 <form> 上面綁定了 Property Binding 到 TypeScript 中的 FormGroup 物件,而內部想要驗證的 <input> 也綁定了 formControlName,這就完成了 TypeScript 與 Template 的交互綁定。按下按鈕觸發 ngSubmit(..) 後直接操作 FormGroup 物件,此時在開發人員工具應該可以看到 FormGroup 的內容,以及可以發現各個 <input> 也具有前面 Template-Driven Form 所說的三組 class ( dirtytouchedinvalid )。

增加驗證

下步驟是增加驗證功能 :

|--app
    |--app.component.html // 更動
    |--app.component.ts // 更動
    |--app.component.css // 更動
<div class="container">
  <div class="row">
    <div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
      <form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
        <div class="form-group">
          <label for="username">Username</label>
          <input
            type="text"
            id="username"
            class="form-control"
            formControlName="username">
          <span
            *ngIf="signupForm.get('username').invalid && signupForm.get('username').touched"
            class="help-block">Please enter a valid username!</span>
        </div>
        <div class="form-group">
          <label for="email">email</label>
          <input
            type="text"
            id="email"
            class="form-control"
            formControlName="email">
          <span
            *ngIf="signupForm.get('email').invalid && signupForm.get('email').touched"
            class="help-block">Please enter a valid email!</span>
        </div>
        <div class="radio" *ngFor="let gender of genders">
          <label>
            <input
              type="radio"
              [value]="gender"
              formControlName="gender">{{ gender }}
          </label>
        </div>
        <span *ngIf="signupForm.invalid && signupForm.touched"
              class="help-block">Please enter a valid data!</span>
        <button class="btn btn-primary" type="submit">Submit</button>
      </form>
    </div>
  </div>
</div>
export class AppComponent implements OnInit{
  genders = ['male', 'female'];
  signupForm: FormGroup;

  ngOnInit(): void {
    this.signupForm = new FormGroup({
      username: new FormControl(null, Validators.required),
      email: new FormControl(null, [Validators.required, Validators.email]),
      gender: new FormControl('female')
    });
  }

  onSubmit() {
    console.log(this.signupForm)
  }
}
.container {
  margin-top: 30px;
}

input.ng-invalid.ng-touched {
  border: 1px solid red;
}

TypeScript 的部分,在 FormControl 中會有兩個參數,第一個參數是設定此 <input> 帶有的預設值,第二個參數可以傳入陣列或是單一的 method,主要是放入驗證的 method,先採用預設的 Validators... 的方法。在 Template 的部分,為了出現和先前一樣的效果,不使用範本參考變數而是改成 *ngIf 去取得放在 TypeScript 中的值,來取得驗證是否成功以及使用者是否有觸及此 <input>,在 css 的部分,則是使用 Angular 設定的 class 給予驗證未通過的 <input> 加上紅色邊框。

FormGroup

|--app
    |--app.component.html // 更動
    |--app.component.ts // 更動
<div class="container">
  <div class="row">
    <div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
      <form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
        <div formGroupName="userdata">
          <div class="form-group">
            <label for="username">Username</label>
            <input
              type="text"
              id="username"
              class="form-control"
              formControlName="username">
            <span
              *ngIf="signupForm.get('userdata.username').invalid && signupForm.get('userdata.username').touched"
              class="help-block">Please enter a valid username!</span>
          </div>
          <div class="form-group">
            <label for="email">email</label>
            <input
              type="text"
              id="email"
              class="form-control"
              formControlName="email">
            <span
              *ngIf="signupForm.get('userdata.email').invalid && signupForm.get('userdata.email').touched"
              class="help-block">Please enter a valid email!</span>
          </div>
        </div>
        <div class="radio" *ngFor="let gender of genders">
          <label>
            <input
              type="radio"
              [value]="gender"
              formControlName="gender">{{ gender }}
          </label>
        </div>
        <span *ngIf="signupForm.invalid && signupForm.touched"
              class="help-block">Please enter a valid data!</span>
        <button class="btn btn-primary" type="submit">Submit</button>
      </form>
    </div>
  </div>
</div>
export class AppComponent implements OnInit{
  genders = ['male', 'female'];
  signupForm: FormGroup;

  ngOnInit(): void {
    this.signupForm = new FormGroup({
      userdata: new FormGroup({
        username: new FormControl(null, Validators.required),
        email: new FormControl(null, [Validators.required, Validators.email])
      }),
      gender: new FormControl('female')
    });
  }

  onSubmit() {
    console.log(this.signupForm)
  }
}

FormGroup 中可以包入另一個 FormGroup,並且給予這個 FormGroup 另一個名稱,而 Template 必須在額外添加 <div>,並給予它 formGroupName 來代表內部 FormGroup 的 key 值。

FormArray

另一個是 FormArray,如果有許多相同類型的 <input> 要做驗證,就會用到這個物件 :

|--app
    |--app.component.html // 更動
    |--app.component.ts // 更動
<div class="container">
  <div class="row">
    <div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
      <form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
        <div formGroupName="userdata">
          <div class="form-group">
            <label for="username">Username</label>
            <input
              type="text"
              id="username"
              class="form-control"
              formControlName="username">
            <span
              *ngIf="signupForm.get('userdata.username').invalid && signupForm.get('userdata.username').touched"
              class="help-block">Please enter a valid username!</span>
          </div>
          <div class="form-group">
            <label for="email">email</label>
            <input
              type="text"
              id="email"
              class="form-control"
              formControlName="email">
            <span
              *ngIf="signupForm.get('userdata.email').invalid && signupForm.get('userdata.email').touched"
              class="help-block">Please enter a valid email!</span>
          </div>
        </div>
        <div class="radio" *ngFor="let gender of genders">
          <label>
            <input
              type="radio"
              [value]="gender"
              formControlName="gender">{{ gender }}
          </label>
        </div>
        <div formArrayName="hobbies">
          <h4>Your hobbies</h4>
          <button type="button"
                  class="btn btn-default"
                  (click)="onAddHobby()">Add hobby</button>
          <div class="form-group"
                *ngFor="let hobbyControl of hobbies; let i = index">
            <input type="text" class="form-control" [formControlName]="i">
          </div>
        </div>
        <span *ngIf="signupForm.invalid && signupForm.touched"
              class="help-block">Please enter a valid data!</span>
        <button class="btn btn-primary" type="submit">Submit</button>
      </form>
    </div>
  </div>
</div>
export class AppComponent implements OnInit{
  genders = ['male', 'female'];
  signupForm: FormGroup;

  ngOnInit(): void {
    this.signupForm = new FormGroup({
      userdata: new FormGroup({
        username: new FormControl(null, Validators.required),
        email: new FormControl(null, [Validators.required, Validators.email])
      }),
      gender: new FormControl('female'),
      hobbies: new FormArray([])
    });
  }

  onSubmit() {
    console.log(this.signupForm)
  }

  onAddHobby() {
    const control = new FormControl(null, Validators.required);
    (this.signupForm.get('hobbies') as FormArray).push(control);
  }

  get hobbies() {
    return (this.signupForm.get('hobbies') as FormArray).controls;
  }
}

TypeScript 的部分,在最原本的 FormGroup 中新增一個 key 值並對應到 FormArray 物件,預期上會希望在前端按下按鈕後,新增一個 <input>,因此在這邊新增 onAddHobby(..) 方法,而 get hobbies(..) 則是為了讓前端 Template 的部分可以使用 *ngFor 而開出的 get(..) 方法 ( 寫成 method 是為了取出 FromArray 內部的 controls,也可以在直接寫在 Template 中,只是 Template 的內容看起來會較為複雜 )。Template 中則多出一整個 <div> 區塊,並給予 formArrayName 名稱對應到 TypeScript 的部分,最後使用 *ngFor 來把 TypeScript 中的 FormArray 給一一迭代出來。

自定義驗證

除了預設的驗證外,還可使用自定義的驗證 :

|--app
    |--app.component.html // 更動
    |--app.component.ts // 更動
<div class="container">
  <div class="row">
    <div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
      <form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
        <div formGroupName="userdata">
          <div class="form-group">
            <label for="username">Username</label>
            <input
              type="text"
              id="username"
              class="form-control"
              formControlName="username">
            <span
              *ngIf="signupForm.get('userdata.username').invalid && signupForm.get('userdata.username').touched"
              class="help-block">
              <span *ngIf="signupForm.get('userdata.username').errors['nameIsForbidden']">This name is invalid!</span>
              <span *ngIf="signupForm.get('userdata.username').errors['required']">This field is required!</span>
            </span>
          </div>
          <div class="form-group">
            <label for="email">email</label>
            <input
              type="text"
              id="email"
              class="form-control"
              formControlName="email">
            <span
              *ngIf="signupForm.get('userdata.email').invalid && signupForm.get('userdata.email').touched"
              class="help-block">Please enter a valid email!</span>
          </div>
        </div>
        <div class="radio" *ngFor="let gender of genders">
          <label>
            <input
              type="radio"
              [value]="gender"
              formControlName="gender">{{ gender }}
          </label>
        </div>
        <div formArrayName="hobbies">
          <h4>Your hobbies</h4>
          <button type="button"
                  class="btn btn-default"
                  (click)="onAddHobby()">Add hobby</button>
          <div class="form-group"
                *ngFor="let hobbyControl of hobbies; let i = index">
            <input type="text" class="form-control" [formControlName]="i">
          </div>
        </div>
        <span *ngIf="signupForm.invalid && signupForm.touched"
              class="help-block">Please enter a valid data!</span>
        <button class="btn btn-primary" type="submit">Submit</button>
      </form>
    </div>
  </div>
</div>
export class AppComponent implements OnInit{
  genders = ['male', 'female'];
  signupForm: FormGroup;
  forbiddenUserNames = ['Chris', 'Anna'];

  ngOnInit(): void {
    this.signupForm = new FormGroup({
      userdata: new FormGroup({
        username: new FormControl(null, [Validators.required, this.forbiddenNames.bind(this)]),
        email: new FormControl(null, [Validators.required, Validators.email])
      }),
      gender: new FormControl('female'),
      hobbies: new FormArray([])
    });
  }

  get hobbies() {
    return (this.signupForm.get('hobbies') as FormArray).controls;
  }

  onSubmit() {
    console.log(this.signupForm)
  }

  onAddHobby() {
    const control = new FormControl(null, Validators.required);
    (this.signupForm.get('hobbies') as FormArray).push(control);
  }

  forbiddenNames(control: FormControl): {[s: string]: boolean} {
    if(this.forbiddenUserNames.indexOf(control.value) !== -1){
      return {
        nameIsForbidden: true
      };
    }
    return null;
  }
}

在 TypeScript 中定義了一個新 method,參數上放入 FormControl 物件,代表想要進行驗證的物件,回傳則定義了一組物件,key 定義為 string、value 定義為 boolean,如果驗證失敗就回傳此物件,成功時則回傳 null ( 不是把 true 更改成 false,這個物件是驗證失敗時傳遞給外部的訊息,null 才是驗證成功, )。接著在上方的 signupForm 補上自己定義好的方法,並且使用 bind(..) 明確綁定當下的 this 為何。在 Template 中則是新增幾個 <span> 來代表驗證失敗時應該要呈現的訊息。

非同步驗證

有時驗證的資料並不會馬上取得,像 Promise 有三種狀態 fulfillment、rejeciton、pending,在 pending 狀態時表示資料還未取得,這時可以使用非同步的方式來進行驗證 ( 等資料來時才驗證 ) :

|--app
    |--app.component.ts // 更動
export class AppComponent implements OnInit{
  genders = ['male', 'female'];
  signupForm: FormGroup;
  forbiddenUserNames = ['Chris', 'Anna'];

  ngOnInit(): void {
    this.signupForm = new FormGroup({
      userdata: new FormGroup({
        username: new FormControl(null, [Validators.required, this.forbiddenNames.bind(this)]),
        email: new FormControl(null, [Validators.required, Validators.email], this.forbiddenEmails)
      }),
      gender: new FormControl('female'),
      hobbies: new FormArray([])
    });
  }

  get hobbies() {
    return (this.signupForm.get('hobbies') as FormArray).controls;
  }

  onSubmit() {
    console.log(this.signupForm)
  }

  onAddHobby() {
    const control = new FormControl(null, Validators.required);
    (this.signupForm.get('hobbies') as FormArray).push(control);
  }

  forbiddenNames(control: FormControl): {[s: string]: boolean} {
    if(this.forbiddenUserNames.indexOf(control.value) !== -1){
      return {
        nameIsForbidden: true
      };
    }
    return null;
  }

  forbiddenEmails(control: FormControl): Promise<any> | Observable<any> {
    const promise = new Promise((resolve, reject) => {
      setTimeout(() => {
        if(control.value === '[email protected]'){
          resolve({emailIsForbidden: true});
        } else {
          resolve(null);
        }
      }, 1500)
    });
    return promise;
  }
}

這邊新增驗證 email 的方法,傳入參數定義為 FormControl,回傳則定義為非同步物件 Promise 或是 Observable,並且寫下方法內部的細節,這邊所採用的是 Promise,並用 setTimeout(..) 來設定非同步訊息,無論驗證成功與否都會執行 resolve(..) 方法,只是驗證失敗時是在內部傳入物件 ( 成功是 null,表示不用帶任何訊息 ),並在最後必須回傳 Promise 物件。如果打開開發人員工具,會發現當 <input> 內部有所變動時,原本的 ng-validng-invalid 會轉變為 ng-pending 一下子,原因就在於 setTimeout(..) 的非同步設定,讓回傳的 Promise 還在 pending 狀態。

監控狀態及內部值

可以使用 valueChanges(..) 以及 statusChanges(..) 這兩個方法進行監控,範例中是在 ngOnInit(..) 中進行訂閱 :

export class AppComponent implements OnInit{
  genders = ['male', 'female'];
  signupForm: FormGroup;
  forbiddenUserNames = ['Chris', 'Anna'];

  ngOnInit(): void {
    this.signupForm = new FormGroup({
      userdata: new FormGroup({
        username: new FormControl(null, [Validators.required, this.forbiddenNames.bind(this)]),
        email: new FormControl(null, [Validators.required, Validators.email], this.forbiddenEmails)
      }),
      gender: new FormControl('female'),
      hobbies: new FormArray([])
    });
    this.signupForm.valueChanges.subscribe(console.log);
    this.signupForm.statusChanges.subscribe(console.log);
  }

  get hobbies() {
    return (this.signupForm.get('hobbies') as FormArray).controls;
  }

  onSubmit() {
    console.log(this.signupForm)
  }

  onAddHobby() {
    const control = new FormControl(null, Validators.required);
    (this.signupForm.get('hobbies') as FormArray).push(control);
  }

  forbiddenNames(control: FormControl): {[s: string]: boolean} {
    if(this.forbiddenUserNames.indexOf(control.value) !== -1){
      return {
        nameIsForbidden: true
      };
    }
    return null;
  }

  forbiddenEmails(control: FormControl): Promise<any> | Observable<any> {
    const promise = new Promise((resolve, reject) => {
      setTimeout(() => {
        if(control.value === '[email protected]'){
          resolve({emailIsForbidden: true});
        } else {
          resolve(null);
        }
      }, 1500)
    });
    return promise;
  }
}

嘗試讓驗證狀態以及 <input> 內部的值改動,可以在開發人員工具看到訊息。

setValue(..)pathcValue(..)reset(..)

與 Template-Driven Form 相同,可以使用的這個三個方法去設定表單的內容以及值,唯一不同的只是操作的物件對象不一樣而已 :

export class AppComponent implements OnInit{
  genders = ['male', 'female'];
  signupForm: FormGroup;
  forbiddenUserNames = ['Chris', 'Anna'];

  ngOnInit(): void {
    this.signupForm = new FormGroup({
      userdata: new FormGroup({
        username: new FormControl(null, [Validators.required, this.forbiddenNames.bind(this)]),
        email: new FormControl(null, [Validators.required, Validators.email], this.forbiddenEmails)
      }),
      gender: new FormControl('female'),
      hobbies: new FormArray([])
    });
    this.signupForm.valueChanges.subscribe(console.log);
    this.signupForm.statusChanges.subscribe(console.log);
    this.signupForm.setValue({
        userdata: {
          username: 'Max',
          email: '[email protected]'
        },
        gender: 'male',
        hobbies: []
    });
    this.signupForm.patchValue({
      userdata: {
        username: 'Anna',
      }
    });
  }

  get hobbies() {
    return (this.signupForm.get('hobbies') as FormArray).controls;
  }

  onSubmit() {
    console.log(this.signupForm)
    this.signupForm.reset();
  }

  onAddHobby() {
    const control = new FormControl(null, Validators.required);
    (this.signupForm.get('hobbies') as FormArray).push(control);
  }

  forbiddenNames(control: FormControl): {[s: string]: boolean} {
    if(this.forbiddenUserNames.indexOf(control.value) !== -1){
      return {
        nameIsForbidden: true
      };
    }
    return null;
  }

  forbiddenEmails(control: FormControl): Promise<any> | Observable<any> {
    const promise = new Promise((resolve, reject) => {
      setTimeout(() => {
        if(control.value === '[email protected]'){
          resolve({emailIsForbidden: true});
        } else {
          resolve(null);
        }
      }, 1500)
    });
    return promise;
  }
}

總結

  • FormControlFormGroupFormArray
  • 自定義驗證方法
  • 自定義非同步驗證方法
  • setValue(..)patchValue(..)reset(..)