آنلاین

Reactive forms

Reactive forms رویکردی model-driven برای مدیریت inputهای فرم فراهم می‌کنند؛ inputهایی که مقدارشان در طول زمان تغییر می‌کند. این راهنما نشان می‌دهد چطور یک form control پایه بسازید و به‌روزرسانی کنید، چند control را در یک group استفاده کنید، مقدارهای فرم را validate کنید و فرم‌های dynamic بسازید که بتوانید در runtime control اضافه یا حذف کنید.

نمای کلی reactive forms

Reactive forms برای مدیریت state فرم در یک نقطه‌ی مشخص از زمان، از رویکردی صریح و immutable استفاده می‌کنند. هر تغییر در form state یک state جدید برمی‌گرداند و این کار integrity مدل را بین تغییرات حفظ می‌کند. Reactive forms حول observable streamها ساخته شده‌اند؛ جایی که inputها و مقدارهای فرم به شکل streamهایی فراهم می‌شوند که می‌توان به‌صورت synchronous به آن‌ها دسترسی داشت.

Reactive forms مسیر ساده‌ای برای testing هم فراهم می‌کنند، چون مطمئن هستید داده‌ی شما هنگام درخواست، سازگار و قابل پیش‌بینی است. هر مصرف‌کننده‌ی این streamها می‌تواند داده را با خیال راحت manipulate کند.

Reactive forms از template-driven forms به شکل‌های مشخصی متفاوت‌اند. Reactive forms دسترسی synchronous به data model، immutability با observable operatorها و change tracking از طریق observable streamها فراهم می‌کنند.

Template-driven forms اجازه می‌دهند داده را مستقیم در template تغییر دهید، اما از reactive forms کمتر explicit هستند، چون به directiveهای embedded در template و داده‌ی mutable برای دنبال کردن تغییرات به‌صورت asynchronous تکیه می‌کنند. برای مقایسه‌ی دقیق این دو paradigm، Forms Overview را ببینید.

اضافه کردن یک form control پایه

برای استفاده از form controlها سه مرحله وجود دارد.

  1. یک component جدید generate کنید و reactive forms module را register کنید. این module directiveهای reactive-form لازم برای استفاده از reactive forms را declare می‌کند.
  2. یک FormControl جدید instantiate کنید.
  3. FormControl را در template register کنید.

سپس می‌توانید با اضافه کردن component به template، فرم را نمایش دهید.

مثال‌های زیر نشان می‌دهند چطور یک form control واحد اضافه کنید. در مثال، کاربر نام خود را در یک input field وارد می‌کند، مقدار input capture می‌شود و مقدار فعلی form control element نمایش داده می‌شود.

از command مربوط به CLI یعنی ng generate component برای generate کردن component در پروژه استفاده کنید، ReactiveFormsModule را از package مربوط به @angular/forms import کنید و آن را به آرایه‌ی imports در Component اضافه کنید.

name-editor.component.ts (excerpt)
// #docplaster
// #docregion create-control
import {Component} from '@angular/core';

// #docregion imports
import {FormControl, ReactiveFormsModule} from '@angular/forms';

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

  // #docregion update-value
  updateName() {
    this.name.setValue('Nancy');
  }
  // #enddocregion update-value
  // #docregion create-control
}
// #enddocregion create-control

از constructor مربوط به FormControl برای تنظیم مقدار اولیه استفاده کنید؛ در این مورد یک string خالی. با ساخت این controlها در کلاس component، بلافاصله به گوش دادن، به‌روزرسانی و validate کردن state مربوط به input فرم دسترسی دارید.

name-editor.component.ts
// #docplaster
// #docregion create-control
import {Component} from '@angular/core';

// #docregion imports
import {FormControl, ReactiveFormsModule} from '@angular/forms';

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

  // #docregion update-value
  updateName() {
    this.name.setValue('Nancy');
  }
  // #enddocregion update-value
  // #docregion create-control
}
// #enddocregion create-control

بعد از ساخت control در کلاس component، باید آن را با یک form control element در template مرتبط کنید. template را با form control به‌روزرسانی کنید؛ با binding مربوط به formControl که توسط FormControlDirective فراهم می‌شود و آن هم در ReactiveFormsModule قرار دارد.

name-editor.component.html
<!-- #docregion control-binding -->
<label for="name">Name: </label>
<input id="name" type="text" [formControl]="name" />
<!-- #enddocregion control-binding -->

<!-- #docregion display-value -->
<p>Value: {{ name.value }}</p>
<!-- #enddocregion display-value -->

<!-- #docregion update-value -->
<button type="button" (click)="updateName()">Update Name</button>
<!-- #enddocregion update-value -->

با استفاده از syntax مربوط به template binding، form control حالا روی input element به نام name در template register شده است. form control و DOM element با هم ارتباط دارند: view تغییرات model را منعکس می‌کند و model تغییرات view را.

وقتی component مربوط به <app-name-editor> به یک template اضافه شود، FormControlای که به property مربوط به name اختصاص داده شده نمایش داده می‌شود.

app.component.html (name editor)
<!-- #docplaster -->
<h1>Reactive Forms</h1>

<!-- #docregion app-name-editor-->
<app-name-editor />
<!-- #enddocregion app-name-editor-->

<!-- #docregion app-profile-editor -->
<app-profile-editor />
<!-- #enddocregion app-profile-editor -->

نمایش مقدار form control

می‌توانید مقدار را به روش‌های زیر نمایش دهید:

  • از طریق observable مربوط به valueChanges، که با آن می‌توانید در template با AsyncPipe یا در کلاس component با متد subscribe() به تغییرات value فرم گوش دهید.
  • با property مربوط به value، که یک snapshot از مقدار فعلی به شما می‌دهد.

مثال زیر نشان می‌دهد چطور مقدار فعلی را با interpolation در template نمایش دهید.

name-editor.component.html (control value)
<!-- #docregion control-binding -->
<label for="name">Name: </label>
<input id="name" type="text" [formControl]="name" />
<!-- #enddocregion control-binding -->

<!-- #docregion display-value -->
<p>Value: {{ name.value }}</p>
<!-- #enddocregion display-value -->

<!-- #docregion update-value -->
<button type="button" (click)="updateName()">Update Name</button>
<!-- #enddocregion update-value -->

مقدار نمایش‌داده‌شده با به‌روزرسانی form control element تغییر می‌کند.

Reactive forms از طریق propertyها و methodهایی که هر instance فراهم می‌کند، به اطلاعات مربوط به یک control مشخص دسترسی می‌دهد. این propertyها و methodهای کلاس زیرین AbstractControl برای کنترل form state و تعیین زمان نمایش پیام‌ها هنگام مدیریت input validation استفاده می‌شوند.

درباره‌ی propertyها و methodهای دیگر FormControl در API Reference بخوانید.

جایگزین کردن مقدار form control

Reactive forms methodهایی برای تغییر programmatic مقدار control دارند؛ این به شما انعطاف می‌دهد بدون تعامل کاربر مقدار را به‌روزرسانی کنید. یک form control instance متد setValue() را فراهم می‌کند که مقدار form control را به‌روزرسانی می‌کند و ساختار مقدار ارائه‌شده را در برابر ساختار control validate می‌کند. مثلا وقتی داده‌ی فرم را از backend API یا service می‌گیرید، از setValue() استفاده کنید تا control را به مقدار جدیدش به‌روزرسانی و مقدار قبلی را کامل جایگزین کنید.

مثال زیر متدی به کلاس component اضافه می‌کند تا مقدار control را با متد setValue() به Nancy تغییر دهد.

name-editor.component.ts (update value)
// #docplaster
// #docregion create-control
import {Component} from '@angular/core';

// #docregion imports
import {FormControl, ReactiveFormsModule} from '@angular/forms';

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

  // #docregion update-value
  updateName() {
    this.name.setValue('Nancy');
  }
  // #enddocregion update-value
  // #docregion create-control
}
// #enddocregion create-control

template را با یک button به‌روزرسانی کنید تا update کردن name را شبیه‌سازی کند. وقتی روی دکمه‌ی Update Name کلیک می‌کنید، مقدار واردشده در form control element به‌عنوان مقدار فعلی آن منعکس می‌شود.

name-editor.component.html (update value)
<!-- #docregion control-binding -->
<label for="name">Name: </label>
<input id="name" type="text" [formControl]="name" />
<!-- #enddocregion control-binding -->

