angular - 表單

【唐】三三發表於2024-05-19

(1)模板驅動表單

參考:

  • 構建模板驅動表單

簡單表單

​ngForm​ 固定的,需要提交的值 ngModel​

 <form #sub="ngForm" (submit)="onSubmit(sub)">
        <input type="text" name="username" ngModel>  
        <input type="id" name="userId" ngModel>
        <button>提交</button>
      </form>
  onSubmit(form:NgForm){
    console.log(form.value);
    console.log(form.valid);

  }

分組

​ngModelGroup​ : 會建立 FormGroup 的例項並把它繫結到 DOM 元素中。

<form #sub="ngForm" (submit)="onSubmit(sub)">
        <ng-container ngModelGroup="user">
          <input type="text" name="username" ngModel>  
        </ng-container>
        <ng-container ngModelGroup="Guid">
        <input type="id" name="Id" ngModel>
      </ng-container>
        <button>提交</button>
      </form>

(2)模板驅動表單驗證

Angular 內建表單驗證器:

  1. required:檢查輸入欄位是否為空。
  2. minlength:檢查輸入欄位的長度是否符合最小長度要求。
  3. maxlength:檢查輸入欄位的長度是否符合最大長度要求。
  4. min:檢查輸入欄位的值是否符合最小值要求。
  5. max:檢查輸入欄位的值是否符合最大值要求。
  6. pattern:使用正規表示式來驗證輸入欄位的值是否符合特定的模式。
  7. email:檢查輸入欄位的值是否符合電子郵件格式。
  8. url:檢查輸入欄位的值是否符合 URL 格式。
  9. date:檢查輸入欄位的值是否符合日期格式。
  10. checkbox:檢查是否選擇了核取方塊。
  11. number:檢查輸入欄位的值是否為數字。
  12. ​requiredTrue()​: 這個驗證器是 Angular 中的一個內建驗證器,它用於檢查輸入欄位是否被選中和/或填寫。如果輸入欄位的值是 true​ 或者使用者已經填寫了該欄位,那麼這個驗證就會透過。如果輸入欄位為空或者未被選中,那麼這個驗證就會失敗。
  13. ​compose()​: 這個函式是用來組合多個驗證器的。你可以將多個驗證器傳遞給 compose()​ 函式,然後它將返回一個新的驗證器,這個新的驗證器將按照你指定的順序執行這些驗證器。你可以使用 pipe()​ 函式來將多個驗證器串聯起來,但是 compose()​ 更加靈活,因為它允許你在不同的上下文中重複使用相同的驗證器。s

Angular 中文文件-內建驗證器,可用於各種表單控制元件

顯示未透過驗證的資訊

要啟用驗證功能:必須有一個模板引用變數 #username="ngModel"​,這裡的 ngModel​指令提供的功能,用於驗證狀態。

驗證物件屬性

名稱 描述
path 這個屬性返回元素的名稱
valid 透過驗證規則定義的條件 (樣式 ng-valid)
invalid 不能透過驗證規則定義的條件 (樣式 ng-invalid)
pristine 內容沒修改,未被使用者編輯(樣式 ng-pristine)
dirty 內容被修改,被使用者編輯 (樣式 ng-drity)
touched 元素被使用者訪問 (樣式 ng-touched)(一般透過製表符 TAB​選擇表單域)
untouched 元素未被使用者訪問, (樣式 ng-untouched)(一般透過製表符 TAB​選擇表單域)
errors 返回一個 ValidationErrors 鍵值對物件
​export declare type ValidationErrors = {

​ [key: string]: any;

​};
value 返回 value 值,用於 自定義表單驗證規則​
      <form #sub="ngForm" (submit)="onSubmit(sub)">
        <input type="text" name="username" required pattern="\d"  #username="ngModel" as ngModel>
        <div *ngIf="username.touched && username.invalid && username.errors">
          <div *ngIf="username.errors['required']">必須填寫</div>
          <div *ngIf="username.errors['pattern']">必須為數字</div>
        </div>

        <input type="id" name="userId" ngModel>
        <button  [disabled]="sub.invalid">提交</button>
      </form>

這裡 angular 15 使用的 errors["XXXX"]​ 的方式(ValidationErrors​),angular 12 使用的 errors.requered 的方式。

表單驗證錯誤描述屬性

名稱 描述
minlength.requiredLength 所需的字元數
minlength.actualLength 輸入的字元數
pattern.requiredPattern 返回指定正則
pattern.actualValue 元素的內容

使用元件顯示驗證訊息,驗證整個表單

formSubmitted 指定表單是否已經提交,並在提交前阻止驗證。

component.ts

import { ApplicationRef, Component } from '@angular/core';
import { Model } from './repository.model';
import { Product } from './product.model';
import { NgForm } from '@angular/forms';

@Component({
  selector: 'app',
  templateUrl: 'template.html',
})
export class ProductComponent {
  model: Model = new Model();
  //是否已經提交
  formSubmitted: boolean = false;