<!-- #docregion display-value -->
<p>Value: {{ name.value }}</p>
<!-- #enddocregion display-value -->

<!-- #docregion update-value -->
<button type="button" (click)="updateName()">Update Name</button>
<!-- #enddocregion update-value -->

form model منبع حقیقت control است؛ بنابراین وقتی روی button کلیک می‌کنید، مقدار input داخل کلاس component تغییر می‌کند و مقدار فعلی آن را override می‌کند.

گروه‌بندی form controlها

فرم‌ها معمولا چند control مرتبط دارند. Reactive forms دو راه برای گروه‌بندی چند control مرتبط در یک input form واحد فراهم می‌کنند.

Form groupsجزئیات
Form groupفرمی با مجموعه‌ای ثابت از controlها تعریف می‌کند که می‌توانید آن‌ها را با هم مدیریت کنید. مبانی form group در همین بخش بررسی می‌شود. همچنین می‌توانید برای ساخت فرم‌های پیچیده‌تر form groupها را nest کنید.
Form arrayیک فرم dynamic تعریف می‌کند که می‌توانید در runtime control اضافه یا حذف کنید. برای ساخت فرم‌های پیچیده‌تر می‌توانید form arrayها را هم nest کنید. برای اطلاعات بیشتر درباره‌ی این گزینه، Creating dynamic forms را ببینید.

همان‌طور که یک form control instance به شما کنترل یک input field واحد را می‌دهد، یک form group instance هم form state مجموعه‌ای از form control instanceها، مثلا یک فرم، را دنبال می‌کند. هر control در یک form group instance هنگام ساخت form group با name دنبال می‌شود. مثال زیر نشان می‌دهد چطور چند form control instance را در یک group واحد مدیریت کنید.

یک component به نام ProfileEditor generate کنید و کلاس‌های FormGroup و FormControl را از package مربوط به @angular/forms import کنید.

shell
ng generate component ProfileEditor
profile-editor.component.ts (imports)
// #docplaster
// #docregion formgroup, nested-formgroup
import {Component} from '@angular/core';
// #docregion imports
import {FormGroup, FormControl, ReactiveFormsModule} from '@angular/forms';

@Component({
  selector: 'app-profile-editor',
  templateUrl: './profile-editor.component.html',
  styleUrls: ['./profile-editor.component.css'],
  imports: [ReactiveFormsModule],
})
export class ProfileEditorComponent {
  // #enddocregion imports
  // #docregion formgroup-compare
  profileForm = new FormGroup({
    firstName: new FormControl(''),
    lastName: new FormControl(''),
    // #enddocregion formgroup
    address: new FormGroup({
      street: new FormControl(''),
      city: new FormControl(''),
      state: new FormControl(''),
      zip: new FormControl(''),
    }),
    // #docregion formgroup
  });
  // #enddocregion formgroup, nested-formgroup, formgroup-compare
  // #docregion patch-value
  updateProfile() {
    this.profileForm.patchValue({
      firstName: 'Nancy',
      address: {
        street: '123 Drew Street',
      },
    });
  }
  // #enddocregion patch-value
  // #docregion formgroup, nested-formgroup
}
// #enddocregion formgroup

برای اضافه کردن form group به این component، مرحله‌های زیر را انجام دهید.

  1. یک instance از FormGroup بسازید.
  2. model و view مربوط به FormGroup را مرتبط کنید.
  3. داده‌ی فرم را ذخیره کنید.

در کلاس component یک property به نام profileForm بسازید و آن را روی یک form group instance جدید قرار دهید. برای initialize کردن form group، به constructor یک object از keyهای نام‌دار بدهید که به controlهایشان map شده‌اند.

برای profile form، دو form control instance با نام‌های firstName و lastName اضافه کنید.

profile-editor.component.ts (form group)
// #docplaster
// #docregion formgroup, nested-formgroup
import {Component} from '@angular/core';
// #docregion imports
import {FormGroup, FormControl, ReactiveFormsModule} from '@angular/forms';

@Component({
  selector: 'app-profile-editor',
  templateUrl: './profile-editor.component.html',
  styleUrls: ['./profile-editor.component.css'],
  imports: [ReactiveFormsModule],
})
export class ProfileEditorComponent {
  // #enddocregion imports
  // #docregion formgroup-compare
  profileForm = new FormGroup({
    firstName: new FormControl(''),
    lastName: new FormControl(''),
    // #enddocregion formgroup
    address: new FormGroup({
      street: new FormControl(''),
      city: new FormControl(''),
      state: new FormControl(''),
      zip: new FormControl(''),
    }),
    // #docregion formgroup
  });
  // #enddocregion formgroup, nested-formgroup, formgroup-compare
  // #docregion patch-value
  updateProfile() {
    this.profileForm.patchValue({
      firstName: 'Nancy',
      address: {
        street: '123 Drew Street',
      },
    });
  }
  // #enddocregion patch-value
  // #docregion formgroup, nested-formgroup
}
// #enddocregion formgroup

form controlهای جداگانه حالا داخل یک group جمع شده‌اند. یک instance از FormGroup مقدار model خودش را به‌صورت objectای فراهم می‌کند که از مقدارهای هر control در group reduce شده است. یک form group instance همان propertyها، مثل value و untouched، و همان methodها، مثل setValue()، را دارد که یک form control instance دارد.

یک form group وضعیت و تغییرات هر control خودش را دنبال می‌کند؛ بنابراین اگر یکی از controlها تغییر کند، parent control هم status یا value change جدیدی emit می‌کند. model مربوط به group از memberهای آن نگه‌داری می‌شود. بعد از تعریف model، باید template را به‌روزرسانی کنید تا model را در view منعکس کند.

profile-editor.component.html (template form group)
<!-- #docplaster -->
<!-- #docregion 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" />

  <!-- #enddocregion formgroup -->
  <!-- #docregion formgroupname -->
  <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>
  <!-- #enddocregion formgroupname -->

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

    @for (alias of aliases.controls; track $index; let i = $index) {
      <div>
        <!-- The repeated alias template -->
        <label for="alias-{{ i }}">Alias: </label>
        <input id="alias-{{ i }}" type="text" [formControlName]="i" />
      </div>
    }
  </div>
  <!-- #docregion formgroup -->
</form>
<!-- #enddocregion formgroup -->

<p>Form Value: {{ profileForm.value | json }}</p>

<!-- #docregion patch-value -->
<button type="button" (click)="updateProfile()">Update Profile</button>
<!-- #enddocregion patch-value -->

همان‌طور که یک form group شامل گروهی از controlهاست، FormGroup مربوط به profileForm با directive مربوط به FormGroup به element مربوط به form bind می‌شود و یک لایه‌ی ارتباطی بین model و formی که inputها را دارد ایجاد می‌کند. input مربوط به formControlName که توسط directive مربوط به FormControlName فراهم می‌شود، هر input جداگانه را به form control تعریف‌شده در FormGroup bind می‌کند. form controlها با elementهای متناظر خود ارتباط دارند. همچنین تغییرات را به form group instance منتقل می‌کنند، که منبع حقیقت برای مقدار model را فراهم می‌کند.

component مربوط به ProfileEditor input را از کاربر می‌پذیرد، اما در یک سناریوی واقعی می‌خواهید مقدار فرم را capture کنید و آن را برای پردازش بیشتر بیرون از component در دسترس بگذارید. directive مربوط به FormGroup به event مربوط به submit که توسط element فرم emit می‌شود گوش می‌دهد و eventای به نام ngSubmit emit می‌کند که می‌توانید به یک callback function bind کنید. یک event listener از نوع ngSubmit با callback method مربوط به onSubmit() به tag فرم اضافه کنید.

profile-editor.component.html (submit event)
<!-- #docplaster -->
<!-- #docregion ng-submit -->
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
  <!-- #enddocregion ng-submit -->
  <label for="first-name">First Name: </label>
  <input id="first-name" type="text" formControlName="firstName" required />

  <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>

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

    @for (alias of aliases.controls; track $index; let i = $index) {
      <div>
        <!-- The repeated alias template -->
        <label for="alias-{{ i }}">Alias:</label>
        <input id="alias-{{ i }}" type="text" [formControlName]="i" />
      </div>
    }
  </div>
  <!-- #enddocregion formarrayname -->

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

<hr />

<p>Form Value: {{ profileForm.value | json }}</p>

<!-- #docregion display-status -->
<p>Form Status: {{ profileForm.status }}</p>
<!-- #enddocregion display-status -->

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

متد onSubmit() در component مربوط به ProfileEditor مقدار فعلی profileForm را capture می‌کند. از output() استفاده کنید تا فرم encapsulated بماند و مقدار فرم بیرون از component فراهم شود. مثال زیر از console.warn برای log کردن پیام در browser console استفاده می‌کند.

profile-editor.component.ts (submit method)
// #docplaster
import {Component, inject} from '@angular/core';
import {FormBuilder, ReactiveFormsModule} from '@angular/forms';
// #docregion validator-imports
import {Validators} from '@angular/forms';
// #enddocregion validator-imports
import {FormArray} from '@angular/forms';
import {JsonPipe} from '@angular/common';

@Component({
  selector: 'app-profile-editor',
  templateUrl: './profile-editor.component.html',
  styleUrls: ['./profile-editor.component.css'],
  imports: [ReactiveFormsModule, JsonPipe],
})
export class ProfileEditorComponent {
  // #docregion required-validator, aliases
  private formBuilder = inject(FormBuilder);

  profileForm = this.formBuilder.group({
    firstName: ['', Validators.required],
    lastName: [''],
    address: this.formBuilder.group({
      street: [''],
      city: [''],
      state: [''],
      zip: [''],
    }),
    // #enddocregion required-validator
    aliases: this.formBuilder.array([this.formBuilder.control('')]),
    // #docregion required-validator
  });
  // #enddocregion required-validator, aliases
  // #docregion aliases-getter
  get aliases() {
    return this.profileForm.get('aliases') as FormArray;
  }
  // #enddocregion aliases-getter

  updateProfile() {
    this.profileForm.patchValue({
      firstName: 'Nancy',
      address: {
        street: '123 Drew Street',
      },
    });
  }
  // #docregion add-alias
  addAlias() {
    this.aliases.push(this.formBuilder.control(''));
  }
  // #enddocregion add-alias
  // #docregion on-submit
  onSubmit() {
    // TODO: Use output() with form value
    console.warn(this.profileForm.value);
  }
  // #enddocregion on-submit
}

event مربوط به submit توسط tag فرم با استفاده از built-in DOM event emit می‌شود. شما با کلیک روی buttonای با type برابر submit این event را trigger می‌کنید. این امکان را می‌دهد که کاربر با فشردن کلید Enter فرم کامل‌شده را submit کند.

از یک element از نوع button استفاده کنید تا buttonی به پایین فرم اضافه شود و form submission را trigger کند.

profile-editor.component.html (submit button)
<!-- #docplaster -->
<!-- #docregion ng-submit -->
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
  <!-- #enddocregion ng-submit -->
  <label for="first-name">First Name: </label>
  <input id="first-name" type="text" formControlName="firstName" required />

  <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>

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

    @for (alias of aliases.controls; track $index; let i = $index) {
      <div>
        <!-- The repeated alias template -->
        <label for="alias-{{ i }}">Alias:</label>
        <input id="alias-{{ i }}" type="text" [formControlName]="i" />
      </div>
    }
  </div>
  <!-- #enddocregion formarrayname -->

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

<hr />

<p>Form Value: {{ profileForm.value | json }}</p>

<!-- #docregion display-status -->
<p>Form Status: {{ profileForm.status }}</p>
<!-- #enddocregion display-status -->

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

button در snippet قبلی همچنین یک binding مربوط به disabled دارد تا وقتی profileForm نامعتبر است button را disabled کند. هنوز هیچ validationای انجام نمی‌دهید، بنابراین button همیشه enabled است. form validation پایه در بخش Validating form input پوشش داده می‌شود.

برای نمایش component مربوط به ProfileEditor که فرم را در خود دارد، آن را به template یک component اضافه کنید.

app.component.html (profile editor)
<!-- #docplaster -->
<h1>Reactive Forms</h1>

<!-- #docregion app-name-editor-->
<app-name-editor />
<!-- #enddocregion app-name-editor-->

<!-- #docregion app-profile-editor -->
<app-profile-editor />
<!-- #enddocregion app-profile-editor -->

ProfileEditor به شما اجازه می‌دهد form control instanceهای مربوط به controlهای firstName و lastName را داخل form group instance مدیریت کنید.

ساخت form groupهای تو در تو

Form groupها می‌توانند هم form control instanceهای جداگانه و هم form group instanceهای دیگر را به‌عنوان child بپذیرند. این کار compose کردن form modelهای پیچیده را ساده‌تر و نگهداری آن‌ها را منطقی‌تر می‌کند.

هنگام ساخت فرم‌های پیچیده، مدیریت areaهای مختلف اطلاعات در بخش‌های کوچک‌تر ساده‌تر است. استفاده از یک nested form group instance اجازه می‌دهد form groupهای بزرگ را به بخش‌های کوچک‌تر و قابل مدیریت‌تر بشکنید.

برای ساخت فرم‌های پیچیده‌تر، مرحله‌های زیر را انجام دهید.

  1. یک nested group بسازید.
  2. nested form را در template group کنید.

بعضی نوع‌های اطلاعات به‌طور طبیعی در یک group قرار می‌گیرند. name و address نمونه‌های معمول چنین nested groupهایی هستند و در مثال‌های زیر استفاده می‌شوند.

برای ساخت یک nested group در profileForm، یک element تو در تو به نام address به form group instance اضافه کنید.

profile-editor.component.ts (nested form group)
// #docplaster
// #docregion formgroup, nested-formgroup
import {Component} from '@angular/core';
// #docregion imports
import {FormGroup, FormControl, ReactiveFormsModule} from '@angular/forms';

@Component({
  selector: 'app-profile-editor',
  templateUrl: './profile-editor.component.html',
  styleUrls: ['./profile-editor.component.css'],
  imports: [ReactiveFormsModule],
})
export class ProfileEditorComponent {
  // #enddocregion imports
  // #docregion formgroup-compare
  profileForm = new FormGroup({
    firstName: new FormControl(''),
    lastName: new FormControl(''),
    // #enddocregion formgroup
    address: new FormGroup({
      street: new FormControl(''),
      city: new FormControl(''),
      state: new FormControl(''),
      zip: new FormControl(''),
    }),
    // #docregion formgroup
  });
  // #enddocregion formgroup, nested-formgroup, formgroup-compare
  // #docregion patch-value
  updateProfile() {
    this.profileForm.patchValue({
      firstName: 'Nancy',
      address: {
        street: '123 Drew Street',
      },
    });
  }
  // #enddocregion patch-value
  // #docregion formgroup, nested-formgroup
}
// #enddocregion formgroup

در این مثال، address group کنترل‌های فعلی firstName و lastName را با کنترل‌های جدید street، city، state و zip ترکیب می‌کند. با اینکه element مربوط به address در form group فرزند element کلی profileForm در form group است، همان ruleها درباره‌ی تغییرات value و status اعمال می‌شوند. تغییرات status و value از nested form group به parent form group propagate می‌شوند و consistency با model کلی را حفظ می‌کنند.

بعد از به‌روزرسانی model در کلاس component، template را به‌روزرسانی کنید تا form group instance و input elementهای آن را وصل کند. form group مربوط به address را که شامل fieldهای street، city، state و zip است به template مربوط به ProfileEditor اضافه کنید.

profile-editor.component.html (template nested form group)
<!-- #docplaster -->
<!-- #docregion 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" />

  <!-- #enddocregion formgroup -->
  <!-- #docregion formgroupname -->
  <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>
  <!-- #enddocregion formgroupname -->

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

    @for (alias of aliases.controls; track $index; let i = $index) {
      <div>
        <!-- The repeated alias template -->
        <label for="alias-{{ i }}">Alias: </label>
        <input id="alias-{{ i }}" type="text" [formControlName]="i" />
      </div>
    }
  </div>
  <!-- #docregion formgroup -->
</form>
<!-- #enddocregion formgroup -->

<p>Form Value: {{ profileForm.value | json }}</p>

<!-- #docregion patch-value -->
<button type="button" (click)="updateProfile()">Update Profile</button>
<!-- #enddocregion patch-value -->

فرم ProfileEditor به‌صورت یک group نمایش داده می‌شود، اما model برای نمایش areaهای grouping منطقی، بیشتر شکسته می‌شود.

مقدار form group instance را در component template با استفاده از property مربوط به value و JsonPipe نمایش دهید.

به‌روزرسانی بخش‌هایی از data model