  submitForm(form: NgForm) {
    this.formSubmitted = true;
    if (form.valid) {
      this.addProject(this.newProduct);
      this.newProduct = new Product();
      form.reset();
      this.formSubmitted = false;
    }
  }

  getValidationMessages(state: any, thingName?: string) {
    let thing: string = state.path || thingName;
    let messages: string[] = [];
    if (state.errors) {
      for (const errorName in state.errors) {
        switch (errorName) {
          case 'required':
            messages.push(`you must enter a ${thing}`);
            break;
          case 'minlength':
            messages.push(
              `a ${thing} must be at least ${state.errors['minlength'].requiredLength}`
            );
            break;
          case 'pattern':
            messages.push(`The ${thing} contains illegal chracters`);
            break;
        }
      }
    }

    return messages;
  } 

//......other method
}

html

form定義一個引用變數 form​,ngForm​賦值給它:#form="ngForm"​,ngSubmit 繫結表示式呼叫控制器 submitForm​

​name="name"​這裡的字串name就是 #username​的 path​

<!-- 14.4.3 驗證整個表單 -->
<style>
  input.ng-dirty.ng-invalid {
    border: 2px solid #ff0000
  }

  input.ng-dirty.ng-valid {
    border: 2px solid #6bc502
  }
</style>

<div class="m-2">
  <div class="bg-info text-white mb-2 p-2">Model Data:{{jsonProduct}}</div>

  <form #form="ngForm" (ngSubmit)="submitForm(form)">
    <div class="bg-danger text-white p-2 mb-2" *ngIf="formSubmitted && form.invalid">
      There are problems with the form
    </div>
    <div class="form-group">
      <label>Name</label>
     <input class="form-control" name="name" [(ngModel)]="newProduct.name" #username="ngModel" required minlength="5"
        pattern="^[A-Za-z ]+$" />
      <ul class="text-danger list-unstlyed"
        *ngIf="(formSubmitted || username.dirty) && username.invalid && username.errors">
        <li *ngFor="let error of getValidationMessages(username)">
          <span>{{error}}</span>
        </li>
      </ul>
    </div>

    <button class="btn btn-primary" type="submit">Create</button>
  </form>

</div>

1 顯示摘要資訊

component.ts

透過 NgForm​ 物件的 controls​ 屬性訪問各個元素

  getFormValidationMessages(form: NgForm): string[] {
    let messages: string[] = [];
    Object.keys(form.controls).forEach((k) => {
      this.getValidationMessages(form.controls[k], k).forEach((m) =>
        messages.push(m)
      );
    });
    return messages;
  }

html

<!-- 14.4.3-1 顯示驗證摘要資訊 -->
<style>
  input.ng-dirty.ng-invalid {
    border: 2px solid #ff0000
  }

  input.ng-dirty.ng-valid {
    border: 2px solid #6bc502
  }
</style>

<div class="m-2">
  <div class="bg-info text-white mb-2 p-2">Model Data:{{jsonProduct}}</div>

  <form #form="ngForm" (ngSubmit)="submitForm(form)">
    <div class="bg-danger text-white p-2 mb-2" *ngIf="formSubmitted && form.invalid">
      There are problems with the form
      <ul>
        <li *ngFor="let error of getFormValidationMessages(form)">
          {{error}}
        </li>
      </ul>
    </div>

    <div class="form-group">
      <label>Name</label>
      <input class="form-control" name="name" [(ngModel)]="newProduct.name" #username="ngModel" required minlength="5"
        pattern="^[A-Za-z ]+$" />
      <ul class="text-danger list-unstlyed"
        *ngIf="(formSubmitted || username.dirty) && username.invalid && username.errors">
        <li *ngFor="let error of getValidationMessages(username)">
          <span>{{error}}</span>
        </li>
      </ul>
    </div>

    <button class="btn btn-primary" type="submit">Create</button>
  </form>

</div>

2 禁用提交按鈕

<button class="btn btn-primary" type="submit" [disabled]="formSubmitted && form.invalid"
      [class.btn-secondary]="formSubmitted && form.invalid">
      Create
    </button>

(3)模型驅動表單

3.1) 響應式表單

3.1.1) 建立基礎表單

1 在你的應用中註冊響應式表單模組。該模組宣告瞭一些你要用在響應式表單中的指令。

要使用響應式表單控制元件,就要從 @angular/forms 包中匯入 ReactiveFormsModule,並把它新增到你的 NgModule 的 imports 陣列中。

import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
  imports: [
    // other imports ...
    ReactiveFormsModule
  ],
})
export class AppModule { }

2 生成一個新的 FormControl 例項,並把它儲存在元件中。

import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';

@Component({
  selector: 'app-name-editor',
  templateUrl: './name-editor.component.html',
  styleUrls: ['./name-editor.component.css']
})
export class NameEditorComponent {
  name = new FormControl('');
}