هنگام به‌روزرسانی مقدار یک form group instance که چند control دارد، ممکن است بخواهید فقط بخش‌هایی از model را به‌روزرسانی کنید. این بخش پوشش می‌دهد چطور بخش‌های مشخصی از form control data model را به‌روزرسانی کنید.

دو راه برای به‌روزرسانی model value وجود دارد:

Methodsجزئیات
setValue()مقدار جدیدی برای یک control جداگانه set می‌کند. متد setValue() به‌صورت strict به ساختار form group پایبند است و کل مقدار control را جایگزین می‌کند.
patchValue()هر property تعریف‌شده در object را که در form model تغییر کرده است جایگزین می‌کند.

checkهای strict متد setValue() کمک می‌کنند خطاهای nesting را در فرم‌های پیچیده پیدا کنید، در حالی که patchValue() در برابر آن خطاها بی‌صدا fail می‌شود.

در ProfileEditorComponent، از متد updateProfile همراه با مثال زیر استفاده کنید تا first name و street address کاربر را به‌روزرسانی کنید.

profile-editor.component.ts (patch value)
// #docplaster
// #docregion formgroup, nested-formgroup
import {Component} from '@angular/core';
// #docregion imports
import {FormGroup, FormControl, ReactiveFormsModule} from '@angular/forms';

@Component({
  selector: 'app-profile-editor',
  templateUrl: './profile-editor.component.html',
  styleUrls: ['./profile-editor.component.css'],
  imports: [ReactiveFormsModule],
})
export class ProfileEditorComponent {
  // #enddocregion imports
  // #docregion formgroup-compare
  profileForm = new FormGroup({
    firstName: new FormControl(''),
    lastName: new FormControl(''),
    // #enddocregion formgroup
    address: new FormGroup({
      street: new FormControl(''),
      city: new FormControl(''),
      state: new FormControl(''),
      zip: new FormControl(''),
    }),
    // #docregion formgroup
  });
  // #enddocregion formgroup, nested-formgroup, formgroup-compare
  // #docregion patch-value
  updateProfile() {
    this.profileForm.patchValue({
      firstName: 'Nancy',
      address: {
        street: '123 Drew Street',
      },
    });
  }
  // #enddocregion patch-value
  // #docregion formgroup, nested-formgroup
}
// #enddocregion formgroup

با اضافه کردن button به template، یک update را شبیه‌سازی کنید تا user profile در صورت نیاز به‌روزرسانی شود.

profile-editor.component.html (update value)
<!-- #docplaster -->
<!-- #docregion 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" />

  <!-- #enddocregion formgroup -->
  <!-- #docregion formgroupname -->
  <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>
  <!-- #enddocregion formgroupname -->

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

    @for (alias of aliases.controls; track $index; let i = $index) {
      <div>
        <!-- The repeated alias template -->
        <label for="alias-{{ i }}">Alias: </label>
        <input id="alias-{{ i }}" type="text" [formControlName]="i" />
      </div>
    }
  </div>
  <!-- #docregion formgroup -->
</form>
<!-- #enddocregion formgroup -->

<p>Form Value: {{ profileForm.value | json }}</p>

<!-- #docregion patch-value -->
<button type="button" (click)="updateProfile()">Update Profile</button>
<!-- #enddocregion patch-value -->

وقتی کاربر روی button کلیک می‌کند، model مربوط به profileForm با مقدارهای جدید برای firstName و street به‌روزرسانی می‌شود. توجه کنید street داخل objectی در property مربوط به address ارائه شده است. این لازم است چون متد patchValue() update را در برابر ساختار model اعمال می‌کند. patchValue() فقط propertyهایی را به‌روزرسانی می‌کند که form model تعریف کرده است.

استفاده از service مربوط به FormBuilder برای generate کردن controlها

ساخت دستی form control instanceها هنگام کار با چند فرم می‌تواند تکراری شود. service مربوط به FormBuilder methodهای راحتی برای generate کردن controlها فراهم می‌کند.

برای استفاده از این service، مرحله‌های زیر را انجام دهید.

  1. کلاس FormBuilder را import کنید.
  2. service مربوط به FormBuilder را inject کنید.
  3. محتوای فرم را generate کنید.

مثال‌های زیر نشان می‌دهند چطور component مربوط به ProfileEditor را refactor کنید تا از form builder service برای ساخت form control و form group instanceها استفاده کند.

کلاس FormBuilder را از package مربوط به @angular/forms import کنید.

profile-editor.component.ts (import)
// #docplaster

import {Component, inject} from '@angular/core';
// #docregion form-builder-imports
import {FormBuilder, ReactiveFormsModule} from '@angular/forms';
// #enddocregion form-builder-imports
// #docregion form-array-imports
import {FormArray} from '@angular/forms';
// #enddocregion form-array-imports

@Component({
  selector: 'app-profile-editor',
  templateUrl: './profile-editor.component.html',
  styleUrls: ['./profile-editor.component.css'],
  imports: [ReactiveFormsModule],
})
export class ProfileEditorComponent {
  // #docregion inject-form-builder
  private formBuilder = inject(FormBuilder);
  // #enddocregion inject-form-builder
  // #docregion formgroup-compare, form-builder
  profileForm = this.formBuilder.group({
    firstName: [''],
    lastName: [''],
    address: this.formBuilder.group({
      street: [''],
      city: [''],
      state: [''],
      zip: [''],
    }),
    // #enddocregion form-builder, formgroup-compare
    aliases: this.formBuilder.array([this.formBuilder.control('')]),
    // #docregion form-builder, formgroup-compare
  });
  // #enddocregion form-builder, formgroup-compare
  get aliases() {
    return this.profileForm.get('aliases') as FormArray;
  }

  updateProfile() {
    this.profileForm.patchValue({
      firstName: 'Nancy',
      address: {
        street: '123 Drew Street',
      },
    });
  }

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

service مربوط به FormBuilder یک injectable provider از reactive forms module است. از function مربوط به inject() استفاده کنید تا این dependency را در component خود inject کنید.

profile-editor.component.ts (property init)
// #docplaster

import {Component, inject} from '@angular/core';
// #docregion form-builder-imports
import {FormBuilder, ReactiveFormsModule} from '@angular/forms';
// #enddocregion form-builder-imports
// #docregion form-array-imports
import {FormArray} from '@angular/forms';
// #enddocregion form-array-imports

@Component({
  selector: 'app-profile-editor',
  templateUrl: './profile-editor.component.html',
  styleUrls: ['./profile-editor.component.css'],
  imports: [ReactiveFormsModule],
})
export class ProfileEditorComponent {
  // #docregion inject-form-builder
  private formBuilder = inject(FormBuilder);
  // #enddocregion inject-form-builder
  // #docregion formgroup-compare, form-builder
  profileForm = this.formBuilder.group({
    firstName: [''],
    lastName: [''],
    address: this.formBuilder.group({
      street: [''],
      city: [''],
      state: [''],
      zip: [''],
    }),
    // #enddocregion form-builder, formgroup-compare
    aliases: this.formBuilder.array([this.formBuilder.control('')]),
    // #docregion form-builder, formgroup-compare
  });
  // #enddocregion form-builder, formgroup-compare
  get aliases() {
    return this.profileForm.get('aliases') as FormArray;
  }

  updateProfile() {
    this.profileForm.patchValue({
      firstName: 'Nancy',
      address: {
        street: '123 Drew Street',
      },
    });
  }

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

service مربوط به FormBuilder سه method دارد: control()، group() و array(). این‌ها factory methodهایی برای generate کردن instanceها در کلاس‌های component شما هستند، از جمله form control، form group و form array. از method مربوط به group برای ساخت controlهای profileForm استفاده کنید.

profile-editor.component.ts (form builder)
// #docplaster

import {Component, inject} from '@angular/core';
// #docregion form-builder-imports
import {FormBuilder, ReactiveFormsModule} from '@angular/forms';
// #enddocregion form-builder-imports
// #docregion form-array-imports
import {FormArray} from '@angular/forms';
// #enddocregion form-array-imports

@Component({
  selector: 'app-profile-editor',
  templateUrl: './profile-editor.component.html',
  styleUrls: ['./profile-editor.component.css'],
  imports: [ReactiveFormsModule],
})
export class ProfileEditorComponent {
  // #docregion inject-form-builder
  private formBuilder = inject(FormBuilder);
  // #enddocregion inject-form-builder
  // #docregion formgroup-compare, form-builder
  profileForm = this.formBuilder.group({
    firstName: [''],
    lastName: [''],
    address: this.formBuilder.group({
      street: [''],
      city: [''],
      state: [''],
      zip: [''],
    }),
    // #enddocregion form-builder, formgroup-compare
    aliases: this.formBuilder.array([this.formBuilder.control('')]),
    // #docregion form-builder, formgroup-compare
  });
  // #enddocregion form-builder, formgroup-compare
  get aliases() {
    return this.profileForm.get('aliases') as FormArray;
  }