3 在模板中註冊這個 FormControl。

<label for="name">Name: </label>
<input id="name" type="text" [formControl]="name">

使用這種模板繫結語法,把該表單控制元件註冊給了模板中名為 name 的輸入元素。這樣,表單控制元件和 DOM 元素就可以互相通訊了:檢視會反映模型的變化,模型也會反映檢視中的變化。

a) 顯示錶單值

  • 透過可觀察物件 valueChanges,你可以在模板中使用 AsyncPipe或在元件類中使用 subscribe() 方法來監聽表單值的變化。

  • 使用 value 屬性。它能讓你獲得當前值的一份快照。

<p>Value: {{ name1.value }}</p>

<label for="name">Name: </label>
<input id="name" type="text" [formControl]="name1">

一旦你修改了表單控制元件所關聯的元素,這裡顯示的值也跟著變化了。

b) 替換表單控制元件的值

html

<p>Value: {{ name1.value }}</p>

<label for="name">Name: </label>
<input id="name" type="text" [formControl]="name1">
<button type="button" (click)="updateName()">Update Name</button>

FormControl 例項提供了一個 setValue() 方法,它會修改這個表單控制元件的值

  updateName() {
    this.name1.setValue('Nancy');
  }

3.1.2) 把表單分組 FormGroup 和 FromArray

FormGroup

定義了一個帶有一組控制元件的表單,你可以把它們放在一起管理。表單組的基礎知識將在本節中討論。你也可以透過巢狀表單組來建立更復雜的表單。

FormArray

定義了一個動態表單,你可以在執行時新增和刪除控制元件。你也可以透過巢狀表單陣列來建立更復雜的表單。欲知詳情,參閱下面的建立動態表單

a) FormGroup 建立確定子控制元件數量的動態表單

1.建立一個 FormGroup 例項。

要初始化這個 FormGroup,請為建構函式提供一個由控制元件組成的物件,物件中的每個名字都要和表單控制元件的名字一一對應

import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-ProfileEditor',
  templateUrl: './ProfileEditor.component.html',
  styleUrls: ['./ProfileEditor.component.css']
})
export class ProfileEditorComponent implements OnInit {
  profileForm = new FormGroup({
    firstName: new FormControl(''),
    lastName: new FormControl(''),
  });
  constructor() { }

  ngOnInit() {
  }
}

2 把這個 FormGroup 模型關聯到檢視

FormControlName 指令提供的 formControlName 屬性把每個輸入框和 FormGroup 中定義的表單控制元件繫結起來。這些表單控制元件會和相應的元素通訊,它們還把更改傳給 FormGroup,這個 FormGroup 是模型值的事實之源。

<form [formGroup]="profileForm">

  <label for="first-name">First Name: </label>
  <input id="first-name" type="text" formControlName="firstName">

  <label for="last-name">Last Name: </label>
  <input id="last-name" type="text" formControlName="lastName">

</form>

3 儲存表單資料。

ProfileEditor 元件從使用者那裡獲得輸入,但在真實的場景中,你可能想要先捕獲表單的值,等將來在元件外部進行處理。FormGroup 指令會監聽 form 元素髮出的 submit 事件,併發出一個 ngSubmit 事件,讓你可以繫結一個回撥函式。把 onSubmit() 回撥方法新增為 form 標籤上的 ngSubmit 事件監聽器。

<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">

ProfileEditor 元件上的 onSubmit() 方法會捕獲 profileForm 的當前值。要保持該表單的封裝性,就要使用 EventEmitter 向元件外部提供該表單的值。下面的例子會使用 console.warn 把這個值記錄到瀏覽器的控制檯中。

onSubmit() {
  // TODO: Use EventEmitter with form value
  console.warn(this.profileForm.value);
}

form 標籤所發出的 submit 事件是內建 DOM 事件,透過點選型別為 submit 的按鈕可以觸發本事件。這還讓使用者可以用Enter鍵來提交填完的表單。往表單的底部新增一個 button,用於觸發表單提交。

  <p>Complete the form to enable button.</p>
  <button type="submit" [disabled]="!profileForm.valid">Submit</button>

b) 巢狀表單組

1 建立一個巢狀的表單組

在這個例子中,address group 把現有的 firstNamelastName 控制元件和新的 streetcitystatezip 控制元件組合在一起。雖然 address 這個 FormGroupprofileForm 這個整體 FormGroup 的一個子控制元件,但是仍然適用同樣的值和狀態的變更規則。來自內嵌控制元件組的狀態和值的變更將會冒泡到它的父控制元件組,以維護整體模型的一致性

import { Component } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';

@Component({
  selector: 'app-profile-editor',
  templateUrl: './profile-editor.component.html',
  styleUrls: ['./profile-editor.component.css']
})
export class ProfileEditorComponent {
  profileForm = new FormGroup({
    firstName: new FormControl(''),
    lastName: new FormControl(''),
    address: new FormGroup({
      street: new FormControl(''),
      city: new FormControl(''),
      state: new FormControl(''),
      zip: new FormControl('')
    })
  });
}

2 在模板中對這個巢狀表單分組

內嵌的使用formGroupName

<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">

  <label for="first-name">First Name: </label>
  <input id="first-name" type="text" formControlName="firstName">

  <label for="last-name">Last Name: </label>
  <input id="last-name" type="text" formControlName="lastName">

  <div formGroupName="address">
    <h2>Address</h2>

    <label for="street">Street: </label>
    <input id="street" type="text" formControlName="street">

    <label for="city">City: </label>
    <input id="city" type="text" formControlName="city">

    <label for="state">State: </label>
    <input id="state" type="text" formControlName="state">

    <label for="zip">Zip Code: </label>
    <input id="zip" type="text" formControlName="zip">
  </div>

  <p>Complete the form to enable button.</p>
  <button type="submit" [disabled]="!profileForm.valid">Submit</button>
</form>

c) 更新部分資料模型

當修改包含多個 FormGroup 例項的值時,你可能只希望更新模型中的一部分,而不是完全替換掉

方法 詳情
setValue() 使用 setValue() 方法來為單個控制元件設定新值。setValue() 方法會嚴格遵循表單組的結構,並整體性替換控制元件的值。
patchValue() 用此物件中定義的任意屬性對錶單模型進行替換。

setValue() 方法的嚴格檢查可以幫助你捕獲複雜表單巢狀中的錯誤,而 patchValue() 在遇到那些錯誤時可能會默默的失敗。

ProfileEditorComponent 中,使用 updateProfile 方法傳入下列資料可以更新使用者的名字與街道住址。

  updateProfile() {
    const newFirstName = 'Alice'; // 新的first name值
    // setValue 更新單個
    this.profileForm.get('firstName')?.setValue(newFirstName);

    // patchValue 更新多個一部分
    // this.profileForm.patchValue({
    //   firstName: 'Nancy',
    //   address: {
    //     street: '123 Drew Street'
    //   }
    // });
  }

加個按鈕來更新

<button type="button" (click)="updateProfile()">Update Profile</button>

3.1.3) FormBuilder 服務生成控制元件

a 匯入 FormBuilder 類。

b 注入這個 FormBuilder 服務。

c 生成表單內容。

FormBuilder 服務有三個方法:control()group()array()。這些方法都是工廠方法,用於在元件類中分別生成 FormControlFormGroupFormArray

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-ProfileEditor',
  templateUrl: './ProfileEditor.component.html',
  styleUrls: ['./ProfileEditor.component.css']
})
export class ProfileEditorComponent implements OnInit {
  profileForm = this.fb.group({
    firstName: [''],
    lastName: [''],
    address: this.fb.group({
      street: [''],
      city: [''],
      state: [''],
      zip: ['']
    }),
  });

  constructor(private fb: FormBuilder) { }

  ngOnInit() {
  }
}
// ts{10-19,21}

在上面的例子中,你可以使用 group() 方法,用和前面一樣的名字來定義這些屬性。這裡,每個控制元件名對應的值都是一個陣列,這個陣列中的第一項是其初始值。

3.1.4) 響應式表單的驗證

3.1.4.1) 在表單元件中匯入一個驗證器函式。

@angular/forms 包中匯入 Validators 類。

import { Validators } from '@angular/forms';

3.1.4.2) 把這個驗證器新增到表單中的相應欄位。

profileForm = this.fb.group({
  firstName: ['', Validators.required],
  lastName: [''],
  address: this.fb.group({
    street: [''],
    city: [''],
    state: [''],
    zip: ['']
  }),
});

3.1.4.3) 新增邏輯來處理驗證狀態。

<p>Form Status: {{ profileForm.status }}</p>

參考:

  • 在響應式表單中驗證輸入

3.1.5) FormArray 建立不確定數量子控制元件的動態表單

FormArray 如果你事先不知道子控制元件的數量,這就是一個很好的選擇。

FormArrayFormGroup 之外的另一個選擇,用於管理任意數量的匿名控制元件。像 FormGroup 例項一樣,你也可以往 FormArray 中動態插入和移除控制元件,並且 FormArray 例項的值和驗證狀態也是根據它的子控制元件計算得來的。不過,你不需要為每個控制元件定義一個名字作為 key.

a) 匯入 FormArray 類。

b) 定義一個 FormArray 控制元件。

使用 FormBuilder.array() 方法來定義該陣列,並用 FormBuilder.control() 方法來往該陣列中新增一個初始控制元件。

FormGroup 中的這個 aliases 控制元件現在管理著一個控制元件,將來還可以動態新增多個。

profileForm = this.fb.group({
  firstName: ['', Validators.required],
  lastName: [''],
  address: this.fb.group({
    street: [''],
    city: [''],
    state: [''],
    zip: ['']
  }),
  aliases: this.fb.array([
    this.fb.control('')
  ])
});