  updateProfile() {
    this.profileForm.patchValue({
      firstName: 'Nancy',
      address: {
        street: '123 Drew Street',
      },
    });
  }

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

در مثال قبلی، از method مربوط به group() با همان object استفاده می‌کنید تا propertyهای model را تعریف کنید. مقدار هر control name آرایه‌ای است که مقدار اولیه را به‌عنوان item اول در array دارد.

ts
// #docplaster
// #docregion formgroup, nested-formgroup
import {Component} from '@angular/core';
// #docregion imports
import {FormGroup, FormControl, ReactiveFormsModule} from '@angular/forms';

@Component({
  selector: 'app-profile-editor',
  templateUrl: './profile-editor.component.html',
  styleUrls: ['./profile-editor.component.css'],
  imports: [ReactiveFormsModule],
})
export class ProfileEditorComponent {
  // #enddocregion imports
  // #docregion formgroup-compare
  profileForm = new FormGroup({
    firstName: new FormControl(''),
    lastName: new FormControl(''),
    // #enddocregion formgroup
    address: new FormGroup({
      street: new FormControl(''),
      city: new FormControl(''),
      state: new FormControl(''),
      zip: new FormControl(''),
    }),
    // #docregion formgroup
  });
  // #enddocregion formgroup, nested-formgroup, formgroup-compare
  // #docregion patch-value
  updateProfile() {
    this.profileForm.patchValue({
      firstName: 'Nancy',
      address: {
        street: '123 Drew Street',
      },
    });
  }
  // #enddocregion patch-value
  // #docregion formgroup, nested-formgroup
}
// #enddocregion formgroup
ts
// #docplaster

import {Component, inject} from '@angular/core';
// #docregion form-builder-imports
import {FormBuilder, ReactiveFormsModule} from '@angular/forms';
// #enddocregion form-builder-imports
// #docregion form-array-imports
import {FormArray} from '@angular/forms';
// #enddocregion form-array-imports

@Component({
  selector: 'app-profile-editor',
  templateUrl: './profile-editor.component.html',
  styleUrls: ['./profile-editor.component.css'],
  imports: [ReactiveFormsModule],
})
export class ProfileEditorComponent {
  // #docregion inject-form-builder
  private formBuilder = inject(FormBuilder);
  // #enddocregion inject-form-builder
  // #docregion formgroup-compare, form-builder
  profileForm = this.formBuilder.group({
    firstName: [''],
    lastName: [''],
    address: this.formBuilder.group({
      street: [''],
      city: [''],
      state: [''],
      zip: [''],
    }),
    // #enddocregion form-builder, formgroup-compare
    aliases: this.formBuilder.array([this.formBuilder.control('')]),
    // #docregion form-builder, formgroup-compare
  });
  // #enddocregion form-builder, formgroup-compare
  get aliases() {
    return this.profileForm.get('aliases') as FormArray;
  }

  updateProfile() {
    this.profileForm.patchValue({
      firstName: 'Nancy',
      address: {
        street: '123 Drew Street',
      },
    });
  }

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

اعتبارسنجی input فرم

Form validation برای اطمینان از کامل و درست بودن input کاربر استفاده می‌شود. این بخش اضافه کردن یک validator واحد به form control و نمایش وضعیت کلی فرم را پوشش می‌دهد. form validation با جزئیات بیشتر در راهنمای Form Validation پوشش داده شده است.

برای اضافه کردن form validation، مرحله‌های زیر را انجام دهید.

  1. یک validator function را در form component خود import کنید.
  2. validator را به field در فرم اضافه کنید.
  3. logic لازم برای مدیریت validation status را اضافه کنید.

رایج‌ترین validation اجباری کردن یک field است. مثال زیر نشان می‌دهد چطور required validation را به control مربوط به firstName اضافه کنید و نتیجه‌ی validation را نمایش دهید.

Reactive forms مجموعه‌ای از validator functionها برای use caseهای رایج دارد. این functionها controlی را برای validate کردن دریافت می‌کنند و بر اساس check مربوط به validation، یک error object یا مقدار null برمی‌گردانند.

کلاس Validators را از package مربوط به @angular/forms import کنید.

profile-editor.component.ts (import)
// #docplaster
import {Component, inject} from '@angular/core';
import {FormBuilder, ReactiveFormsModule} from '@angular/forms';
// #docregion validator-imports
import {Validators} from '@angular/forms';
// #enddocregion validator-imports
import {FormArray} from '@angular/forms';
import {JsonPipe} from '@angular/common';

@Component({
  selector: 'app-profile-editor',
  templateUrl: './profile-editor.component.html',
  styleUrls: ['./profile-editor.component.css'],
  imports: [ReactiveFormsModule, JsonPipe],
})
export class ProfileEditorComponent {
  // #docregion required-validator, aliases
  private formBuilder = inject(FormBuilder);

  profileForm = this.formBuilder.group({
    firstName: ['', Validators.required],
    lastName: [''],
    address: this.formBuilder.group({
      street: [''],
      city: [''],
      state: [''],
      zip: [''],
    }),
    // #enddocregion required-validator
    aliases: this.formBuilder.array([this.formBuilder.control('')]),
    // #docregion required-validator
  });
  // #enddocregion required-validator, aliases
  // #docregion aliases-getter
  get aliases() {
    return this.profileForm.get('aliases') as FormArray;
  }
  // #enddocregion aliases-getter

  updateProfile() {
    this.profileForm.patchValue({
      firstName: 'Nancy',
      address: {
        street: '123 Drew Street',
      },
    });
  }
  // #docregion add-alias
  addAlias() {
    this.aliases.push(this.formBuilder.control(''));
  }
  // #enddocregion add-alias
  // #docregion on-submit
  onSubmit() {
    // TODO: Use output() with form value
    console.warn(this.profileForm.value);
  }
  // #enddocregion on-submit
}

در component مربوط به ProfileEditor، static method مربوط به Validators.required را به‌عنوان item دوم در array مربوط به control firstName اضافه کنید.

profile-editor.component.ts (required validator)
// #docplaster
import {Component, inject} from '@angular/core';
import {FormBuilder, ReactiveFormsModule} from '@angular/forms';
// #docregion validator-imports
import {Validators} from '@angular/forms';
// #enddocregion validator-imports
import {FormArray} from '@angular/forms';
import {JsonPipe} from '@angular/common';

@Component({
  selector: 'app-profile-editor',
  templateUrl: './profile-editor.component.html',
  styleUrls: ['./profile-editor.component.css'],
  imports: [ReactiveFormsModule, JsonPipe],
})
export class ProfileEditorComponent {
  // #docregion required-validator, aliases
  private formBuilder = inject(FormBuilder);

  profileForm = this.formBuilder.group({
    firstName: ['', Validators.required],
    lastName: [''],
    address: this.formBuilder.group({
      street: [''],
      city: [''],
      state: [''],
      zip: [''],
    }),
    // #enddocregion required-validator
    aliases: this.formBuilder.array([this.formBuilder.control('')]),
    // #docregion required-validator
  });
  // #enddocregion required-validator, aliases
  // #docregion aliases-getter
  get aliases() {
    return this.profileForm.get('aliases') as FormArray;
  }
  // #enddocregion aliases-getter

  updateProfile() {
    this.profileForm.patchValue({
      firstName: 'Nancy',
      address: {
        street: '123 Drew Street',
      },
    });
  }
  // #docregion add-alias
  addAlias() {
    this.aliases.push(this.formBuilder.control(''));
  }
  // #enddocregion add-alias
  // #docregion on-submit
  onSubmit() {
    // TODO: Use output() with form value
    console.warn(this.profileForm.value);
  }
  // #enddocregion on-submit
}

وقتی یک required field به form control اضافه می‌کنید، status اولیه‌ی آن invalid است. این invalid status به parent form group element propagate می‌شود و status آن را invalid می‌کند. به status فعلی form group instance از طریق property مربوط به status دسترسی پیدا کنید.

status فعلی profileForm را با interpolation نمایش دهید.

profile-editor.component.html (display status)
<!-- #docplaster -->
<!-- #docregion ng-submit -->
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
  <!-- #enddocregion ng-submit -->
  <label for="first-name">First Name: </label>
  <input id="first-name" type="text" formControlName="firstName" required />

  <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>

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

    @for (alias of aliases.controls; track $index; let i = $index) {
      <div>
        <!-- The repeated alias template -->
        <label for="alias-{{ i }}">Alias:</label>
        <input id="alias-{{ i }}" type="text" [formControlName]="i" />
      </div>
    }
  </div>
  <!-- #enddocregion formarrayname -->

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

<hr />

<p>Form Value: {{ profileForm.value | json }}</p>

<!-- #docregion display-status -->
<p>Form Status: {{ profileForm.status }}</p>
<!-- #enddocregion display-status -->

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

دکمه‌ی Submit disabled است چون profileForm به دلیل form control اجباری firstName نامعتبر است. بعد از پر کردن input مربوط به firstName، فرم معتبر می‌شود و دکمه‌ی Submit enabled می‌شود.

برای اطلاعات بیشتر درباره‌ی form validation، راهنمای Form Validation را ببینید.

ساخت فرم‌های dynamic

FormArray جایگزینی برای FormGroup است تا هر تعداد control بدون نام را مدیریت کنید. همانند form group instanceها، می‌توانید controlها را به‌صورت dynamic داخل form array instanceها insert و remove کنید، و مقدار form array instance و validation status آن از child controlهایش محاسبه می‌شود. اما لازم نیست برای هر control یک key با name تعریف کنید؛ پس اگر تعداد child valueها را از قبل نمی‌دانید، این گزینه بسیار مناسب است.

برای تعریف dynamic form، مرحله‌های زیر را انجام دهید.

  1. کلاس FormArray را import کنید.
  2. یک control از نوع FormArray تعریف کنید.
  3. با getter method به control مربوط به FormArray دسترسی پیدا کنید.
  4. form array را در template نمایش دهید.

مثال زیر نشان می‌دهد چطور arrayای از aliases را در ProfileEditor مدیریت کنید.

کلاس FormArray را از @angular/forms import کنید تا برای type information استفاده شود. service مربوط به FormBuilder آماده است تا یک instance از FormArray بسازد.

profile-editor.component.ts (import)
// #docplaster

import {Component, inject} from '@angular/core';
// #docregion form-builder-imports
import {FormBuilder, ReactiveFormsModule} from '@angular/forms';
// #enddocregion form-builder-imports
// #docregion form-array-imports
import {FormArray} from '@angular/forms';
// #enddocregion form-array-imports

@Component({
  selector: 'app-profile-editor',
  templateUrl: './profile-editor.component.html',
  styleUrls: ['./profile-editor.component.css'],
  imports: [ReactiveFormsModule],
})
export class ProfileEditorComponent {
  // #docregion inject-form-builder
  private formBuilder = inject(FormBuilder);
  // #enddocregion inject-form-builder
  // #docregion formgroup-compare, form-builder
  profileForm = this.formBuilder.group({
    firstName: [''],
    lastName: [''],
    address: this.formBuilder.group({
      street: [''],
      city: [''],
      state: [''],
      zip: [''],
    }),
    // #enddocregion form-builder, formgroup-compare
    aliases: this.formBuilder.array([this.formBuilder.control('')]),
    // #docregion form-builder, formgroup-compare
  });
  // #enddocregion form-builder, formgroup-compare
  get aliases() {
    return this.profileForm.get('aliases') as FormArray;
  }

  updateProfile() {
    this.profileForm.patchValue({
      firstName: 'Nancy',
      address: {
        street: '123 Drew Street',
      },
    });
  }

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

می‌توانید form array را با هر تعداد control، از صفر تا تعداد زیاد، با تعریف آن‌ها در یک array initialize کنید. برای تعریف form array، یک property به نام aliases به form group instance مربوط به profileForm اضافه کنید.

از متد FormBuilder.array() برای تعریف array و از متد FormBuilder.control() برای پر کردن array با یک control اولیه استفاده کنید.

profile-editor.component.ts (aliases form array)
// #docplaster
import {Component, inject} from '@angular/core';
import {FormBuilder, ReactiveFormsModule} from '@angular/forms';
// #docregion validator-imports
import {Validators} from '@angular/forms';
// #enddocregion validator-imports
import {FormArray} from '@angular/forms';
import {JsonPipe} from '@angular/common';

@Component({
  selector: 'app-profile-editor',
  templateUrl: './profile-editor.component.html',
  styleUrls: ['./profile-editor.component.css'],
  imports: [ReactiveFormsModule, JsonPipe],
})
export class ProfileEditorComponent {
  // #docregion required-validator, aliases
  private formBuilder = inject(FormBuilder);

  profileForm = this.formBuilder.group({
    firstName: ['', Validators.required],
    lastName: [''],
    address: this.formBuilder.group({
      street: [''],
      city: [''],
      state: [''],
      zip: [''],
    }),
    // #enddocregion required-validator
    aliases: this.formBuilder.array([this.formBuilder.control('')]),
    // #docregion required-validator
  });
  // #enddocregion required-validator, aliases
  // #docregion aliases-getter
  get aliases() {
    return this.profileForm.get('aliases') as FormArray;
  }
  // #enddocregion aliases-getter

  updateProfile() {
    this.profileForm.patchValue({
      firstName: 'Nancy',
      address: {
        street: '123 Drew Street',
      },
    });
  }
  // #docregion add-alias
  addAlias() {
    this.aliases.push(this.formBuilder.control(''));
  }
  // #enddocregion add-alias
  // #docregion on-submit
  onSubmit() {
    // TODO: Use output() with form value
    console.warn(this.profileForm.value);
  }
  // #enddocregion on-submit
}

control مربوط به aliases در form group instance حالا با یک control واحد پر شده است تا زمانی که controlهای بیشتری به‌صورت dynamic اضافه شوند.

یک getter در مقایسه با تکرار متد profileForm.get() برای گرفتن هر instance، دسترسی به aliases در form array instance را فراهم می‌کند. form array instance تعداد نامشخصی از controlها را در یک array نمایش می‌دهد. دسترسی به control از طریق getter راحت است و این رویکرد برای controlهای بیشتر هم مستقیم قابل تکرار است.

از getter syntax استفاده کنید تا class propertyای به نام aliases بسازید و form array control مربوط به alias را از parent form group دریافت کنید.

profile-editor.component.ts (aliases getter)
// #docplaster
import {Component, inject} from '@angular/core';
import {FormBuilder, ReactiveFormsModule} from '@angular/forms';
// #docregion validator-imports
import {Validators} from '@angular/forms';
// #enddocregion validator-imports
import {FormArray} from '@angular/forms';
import {JsonPipe} from '@angular/common';

@Component({
  selector: 'app-profile-editor',
  templateUrl: './profile-editor.component.html',
  styleUrls: ['./profile-editor.component.css'],
  imports: [ReactiveFormsModule, JsonPipe],
})
export class ProfileEditorComponent {
  // #docregion required-validator, aliases
  private formBuilder = inject(FormBuilder);

  profileForm = this.formBuilder.group({
    firstName: ['', Validators.required],
    lastName: [''],
    address: this.formBuilder.group({
      street: [''],
      city: [''],
      state: [''],
      zip: [''],
    }),
    // #enddocregion required-validator
    aliases: this.formBuilder.array([this.formBuilder.control('')]),
    // #docregion required-validator
  });
  // #enddocregion required-validator, aliases
  // #docregion aliases-getter
  get aliases() {
    return this.profileForm.get('aliases') as FormArray;
  }
  // #enddocregion aliases-getter