c) 使用 getter 方法訪問 FormArray 控制元件。

相對於重複使用 profileForm.get() 方法獲取每個例項的方式,getter 可以讓你輕鬆訪問表單陣列各個例項中的別名。表單陣列例項用一個陣列來代表未定數量的控制元件。透過 getter 來訪問控制元件很方便,這種方法還能很容易地重複處理更多控制元件。
使用 getter 語法建立類屬性 aliases,以從父表單組中接收表示綽號的表單陣列控制元件。

get aliases() {
  return this.profileForm.get('aliases') as FormArray;
}

注意
因為返回的控制元件的型別是 AbstractControl,所以你要為該方法提供一個顯式的型別宣告來訪問 FormArray 特有的語法。

定義一個方法來把一個綽號控制元件動態插入到綽號 FormArray 中。用 FormArray.push() 方法把該控制元件新增為陣列中的新條目。

addAlias() {
  this.aliases.push(this.fb.control(''));
}

在這個模板中,這些控制元件會被迭代,把每個控制元件都顯示為一個獨立的輸入框。

d) 在模板中顯示這個表單陣列。

要想為表單模型新增 aliases,你必須把它加入到模板中供使用者輸入。和 FormGroupNameDirective 提供的 formGroupName 一樣,FormArrayNameDirective 也使用 formArrayName 在這個 FormArray 例項和模板之間建立繫結。
formGroupName <div> 元素的結束標籤下方,新增一段模板 HTML。

  <div formArrayName="aliases">
    <h2>Aliases</h2>
    <button type="button" (click)="addAlias()">+ Add another alias</button>

    <div *ngFor="let alias of aliases.controls; let i=index">
      <!-- The repeated alias template -->
      <label for="alias-{{ i }}">Alias:</label>
      <input id="alias-{{ i }}" type="text" [formControlName]="i">
    </div>
  </div>

3.2) 嚴格型別化表單

從 Angular 14 開始,響應式表單預設是嚴格型別的。

3.2.1) FormControl 入門

const email = new FormControl('angularrox@gmail.com');

此控制元件將被自動推斷為 FormControl<string|null> 型別。TypeScript 會在整個FormControl API中自動強制執行此型別,例如 email.valueemail.valueChangesemail.setValue(...) 等。

a) 可空性 nonNullable

你可能想知道:為什麼此控制元件的型別包含 null ?這是因為控制元件可以隨時透過呼叫 reset 變為 null

const email = new FormControl('angularrox@gmail.com');
email.reset();
console.log(email.value); // null

TypeScript 將強制你始終處理控制元件已變為 null 的可能性。如果要使此控制元件不可為空,可以用 nonNullable 選項。這將導致控制元件重置為其初始值,而不是 null

const email = new FormControl('angularrox@gmail.com', {nonNullable: true});
email.reset();
console.log(email.value); // angularrox@gmail.com

3.2.2) FormArray:動態的、同質的集合

FormArray 包含一個開放式控制元件列表。type 引數對應於每個內部控制元件的型別:

const names = new FormArray([new FormControl('Alex')]);
names.push(new FormControl('Jess'));

此 FormArray 將具有內部控制元件型別 FormControl<string|null>

如果你想在陣列中有多個不同的元素型別,則必須使用 UntypedFormArray,因為 TypeScript 無法推斷哪種元素型別將出現在哪個位置。

3.2.3) FormGroup 和 FormRecord

Angular 為具有列舉鍵集的表單提供了 FormGroup 型別,併為開放式或動態組提供了一種名為 FormRecord 的型別。

a) 部分值

const login = new FormGroup({
    email: new FormControl('', {nonNullable: true}),
    password: new FormControl('', {nonNullable: true}),
});

在任何 FormGroup 上,都可以禁用控制元件。任何禁用的控制元件都不會出現在組的值中。

因此,login.value 的型別是 Partial<{email: string, password: string}>。這種型別的 Partial 意味著每個成員可能是未定義的。

更具體地說,login.value.email 的型別是 string|undefined,TypeScript 將強制你處理可能 undefined 的值(如果你啟用了 strictNullChecks)。

如果你想訪問包括禁用控制元件的值,從而繞過可能的 undefined 欄位,可以用 login.getRawValue()

b) 可選控制元件和動態組

某些表單的控制元件可能存在也可能不存在,可以在執行時新增和刪除。你可以用可選欄位來表示這些控制元件:

interface LoginForm {
    email: FormControl<string>;
    password?: FormControl<string>;
}

const login = new FormGroup<LoginForm>({
    email: new FormControl('', {nonNullable: true}),
    password: new FormControl('', {nonNullable: true}),
});

login.removeControl('password');

在這個表單中,我們明確地指定了型別,這使我們可以將 password 控制元件設為可選的。TypeScript 會強制只有可選控制元件才能被新增或刪除。

c) FormRecord

某些 FormGroup 的用法不符合上述模式,因為鍵是無法提前知道的。FormRecord 類就是為這種情況設計的:

const addresses = new FormRecord<FormControl<string|null>>({});
addresses.addControl('Andrew', new FormControl('2340 Folsom St'));

任何 string|null 型別的控制元件都可以新增到此 FormRecord

如果你需要一個動態(開放式)和異構(控制元件是不同型別)的 FormGroup,則無法提升為型別安全的,這時你應該使用 UntypedFormGroup

FormRecord 也可以用 FormBuilder 構建:

const addresses = fb.record({'Andrew': '2340 Folsom St'});

如果你需要一個動態(開放式)和異構(控制元件是不同型別)的 FormGroup,則無法提高型別安全,你應該使用 UntypedFormGroup

3.2.4) FormBuilder 和 NonNullableFormBuilder

FormBuilder 類已升級為支援新增的型別的版本,方式與上面的示例相同。

此外,還有一個額外的構建器:NonNullableFormBuilder。它是在所有控制元件都上指定 {nonNullable: true} 的簡寫,用來在大型非空表單中消除主要的樣板程式碼。你可以用 FormBuilder 上的 nonNullable 屬性訪問它:

const fb = new FormBuilder();
const login = fb.nonNullable.group({
    email: '',
    password: '',
});

在上面的示例中,兩個內部控制元件都將不可為空(即將設定 nonNullable)。

你還可以用名稱 NonNullableFormBuilder 注入它。

3.3) 構建動態表單

3.4) 例子

ReactiveFormsModule 的使用

app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

// import { AppComponent } from './app.component';
import { ProductComponent } from './component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

@NgModule({
  declarations: [ProductComponent], //列出應用的類,告訴Angular哪些類構成了這個模組。
  imports: [BrowserModule, FormsModule, ReactiveFormsModule],
  providers: [], //提供服務
  bootstrap: [ProductComponent], //根元件
})
export class AppModule {}

基於模型的功能在 ReactiveFormsModule 模組定義中。

FormControl 和 FormGroup 的使用

FormControl: 表單中的表單項

FormGroup: 表單組,表單至少是一個 FormGroup

// form.module.ts
import { FormControl, FormGroup, Validators } from '@angular/forms';

export class ProductFormControl extends FormControl {   //FormControl表單中的單個元素
  lable: string;  
  modelProperty: string;

  /**
   * 建構函式用於初始化一個帶有label、模型屬性、值和驗證器的 FormControl。
   * @param {string} lable - 表示控制元件的標籤或名稱的字串。它用於在使用者介面中顯示控制元件的label。
   * @param {string} modelProperty - 表示繫結到此表單控制元件的模型物件的屬性名稱的字串。它用於將表單控制元件的值對映到模型物件的相應屬性上。
   * @param {any} value - 代表控制元件當前值的引數。對於 FormControl,它是當前值。對於已啟用的 FormGroup,它是組內所有已啟用控制元件的值,以鍵值對的形式呈現為物件。對於已禁用的 FormGroup,它是組內所有控制元件的值。
   * @param {any} validator - 用於確定控制元件有效性的函式。它可以是一個單獨的驗證器函式,也可以是使用 Validators.compose() 函式組合多個驗證器函式的結果。驗證器用於對控制元件的值執行驗證檢查,並在值無效時返回錯誤物件。
   */
  constructor(
    lable: string,
    modelProperty: string,
    value: any,
    validator: any
  ) {

/**
 * 例子:const fc = new FormControl('foo');
 *  
 * value: 控制元件的當前值。
    對於 FormControl,是當前值。
    對於已啟用的 FormGroup,是組內所有已啟用控制元件的值,以物件形式呈現,每個組成員都有一個鍵值對。
    對於已禁用的 FormGroup,是組內所有控制元件的值,以物件形式呈現,每個組成員都有一個鍵值對。
    對於 FormArray,是組內所有已啟用控制元件的值,以陣列形式呈現。

    validator: 返回用於同步確定此控制元件有效性的函式。如果已新增多個驗證器,則這將是一個組合函式。有關更多資訊,請參閱 Validators.compose()。
 */  
    super(value, validator);  //實現 FormControl 的建構函式
    this.lable = lable;
    this.modelProperty = modelProperty;
  }
}

export class ProductFormGroup extends FormGroup {   //FormGroup 管理form 元素
  constructor() {
    super({
      name: new ProductFormControl('Name', 'name', '', Validators.required),
      category: new ProductFormControl(
        'Category',
        'category',
        '',
        Validators.compose([
          Validators.required,
          Validators.pattern('^[A-Za-z ]+$'),
          Validators.minLength(3),
          Validators.maxLength(10),
        ])
      ),
      price: new ProductFormControl(
        'Price',
        'price',
        '',
        Validators.compose([
          Validators.required,
          Validators.pattern('^[0-9\.]+$'),
        ])
      ),
    });
  }
}