  updateProfile() {
    this.profileForm.patchValue({
      firstName: 'Nancy',
      address: {
        street: '123 Drew Street',
      },
    });
  }
  // #docregion add-alias
  addAlias() {
    this.aliases.push(this.formBuilder.control(''));
  }
  // #enddocregion add-alias
  // #docregion on-submit
  onSubmit() {
    // TODO: Use output() with form value
    console.warn(this.profileForm.value);
  }
  // #enddocregion on-submit
}

چون control برگشتی از type مربوط به AbstractControl است، باید type صریحی ارائه کنید تا به method syntax مربوط به form array instance دسترسی داشته باشید. متدی تعریف کنید که یک alias control را به‌صورت dynamic داخل form array مربوط به alias insert کند. متد FormArray.push()، control را به‌عنوان item جدید در array insert می‌کند، و می‌توانید arrayای از controlها را هم به FormArray.push() پاس دهید تا چند control را یک‌جا register کنید.

profile-editor.component.ts (add alias)
// #docplaster
import {Component, inject} from '@angular/core';
import {FormBuilder, ReactiveFormsModule} from '@angular/forms';
// #docregion validator-imports
import {Validators} from '@angular/forms';
// #enddocregion validator-imports
import {FormArray} from '@angular/forms';
import {JsonPipe} from '@angular/common';

@Component({
  selector: 'app-profile-editor',
  templateUrl: './profile-editor.component.html',
  styleUrls: ['./profile-editor.component.css'],
  imports: [ReactiveFormsModule, JsonPipe],
})
export class ProfileEditorComponent {
  // #docregion required-validator, aliases
  private formBuilder = inject(FormBuilder);

  profileForm = this.formBuilder.group({
    firstName: ['', Validators.required],
    lastName: [''],
    address: this.formBuilder.group({
      street: [''],
      city: [''],
      state: [''],
      zip: [''],
    }),
    // #enddocregion required-validator
    aliases: this.formBuilder.array([this.formBuilder.control('')]),
    // #docregion required-validator
  });
  // #enddocregion required-validator, aliases
  // #docregion aliases-getter
  get aliases() {
    return this.profileForm.get('aliases') as FormArray;
  }
  // #enddocregion aliases-getter

  updateProfile() {
    this.profileForm.patchValue({
      firstName: 'Nancy',
      address: {
        street: '123 Drew Street',
      },
    });
  }
  // #docregion add-alias
  addAlias() {
    this.aliases.push(this.formBuilder.control(''));
  }
  // #enddocregion add-alias
  // #docregion on-submit
  onSubmit() {
    // TODO: Use output() with form value
    console.warn(this.profileForm.value);
  }
  // #enddocregion on-submit
}

در template، هر control به‌عنوان input field جداگانه نمایش داده می‌شود.

برای وصل کردن aliases از form model خود، باید آن را به template اضافه کنید. شبیه input مربوط به formGroupName که توسط FormGroupNameDirective فراهم می‌شود، formArrayName ارتباط را از form array instance به template با FormArrayNameDirective bind می‌کند.

HTML template زیر را بعد از <div>ای که element مربوط به formGroupName را می‌بندد اضافه کنید.

profile-editor.component.html (aliases form array template)
<!-- #docplaster -->
<!-- #docregion ng-submit -->
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
  <!-- #enddocregion ng-submit -->
  <label for="first-name">First Name: </label>
  <input id="first-name" type="text" formControlName="firstName" required />

  <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>

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

    @for (alias of aliases.controls; track $index; let i = $index) {
      <div>
        <!-- The repeated alias template -->
        <label for="alias-{{ i }}">Alias:</label>
        <input id="alias-{{ i }}" type="text" [formControlName]="i" />
      </div>
    }
  </div>
  <!-- #enddocregion formarrayname -->

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

<hr />

<p>Form Value: {{ profileForm.value | json }}</p>

<!-- #docregion display-status -->
<p>Form Status: {{ profileForm.status }}</p>
<!-- #enddocregion display-status -->

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

block مربوط به @for روی هر form control instance فراهم‌شده توسط aliases form array instance iterate می‌کند. چون elementهای form array نام ندارند، index را به متغیر i assign می‌کنید و آن را به هر control پاس می‌دهید تا به input مربوط به formControlName bind شود.

هر بار که یک alias instance جدید اضافه می‌شود، form array instance جدید، control خودش را بر اساس index دریافت می‌کند. این به شما اجازه می‌دهد هنگام محاسبه‌ی status و value مربوط به root control، هر control جداگانه را دنبال کنید.

ts
import {ChangeDetectorRef, Component, inject} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';

@Component({
  /* ... */
})
export class ProfileEditor {
  private readonly cdr = inject(ChangeDetectorRef);

  constructor() {
    this.profileForm.valueChanges
      .pipe(takeUntilDestroyed())
      .subscribe(() => this.cdr.markForCheck());
  }
}

استفاده از FormArrayDirective برای form arrayهای سطح‌بالا

می‌توانید با استفاده از FormArrayDirective، یک FormArray را مستقیم به element از نوع <form> bind کنید. این حالت زمانی مفید است که فرم از FormGroup سطح‌بالا استفاده نمی‌کند و خود array کل form model را نمایش می‌دهد.

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

@Component({
  selector: 'form-array-example',
  template: `
    <form [formArray]="form">
      @for (control of form.controls; track $index) {
        <input [formControlName]="$index" />
      }
    </form>
  `,
})
export class FormArrayExampleComponent {
  controls = [new FormControl('fish'), new FormControl('cat'), new FormControl('dog')];

  form = new FormArray(this.controls);
}

در ابتدا، فرم یک field به نام Alias دارد. برای اضافه کردن field دیگر، روی دکمه‌ی Add Alias کلیک کنید. همچنین می‌توانید array مربوط به aliasها را که form model گزارش داده و پایین template با Form Value نمایش داده شده validate کنید. به‌جای یک form control instance برای هر alias، می‌توانید یک form group instance دیگر با fieldهای اضافه compose کنید. فرایند تعریف control برای هر item یکسان است.

رویدادهای یکپارچه‌ی تغییر state کنترل

همه‌ی form controlها یک stream واحد و یکپارچه از control state change events را از طریق observable مربوط به events روی AbstractControl expose می‌کنند؛ شامل FormControl، FormGroup، FormArray و FormRecord. این stream یکپارچه به شما اجازه می‌دهد به تغییرات state مربوط به value، status، pristine، touched و reset، و همچنین actionهای سطح فرم مثل submit واکنش نشان دهید؛ یعنی همه‌ی updateها را با یک subscription مدیریت کنید، به‌جای اینکه چند observable را جداگانه wire کنید.

نوع eventها

هر item که توسط events emit می‌شود instanceای از یک event class مشخص است:

  • ValueChangeEvent — وقتی value کنترل تغییر می‌کند.
  • StatusChangeEvent — وقتی validation status کنترل به یکی از مقدارهای FormControlStatus، یعنی VALID، INVALID، PENDING یا DISABLED، به‌روزرسانی می‌شود.
  • PristineChangeEvent — وقتی state مربوط به pristine/dirty کنترل تغییر می‌کند.
  • TouchedChangeEvent — وقتی state مربوط به touched/untouched کنترل تغییر می‌کند.
  • FormResetEvent — وقتی یک control یا form reset می‌شود، چه از طریق API مربوط به reset() و چه از طریق action بومی.
  • FormSubmittedEvent — وقتی فرم submit می‌شود.

همه‌ی event classها از ControlEvent extend می‌کنند و شامل یک reference به نام source به AbstractControlای هستند که تغییر از آن شروع شده؛ این در فرم‌های بزرگ مفید است.

ts
import {Component} from '@angular/core';
import {
  FormControl,
  ValueChangeEvent,
  StatusChangeEvent,
  PristineChangeEvent,
  TouchedChangeEvent,
  FormResetEvent,
  FormSubmittedEvent,
  ReactiveFormsModule,
  FormGroup,
} from '@angular/forms';

@Component(/* ... */)
export class UnifiedEventsBasicComponent {
  form = new FormGroup({
    username: new FormControl(''),
  });