​FormGroup​​的建構函式接收一個物件,該物件各個屬性與各個屬性的名稱與模板各個 input 元素的名稱一一對應,每個屬性都賦予一個用來表示該 input 元素的 ProductFormControl​​ 物件,該物件同時指定 input 的驗證要求。

傳給超類建構函式物件第一個屬性最簡單:

​name: new ProductFormControl('Name', 'name', '', Validators.required),​​

屬性名為 name​​,告訴angular 屬性對應模板中名為 name 的input元素。ProductFormControl 建構函式實參指定:

與 input 元素相關的 label 元素內容(Name),

input 元素繫結的 Product 類的某個屬性名稱(name),

資料繫結的初始值(空字串)

所需的驗證(Validators.required)

可以使用 Validators.compose​​ 將多個驗證器組合起來。

FormArray: 複雜表單, 動態新增表單項和表單組,表單驗證時,FormArray 有一項沒透過,整體不透過。

(4)模型驅動的表單驗證

錯誤訊息生成從元件移動到表單模型類,讓元件儘可能簡單。ProductFormControl​​ 類的 getValidationMessages​​ 方法中尉 maxLength​​ 新增的驗證訊息。

// form.module.ts
import { FormControl, FormGroup, Validators } from '@angular/forms';

export class ProductFormControl extends FormControl {
  //FormControl表單中的單個元素
  lable: string;
  modelProperty: string;

  /**
   * 建構函式用於初始化一個帶有lable、模型屬性、值和驗證器的 FormControl。
   * @param {string} lable - 表示控制元件的標籤或名稱的字串。它用於在使用者介面中顯示控制元件的 lable。
   * @param {string} modelProperty - 表示繫結到此表單控制元件的模型物件的屬性名稱的字串。它用於將表單控制元件的值對映到模型物件的相應屬性上。
   * @param {any} value - 代表控制元件當前值的引數。對於 FormControl,它是當前值。對於已啟用的 FormGroup,它是組內所有已啟用控制元件的值,以鍵值對的形式呈現為物件。對於已禁用的 FormGroup,它是組內所有控制元件的值。
   * @param {any} validator - 用於確定控制元件有效性的函式。它可以是一個單獨的驗證器函式,也可以是使用 Validators.compose() 函式組合多個驗證器函式的結果。驗證器用於對控制元件的值執行驗證檢查,並在值無效時返回錯誤物件。
   */
  constructor(
    lable: string,
    modelProperty: string,
    value: any,
    validator: any
  ) {
    /**
 * 例子:const fc = new FormControl('foo');
 *  
 * value: 控制元件的當前值。
    對於 FormControl,是當前值。
    對於已啟用的 FormGroup,是組內所有已啟用控制元件的值,以物件形式呈現,每個組成員都有一個鍵值對。
    對於已禁用的 FormGroup,是組內所有控制元件的值,以物件形式呈現,每個組成員都有一個鍵值對。
    對於 FormArray,是組內所有已啟用控制元件的值,以陣列形式呈現。

    validator: 返回用於同步確定此控制元件有效性的函式。如果已新增多個驗證器,則這將是一個組合函式。有關更多資訊,請參閱 Validators.compose()。
 */
    super(value, validator); //實現 FormControl 的建構函式
    this.lable = lable;
    this.modelProperty = modelProperty;
  }

  //14.5.2 定義表單模型
  getValidationMessages() {
    let messages: string[] = [];
    if (this.errors) {
      for (const errorName in this.errors) {
        switch (errorName) {
          case 'required':
            messages.push(`you must enter a ${this.lable}`);
            break;
          case 'minlength':
            messages.push(
              `a ${this.lable} must be at least ${this.errors['minlength'].requiredLength}`
            );
            break;
          case 'maxlength':
            messages.push(
              `a ${this.lable} must be no more than ${this.errors['minlength'].requiredLength}`
            );
            break;
          case 'pattern':
            messages.push(`The ${this.lable} contains illegal chracters`);
            break;
        }
      }
    }
    return messages;
  }
}

export class ProductFormGroup extends FormGroup {
  //FormGroup 管理form 元素
  constructor() {
    super({
      name: new ProductFormControl('Name', 'username', '', Validators.required),
      category: new ProductFormControl(
        'Category',
        'category',
        '',
        Validators.compose([
          Validators.required,
          Validators.pattern('^[A-Za-z ]+$'),
          Validators.minLength(3),
          Validators.maxLength(10),
        ])
      ),
      price: new ProductFormControl(
        'Price',
        'price',
        '',
        Validators.compose([
          Validators.required,
          Validators.pattern('^[0-9.]+$'),
        ])
      ),
    });
  }

  get productControls(): ProductFormControl[] {
    return Object.keys(this.controls).map(
      (k) => this.controls[k] as ProductFormControl
    );
  }

  getValidationMessages(name: string): string[] {
    return (
      this.controls[name] as ProductFormControl
    ).getValidationMessages();
  }

  getFormValidationMessages(): string[] {
    let messages: string[] = [];
    Object.values(this.controls).forEach((c) =>
      messages.push(...(c as ProductFormControl).getValidationMessages())
    );
    return messages;
  }
}

刪除了用於生成錯誤訊息的方法,它們被移動到 form.model​​

匯入 ProductFormGroup 類,並且定義一個 fromGroup 屬性,這樣就可以使用自定義表單模型類。

//14.5.3 使用模型驗證
//component.ts
import { ApplicationRef, Component } from '@angular/core';
import { Model } from './repository.model';
import { Product } from './product.model';
import { NgForm } from '@angular/forms';
import { ProductFormGroup } from './form.module';

@Component({
  selector: 'app',
  templateUrl: 'template.html',
})
export class ProductComponent {
  model: Model = new Model();
  formGroup: ProductFormGroup = new ProductFormGroup();
  selectedProduct: string = '';

  //是否已經提交
  formSubmitted: boolean = false;

  submitForm() {
    Object.keys(this.formGroup.controls)
      .forEach(c=> this.newProduct[c] = this.formGroup.controls[c].value);//如果錯誤:No index signature with a parameter of type 'string' was found on type 'Product'. => Product 類定義 [key: string]: any;

    this.formSubmitted = true;
    if (this.formGroup.valid) {
      this.addProject(this.newProduct);
      this.newProduct = new Product();
      this.formGroup.reset();
      this.formSubmitted = false;
    }
  }

 newProduct: Product = new Product();

  get jsonProduct() {
    return JSON.stringify(this.newProduct);
  }

  addProject(p: Product) {
    console.log('new project: ' + this.newProduct);
  }

}

下面是HTML,

<!-- 14.5.3 使用模型驗證 -->
<style>
  input.ng-dirty.ng-invalid {
    border: 2px solid #ff0000
  }

  input.ng-dirty.ng-valid {
    border: 2px solid #6bc502
  }
</style>

<div class="m-2">
  <div class="bg-info text-white mb-2 p-2">Model Data:{{jsonProduct}}</div>

  <form class="m-2" novalidate [formGroup]="formGroup" (ngSubmit)="submitForm()">
    <div class="bg-danger text-white p-2 mb-2" *ngIf="formSubmitted && formGroup.invalid">
      There are problems with the form
      <ul>
        <li *ngFor="let error of formGroup.getFormValidationMessages()">
          {{error}}
        </li>
      </ul>
    </div>

    <div class="form-group">
      <label>Name</label>
      <input class="form-control" name="name" formControlName="name" />
      <ul class="text-danger list-unstlyed"
        *ngIf="(formSubmitted || formGroup.controls['name'].dirty) && formGroup.controls['name'].invalid">
        <li *ngFor="let error of formGroup.getValidationMessages('name')">
          <span>{{error}}</span>
        </li>
      </ul>
    </div>

    <div class="form-group">
      <label>Category</label>
      <input class="form-control" name="price" formControlName="category" />
      <ul class="text-danger list-unstlyed"
        *ngIf="(formSubmitted || formGroup.controls['category'].dirty) && formGroup.controls['category'].invalid">
        <li *ngFor="let error of formGroup.getValidationMessages('category')">
          <span>{{error}}</span>
        </li>
      </ul>
    </div>

    <div class="form-group">
      <label>Price</label>
      <input class="form-control" name="price" formControlName="price" />
      <ul class="text-danger list-unstlyed"
        *ngIf="(formSubmitted || formGroup.controls['price'].dirty) && formGroup.controls['price'].invalid">
        <li *ngFor="let error of formGroup.getValidationMessages('price')">
          <span>{{error}}</span>
        </li>
      </ul>
    </div>

    <button class="btn btn-primary" type="submit" 
    [disabled]="formSubmitted && formGroup.invalid"
      [class.btn-secondary]="formSubmitted && formGroup.invalid">
      Create
    </button>
  </form>
</div>

第1個修改:form

<form class="m-2" novalidate [formGroup]="formGroup" (ngSubmit)="submitForm()">

​[formGroup]="formGroup"​​ 賦給 formGroup 指令的值是 form​​ 屬性,這裡是 formGroup,返回一個 ProductFormGroup​​ 物件。

第2個修改:input

​刪除​​了單獨的驗證屬性和被賦予特殊值 ngForm​​ 的模板變數。但是新增了 foprmControlName​​ 屬性,模型表單這裡使用 ProductFormGroup 使用名稱來識別。

foprmControlName 讓 angular 新增和移除 input元素的驗證類。告訴 angular,用特定的驗證器驗證。

···
name: new ProductFormControl('Name', 'username', '', Validators.required),
···

第3個修改: ul

FormGroup 提供一個 controls 屬性,返回自己管理的 FormControl 物件集合,按名稱進行索引。

export class Product {
  constructor(
    public id?: number,
    public name?: string,
    public category?: string,
    public price?: number
  ) {}
  [key: string]: any;
}

相關文章