  constructor() {
    this.form.events.subscribe((e) => {
      if (e instanceof ValueChangeEvent) {
        console.log('Value changed to: ', e.value);
      }

      if (e instanceof StatusChangeEvent) {
        console.log('Status changed to: ', e.status);
      }

      if (e instanceof PristineChangeEvent) {
        console.log('Pristine status changed to: ', e.pristine);
      }

      if (e instanceof TouchedChangeEvent) {
        console.log('Touched status changed to: ', e.touched);
      }

      if (e instanceof FormResetEvent) {
        console.log('Form was reset');
      }

      if (e instanceof FormSubmittedEvent) {
        console.log('Form was submitted');
      }
    });
  }
}

فیلتر کردن eventهای مشخص

وقتی فقط به زیرمجموعه‌ای از event typeها نیاز دارید، RxJS operatorها را ترجیح دهید.

ts
import {filter} from 'rxjs/operators';
import {StatusChangeEvent} from '@angular/forms';

control.events
  .pipe(filter((e) => e instanceof StatusChangeEvent))
  .subscribe((e) => console.log('Status:', e.status));

یکپارچه‌سازی چند subscription

Before

ts
import {combineLatest} from 'rxjs/operators';

combineLatest([control.valueChanges, control.statusChanges]).subscribe(([value, status]) => {
  /* ... */
});

After

ts
control.events.subscribe((e) => {
  // Handle ValueChangeEvent, StatusChangeEvent, etc.
});

مدیریت form control state

Reactive forms، control state را از طریق touched/untouched و pristine/dirty دنبال می‌کنند. Angular این‌ها را هنگام تعامل‌های DOM به‌صورت خودکار به‌روزرسانی می‌کند، اما شما هم می‌توانید آن‌ها را به‌صورت programmatic مدیریت کنید.

markAsTouched — یک control یا form را از طریق focus و blur eventهایی که مقدار را تغییر نمی‌دهند به‌عنوان touched علامت‌گذاری می‌کند. به‌صورت پیش‌فرض به parent controlها propagate می‌شود.

ts
// Show validation errors after user leaves a field
onEmailBlur() {
  const email = this.form.get('email');
  email.markAsTouched();
}

markAsUntouched — یک control یا form را به‌عنوان untouched علامت‌گذاری می‌کند. به همه‌ی child controlها cascade می‌شود و touched status همه‌ی parent controlها را دوباره محاسبه می‌کند.

ts
// Reset form state after successful submission
onSubmitSuccess() {
  this.form.markAsUntouched();
  this.form.markAsPristine();
}

markAsDirty — یک control یا form را به‌عنوان dirty علامت‌گذاری می‌کند، یعنی مقدار تغییر کرده است. به‌صورت پیش‌فرض به parent controlها propagate می‌شود.

ts
// Mark programmatically changed values as modified
autofillAddress() {
  const previousAddress = getAddress();
  this.form.patchValue(previousAddress, { emitEvent: false });
  this.form.markAsDirty();
}

markAsPristine — یک control یا form را به‌عنوان pristine علامت‌گذاری می‌کند. همه‌ی child controlها را pristine می‌کند و pristine status همه‌ی parent controlها را دوباره محاسبه می‌کند.

ts
// Reset pristine state after saving to track new changes
saveForm() {
  this.api.save(this.form.value).subscribe(() => {
    this.form.markAsPristine();
  });
}

markAllAsDirty — control یا form و همه‌ی descendant controlهای آن را به‌عنوان dirty علامت‌گذاری می‌کند.

ts
// Mark imported data as dirty
loadData(data: FormData) {
  this.form.patchValue(data);
  this.form.markAllAsDirty();
}

markAllAsTouched — control یا form و همه‌ی descendant controlهای آن را به‌عنوان touched علامت‌گذاری می‌کند. برای نمایش validation errorها در سراسر فرم مفید است.

ts
// Show all validation errors before submission
onSubmit() {
  if (this.form.invalid) {
    this.form.markAllAsTouched();
    return;
  }
  this.saveForm();
}

کنترل event emission و propagation

وقتی form controlها را به‌صورت programmatic به‌روزرسانی می‌کنید، کنترل دقیقی روی این دارید که تغییرات چطور در hierarchy فرم propagate شوند و آیا eventها emit شوند یا نه.

شناخت event emission

به‌صورت پیش‌فرض emitEvent: true است؛ هر تغییر روی یک control، eventهایی را از طریق observableهای valueChanges و statusChanges emit می‌کند. تنظیم emitEvent: false این emissionها را suppress می‌کند؛ چیزی که هنگام set کردن مقدارها به‌صورت programmatic بدون trigger کردن رفتار reactive مثل auto-save، جلوگیری از updateهای circular بین controlها، یا انجام bulk updateهایی که eventها باید فقط یک‌بار در پایان emit شوند مفید است.

ts
@Component({
  /* ... */
})
export class BlogPostEditor {
  postForm = new FormGroup({
    title: new FormControl(''),
    content: new FormControl(''),
  });

  constructor() {
    // Auto-save draft every time user types
    this.postForm.valueChanges.subscribe((formValue) => {
      this.autosaveDraft(formValue);
    });
  }

  loadExistingDraft(savedDraft: {title: string; content: string}) {
    // Restore draft without triggering auto-save
    this.postForm.setValue(savedDraft, {emitEvent: false});
  }
}

شناخت propagation control

به‌صورت پیش‌فرض onlySelf: false است؛ updateها به parent controlها cascade می‌شوند و value و validation status آن‌ها را دوباره محاسبه می‌کنند. تنظیم onlySelf: true update را به control فعلی محدود می‌کند و از notification به parent جلوگیری می‌کند. این برای batch operationهایی مفید است که می‌خواهید parent update را یک‌بار به‌صورت دستی trigger کنید.

ts
updatePostalCodeValidator(country: string) {
  const postal = this.addressForm.get('postalCode');

  const validators = country === 'US'
    ? [Validators.maxLength(5)]
    : [Validators.maxLength(7)];

  postal.setValidators(validators);
  postal.updateValueAndValidity({ onlySelf: true, emitEvent: false });
}

Utility functionها برای narrow کردن typeهای form control

Angular چهار utility function فراهم می‌کند که کمک می‌کنند type concrete یک AbstractControl را مشخص کنید. این functionها به‌عنوان type guard عمل می‌کنند و وقتی true برگردانند type کنترل را narrow می‌کنند؛ بنابراین می‌توانید داخل همان block به propertyهای مخصوص subtype با خیال راحت دسترسی داشته باشید.

Utility functionجزئیات
isFormControlوقتی control یک FormControl باشد true برمی‌گرداند.
isFormGroupوقتی control یک FormGroup باشد true برمی‌گرداند.
isFormRecordوقتی control یک FormRecord باشد true برمی‌گرداند.
isFormArrayوقتی control یک FormArray باشد true برمی‌گرداند.

این helperها به‌خصوص در custom validatorها مفیدند؛ جایی که function signature یک AbstractControl دریافت می‌کند، اما logic برای نوع مشخصی از control طراحی شده است.

ts
import {AbstractControl, isFormArray} from '@angular/forms';

export function positiveValues(control: AbstractControl) {
  if (!isFormArray(control)) {
    return null; // Not a FormArray: validator is not applicable.
  }

  // Safe to access FormArray-specific API after narrowing.
  const hasNegative = control.controls.some((c) => c.value < 0);
  return hasNegative ? {positiveValues: true} : null;
}

خلاصه‌ی API مربوط به reactive forms

جدول زیر کلاس‌ها و serviceهای پایه‌ای را فهرست می‌کند که برای ساخت و مدیریت reactive form controlها استفاده می‌شوند. برای جزئیات کامل syntax، مستندات API مربوط به Forms package را ببینید.

کلاس‌ها

Classجزئیات
AbstractControlکلاس پایه‌ی abstract برای کلاس‌های concrete form control یعنی FormControl، FormGroup و FormArray. رفتارها و propertyهای مشترک آن‌ها را فراهم می‌کند.
FormControlمقدار و validity status یک form control جداگانه را مدیریت می‌کند. با یک HTML form control مثل <input> یا <select> متناظر است.
FormGroupمقدار و validity state گروهی از instanceهای AbstractControl را مدیریت می‌کند. propertyهای group شامل child controlهای آن است. فرم سطح‌بالا در component شما FormGroup است.
FormArrayمقدار و validity state یک array با index عددی از instanceهای AbstractControl را مدیریت می‌کند.
FormBuilderیک injectable service که factory methodهایی برای ساخت control instanceها فراهم می‌کند.
FormRecordمقدار و validity state مجموعه‌ای از instanceهای FormControl را دنبال می‌کند که هر کدام value type یکسانی دارند.

Directiveها

Directiveجزئیات
FormControlDirectiveیک standalone FormControl instance را با form control element sync می‌کند.
FormControlNameFormControl موجود در یک FormGroup instance را با form control element بر اساس name sync می‌کند.
FormGroupDirectiveیک FormGroup instance موجود را با یک DOM element sync می‌کند.
FormGroupNameیک nested FormGroup instance را با یک DOM element sync می‌کند.
FormArrayNameیک nested FormArray instance را با یک DOM element sync می‌کند.
FormArrayDirectiveیک standalone FormArray instance را با یک DOM element sync می‌کند.