シグナルフォーム: 未来のリアクティブフォーム
Angular のリアクティブ フォームに基づく FormControl, FormGroup e
FormArray、長年にわたってフォーム管理の標準を代表してきました。
コンプレックス。しかし、の導入により、 信号 リアクティブプリミティブとして
フレームワークの中核である Angular チームは、新世代のフォームに取り組んでいます。
完全に Signal に基づいています: i 信号形式。この進化は期待できる
フォーム状態の RxJS への依存を排除し、定型文を大幅に削減します。
きめ細かい応答性をフォーム管理システムにネイティブに統合します。
このシリーズの 5 番目の記事では、 モダンアンギュラー、制限を分析します 現在のリアクティブ フォームについては、シグナル フォームの RFC 提案を調査し、どのようにするかを見ていきます。 新しいプリミティブがどのように機能するか、および移行の準備方法について説明します。を始めているかどうか 新しいプロジェクトや既存のエンタープライズ アプリケーションの管理については、このガイドで提供されます。 Angular のフォームの将来に直面するために必要な知識。
最新の Angular シリーズの概要
| # | アイテム | 集中 |
|---|---|---|
| 1 | 角度信号 | きめ細かい応答性 |
| 2 | ゾーンレスの変更検出 | Zone.jsを削除する |
| 3 | 新しいテンプレート @if、@for、@defer | 最新の制御フロー |
| 4 | スタンドアロンコンポーネント | NgModule を使用しないアーキテクチャ |
| 5 | あなたはここにいます — シグナルフォーム | シグナルを使用したレスポンシブフォーム |
| 6 | SSR と増分水分補給 | サーバーサイドレンダリング |
| 7 | Angular におけるコア Web バイタル | パフォーマンスと指標 |
| 8 | 角度のある PWA | プログレッシブ Web アプリ |
| 9 | 高度な依存関係の注入 | DIツリーシェイク可能 |
| 10 | Angular 17 から 21 への移行 | 移行ガイド |
現在のリアクティブフォームの限界
リアクティブ フォームは Angular 2 で導入され、クラス階層に基づいています。
要約 (AbstractControl, FormControl, FormGroup,
FormArray) RxJS を使用する Observable 変更を通知するため
価値もステータスも。強力ではありますが、いくつかの構造上の制限があることが明らかになっています。
複雑なアプリケーションで。
反応型の構造的問題
- RxJS への依存関係: すべて順調です
FormControl暴露するvalueChangesestatusChangesObservable のように、開発者はサブスクリプション、オペレーター、メモリ リークを管理する必要があります - 弱い型付け: Angular 14 まで、
FormGroup返されましたany。型付きフォームにより状況は改善されましたが、構文は冗長なままです - AbstractControl 階層: チェーン継承により、コントロールの動作の拡張やカスタマイズが困難になります
- 過剰な定型文: 検証を含むフォームを作成するには、テンプレートに触れる前に数十行の TypeScript コードが必要です
- 暗黙的な状態: プロパティとして
dirty,touched,validそれらは変更可能で非派生的であるため、不整合が発生します。 - Signals との統合なし: リアクティブフォームはシグナルのきめ細かい反応性システムを利用しないため、Observable と Signal の間で手動で変換する必要があります。
- 複雑なリセット: フォームの状態 (値、ダーティ、タッチ) をリセットするには複数回の呼び出しが必要であり、常に期待どおりに動作するとは限りません。
// Un form di registrazione con i Reactive Forms attuali
@Component({
selector: 'app-registration',
standalone: true,
imports: [ReactiveFormsModule],
template: `...`
})
export class RegistrationComponent implements OnInit, OnDestroy {
private fb = inject(FormBuilder);
private destroy$ = new Subject<void>();
form = this.fb.group({
name: ['', [Validators.required, Validators.minLength(2)]],
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
confirmPassword: ['', Validators.required],
address: this.fb.group({
street: [''],
city: ['', Validators.required],
zip: ['', Validators.pattern(/^\d{5}$/)]
})
});
// Devo sottoscrivermi manualmente per reagire ai cambiamenti
ngOnInit() {
this.form.get('password')?.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(value => {
this.form.get('confirmPassword')?.updateValueAndValidity();
});
}
// Devo gestire manualmente la distruzione delle sottoscrizioni
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
get nameErrors() {
const ctrl = this.form.get('name');
if (ctrl?.hasError('required')) return 'Nome obbligatorio';
if (ctrl?.hasError('minlength')) return 'Minimo 2 caratteri';
return '';
}
}
ご覧のとおり、中程度に複雑なフォームであっても、かなりの量の作業が必要になります。 定型コードの説明: 手動サブスクリプション、ライフサイクル管理、i のゲッター エラー メッセージ、および監視可能な値と命令的な値の間の変換。シグナルフォームの約束 これらすべての問題を解決するために。
シグナルフォームの提案: RFC と新しいプリミティブ
Angular チームは、 RFC (コメント要求) 信号形式の場合、 Signals に基づいたフォーム管理システムを完全に書き直しました。目標 主なことは、次の API を作成することです。 本質的に反応性の高い, without depending のパワーと柔軟性を維持しながら、フォーム状態を RxJS から取得します。 Current Reactive Forms.
新しい原始的な提案
| 原始的な信号形式 | リアクティブフォーム同等品 | 説明 |
|---|---|---|
FormSignal |
FormControl |
単一形式の値、応答状態の書き込み可能な信号 |
FormRecord |
FormGroup |
フォーム信号グループ。複数の FormSignal を 1 つのオブジェクトに構成します。 |
FormList |
FormArray |
繰り返される収集のための信号形式の動的リスト |
computed() |
valueChanges |
フォーム値からの自動応答導出 |
effect() |
.subscribe() |
手動管理を必要としないクリーンアップによる自動副作用 |
// Lo stesso form di registrazione con Signal Forms
@Component({
selector: 'app-registration',
standalone: true,
template: `...`
})
export class RegistrationComponent {
// Dichiarazione concisa con validazione inline
name = formSignal('', {
validators: [required(), minLength(2)]
});
email = formSignal('', {
validators: [required(), email()]
});
password = formSignal('', {
validators: [required(), minLength(8)]
});
confirmPassword = formSignal('', {
validators: [required(), matchesField(this.password)]
});
address = formRecord({
street: formSignal(''),
city: formSignal('', { validators: [required()] }),
zip: formSignal('', { validators: [pattern(/^\d{5}$/)] })
});
// Lo stato è derivato automaticamente, nessuna sottoscrizione
isFormValid = computed(() =>
this.name.isValid() &&
this.email.isValid() &&
this.password.isValid() &&
this.confirmPassword.isValid() &&
this.address.isValid()
);
// Nessun ngOnInit, nessun ngOnDestroy, nessun Subject
}
シグナルフォームの主な利点
- ゼロサブスクリプション: いいえ
subscribe()、 いいえtakeUntil、メモリリークに対処する必要はありません - 派生状態: プロパティとして
isValid(),isDirty(),errors()それらは計算された信号であり、常に同期されています - ネイティブにタイプセーフ: 値の型は初期値から自動的に推測されます。
- テンプレートとの統合: 信号はパイプや変換を行わずにテンプレートに直接接続されます。
- 自動クリーンアップ: コンポーネントのライフサイクルを手動で管理する必要がない
シグナルに基づくフォームステータス
シグナルフォームへの最も重要な変更の 1 つは次のとおりです。 フォーム全体の状態
信号になる。のような変更可能なプロパティはもうありません .dirty o
.touched 内部的に更新されるもの: 状態のあらゆる側面と
computed 真実の源から得られた信号。
const username = formSignal('', {
validators: [required(), minLength(3), maxLength(20)]
});
// Il valore corrente: WritableSignal<string>
console.log(username.value()); // ''
// Stato di validazione: Signal<boolean>
console.log(username.isValid()); // false
console.log(username.isInvalid()); // true
// Errori di validazione: Signal<ValidationErrors | null>
console.log(username.errors()); // { required: true }
// Stato di interazione: Signal<boolean>
console.log(username.isDirty()); // false
console.log(username.isPristine()); // true
console.log(username.isTouched()); // false
console.log(username.isUntouched()); // true
// Stato di attesa (validazione asincrona): Signal<boolean>
console.log(username.isPending()); // false
// Stato di disabilitazione: Signal<boolean>
console.log(username.isDisabled()); // false
console.log(username.isEnabled()); // true
// Aggiornamento programmatico del valore
username.value.set('angular_dev');
// Ora: isValid() === true, isDirty() === true, errors() === null
// Marcare come touched (ad esempio on blur)
username.markAsTouched();
// Ora: isTouched() === true
比較: リアクティブフォームとシグナルフォームの状態
| 財産 | 反応性フォーム | 信号形式 |
|---|---|---|
| 価値 | control.value (可変ゲッター) |
control.value() (信号) |
| 有効 | control.valid (ブール型プロパティ) |
control.isValid() (計算された信号) |
| エラー | control.errors (オブジェクトまたはnull) |
control.errors() (信号) |
| 汚い | control.dirty (内部的に変更されたブール値) |
control.isDirty() (計算された信号) |
| 変化を観察する | control.valueChanges.subscribe() |
effect(() => console.log(control.value())) |
| 掃除 | マニュアル(購読解除、テイクアンティル) | 自動 (DestroyRef に関連付けられている) |
主な利点は、状態が 常に一貫性のある。リアクティブフォームでは、
中間状態が観察される場合があります。 dirty 更新されてますが、 errors
まだですよ。 Signal Forms を使用すると、計算されたすべての更新がグリッチなしでアトミックに行われます。
シグナルのスケジュール設定。
シグナルによる検証
Signal Forms は検証システムを完全にリセットします。バリデーターは、
返す純粋関数 エラー信号、ネイティブサポート付き
などの個別のインターフェイスを必要としない同期および非同期検証
Validator e AsyncValidator.
// Validatori built-in
const email = formSignal('', {
validators: [
required(), // Non può essere vuoto
email(), // Deve essere un'email valida
maxLength(100) // Massimo 100 caratteri
]
});
// Validatore personalizzato: funzione pura
function strongPassword(): ValidatorFn {
return (value: string) => {
const hasUpperCase = /[A-Z]/.test(value);
const hasLowerCase = /[a-z]/.test(value);
const hasDigit = /\d/.test(value);
const hasSpecial = /[!@#$%^&*]/.test(value);
if (hasUpperCase && hasLowerCase && hasDigit && hasSpecial) {
return null; // Valido
}
return {
strongPassword: {
hasUpperCase,
hasLowerCase,
hasDigit,
hasSpecial,
message: 'La password deve contenere maiuscole, minuscole, numeri e caratteri speciali'
}
};
};
}
const password = formSignal('', {
validators: [required(), minLength(8), strongPassword()]
});
// Accesso reattivo agli errori specifici
const passwordErrors = computed(() => {
const errs = password.errors();
if (!errs) return [];
const messages: string[] = [];
if (errs['required']) messages.push('Password obbligatoria');
if (errs['minlength']) messages.push('Minimo 8 caratteri');
if (errs['strongPassword']) messages.push(errs['strongPassword'].message);
return messages;
});
// Validatore asincrono: controlla se l'username è disponibile
function usernameAvailable(userService: UserService): AsyncValidatorFn {
return async (value: string) => {
if (value.length < 3) return null; // Troppo corto, saltiamo la verifica
const exists = await userService.checkUsername(value);
return exists
? { usernameTaken: { message: `L'username "${value}" è già in uso` } }
: null;
};
}
@Component({
selector: 'app-signup',
standalone: true,
template: `
<label>Username</label>
<input [formSignal]="username" />
@if (username.isPending()) {
<span class="checking">Verifica in corso...</span>
}
@if (username.errors()?.['usernameTaken']; as err) {
<span class="error">{{ err.message }}</span>
}
`
})
export class SignupComponent {
private userService = inject(UserService);
username = formSignal('', {
validators: [required(), minLength(3)],
asyncValidators: [usernameAvailable(this.userService)],
asyncDebounceMs: 300 // Debounce integrato!
});
}
シグナルフォームによる検証の新機能
- 統合されたデバウンス: 非同期バリデータのサポート
asyncDebounceMsネイティブに、必要なくdebounceTime()by RxJS - 反応保留ステータス:
isPending()非同期検証中に自動的に更新されるシグナル - クロスフィールドバリデータ: 他の FormSignal に依存関係としてアクセスでき、リンクされた値が変更されると自動更新されます。
- 構成: バリデーターはシンプルな関数であり、簡単に作成およびテストできます。
信号によるグループの形成: 構成とネスト
formRecord() さらにダイヤルできるようになります formSignal 構造の中で
階層的な、と同等 FormGroup 反応性フォームの。主な違い
レコードの状態は 自動的に導き出される 子どもたちの様子から。
// Form completo con gruppi annidati
const profileForm = formRecord({
personalInfo: formRecord({
firstName: formSignal('', { validators: [required()] }),
lastName: formSignal('', { validators: [required()] }),
birthDate: formSignal<Date | null>(null)
}),
contactInfo: formRecord({
email: formSignal('', { validators: [required(), email()] }),
phone: formSignal('', { validators: [pattern(/^\+?\d{10,15}$/)] })
}),
preferences: formRecord({
newsletter: formSignal(false),
theme: formSignal<'light' | 'dark'>('dark'),
language: formSignal('it')
}),
// FormList per indirizzi multipli
addresses: formList([
formRecord({
type: formSignal<'home' | 'work'>('home'),
street: formSignal('', { validators: [required()] }),
city: formSignal('', { validators: [required()] }),
zip: formSignal('', { validators: [required()] })
})
])
});
// Lo stato si propaga automaticamente verso l'alto
const isProfileValid = profileForm.isValid();
// true solo se TUTTI i campi di TUTTI i gruppi sono validi
const isDirty = profileForm.isDirty();
// true se QUALSIASI campo in qualsiasi gruppo e stato modificato
// Accesso al valore completo come oggetto tipizzato
const formValue = profileForm.value();
// Tipo inferito automaticamente:
// {
// personalInfo: { firstName: string; lastName: string; birthDate: Date | null }
// contactInfo: { email: string; phone: string }
// preferences: { newsletter: boolean; theme: 'light' | 'dark'; language: string }
// addresses: Array<{ type: 'home' | 'work'; street: string; city: string; zip: string }>
// }
// Aggiungere un indirizzo alla lista
profileForm.controls.addresses.push(
formRecord({
type: formSignal<'home' | 'work'>('work'),
street: formSignal('', { validators: [required()] }),
city: formSignal('', { validators: [required()] }),
zip: formSignal('', { validators: [required()] })
})
);
自動状態伝播
グループステータスが内部で計算されるリアクティブフォームとは異なります。
Signal Forms の状態は一時的な不一致を引き起こす可能性があります。 formRecord
それは 計算された信号 それは彼の子供たち次第です。これは次のことを意味します。
formRecord.isValid()ècomputed(() => children.every(c => c.isValid()))formRecord.isDirty()ècomputed(() => children.some(c => c.isDirty()))- グリッチのないスケジューリングにより、一貫性のない中間状態がありません。
- レコードをリセットすると、すべての子がアトミックにリセットされます
model() とシグナルフォームによる双方向バインディング
Angular がこの機能を導入しました model() のように 双方向バインド可能な信号
コンポーネント用。 Signal Forms は次のものとシームレスに統合されます model()、作成中
テンプレートとフォーム状態間のクリーンな双方向データ フローを必要とせずに実現します。
などの特別な指令の formControlName.
// Componente input personalizzato con model()
@Component({
selector: 'app-text-input',
standalone: true,
template: `
<div class="input-wrapper" [class.invalid]="hasError()">
<label>{{ label() }}</label>
<input
[value]="value()"
(input)="onInput($event)"
(blur)="onBlur()"
[placeholder]="placeholder()"
/>
@if (hasError() && isTouched()) {
<span class="error-message">{{ errorMessage() }}</span>
}
</div>
`
})
export class TextInputComponent {
// Two-way binding con il componente padre
value = model<string>('');
label = input<string>('');
placeholder = input<string>('');
hasError = input<boolean>(false);
isTouched = input<boolean>(false);
errorMessage = input<string>('');
onInput(event: Event) {
const target = event.target as HTMLInputElement;
this.value.set(target.value);
}
onBlur() {
// Notifica il padre che il campo e stato toccato
}
}
// Utilizzo nel componente padre con Signal Forms
@Component({
selector: 'app-contact-form',
standalone: true,
imports: [TextInputComponent],
template: `
<form (ngSubmit)="submit()">
<app-text-input
[(value)]="name.value"
[label]="'Nome completo'"
[hasError]="name.isInvalid()"
[isTouched]="name.isTouched()"
[errorMessage]="nameError()"
/>
<app-text-input
[(value)]="email.value"
[label]="'Email'"
[hasError]="email.isInvalid()"
[isTouched]="email.isTouched()"
[errorMessage]="emailError()"
/>
<button [disabled]="!isFormValid()">Invia</button>
</form>
`
})
export class ContactFormComponent {
name = formSignal('', { validators: [required()] });
email = formSignal('', { validators: [required(), email()] });
nameError = computed(() => {
if (this.name.errors()?.['required']) return 'Nome obbligatorio';
return '';
});
emailError = computed(() => {
if (this.email.errors()?.['required']) return 'Email obbligatoria';
if (this.email.errors()?.['email']) return 'Email non valida';
return '';
});
isFormValid = computed(() =>
this.name.isValid() && this.email.isValid()
);
submit() {
console.log({ name: this.name.value(), email: this.email.value() });
}
}
model() + シグナルフォーム統合の利点
- 特別な指示はありません: それらは必要ありません
[formControl]oformControlNameテンプレート内で - 再利用可能なコンポーネント: カスタム入力は、経由で値を受け取り、返します。
model()、フォームロジックから完全に独立しています。 - 完全なタイプセーフティ: 値の型は、FormSignal から model() を介してテンプレートに流れます。
- テスト容易性: FormModule を使用せずに、入力コンポーネントを単独でテストできます
今日の実践的なパターン: ハイブリッド シグナル + リアクティブ フォーム アプローチ
シグナル フォームはまだ RFC 段階にありますが、シグナルの活用を開始できます。
現在の Reactive Forms では、 ハイブリッドアプローチ なんと簡単なことか
将来の移住。これは、使用することを意味します toSignal() を変換する
Signals 内のフォームを観察し、派生状態を次のように構築します。 computed().
@Component({
selector: 'app-search',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<input [formControl]="searchControl" placeholder="Cerca..." />
@if (isSearching()) {
<span class="spinner">Ricerca in corso...</span>
}
@if (hasResults()) {
<p>Trovati {{ resultCount() }} risultati per "{{ searchTerm() }}"</p>
}
@for (result of results(); track result.id) {
<app-result-card [result]="result" />
} @empty {
@if (searchTerm().length > 0) {
<p>Nessun risultato per "{{ searchTerm() }}"</p>
}
}
`
})
export class SearchComponent {
private searchService = inject(SearchService);
searchControl = new FormControl('', { nonNullable: true });
// Converto valueChanges in Signal
searchTerm = toSignal(
this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged()
),
{ initialValue: '' }
);
// Stato derivato con computed
private searchResult = toSignal(
toObservable(this.searchTerm).pipe(
switchMap(term =>
term.length > 2
? this.searchService.search(term)
: of({ items: [], total: 0 })
)
),
{ initialValue: { items: [], total: 0 } }
);
results = computed(() => this.searchResult().items);
resultCount = computed(() => this.searchResult().total);
hasResults = computed(() => this.resultCount() > 0);
isSearching = signal(false);
}
移行戦略: リアクティブフォームからシグナルフォームへ
- フェーズ 1 (今日): 変換する
valueChanges信号中toSignal()。アメリカ合衆国computed()派生状態による - フェーズ 2 (今日): フォーム ロジックを Signal を公開する専用サービスに分離する
- フェーズ 3 (利用可能な場合): FormControl を FormSignal に一度に 1 つずつ置き換えます
- フェーズ 4: 取り除く
ReactiveFormsModuleフォームに関連する RxJS 依存関係
シグナルを使用したカスタム フォーム コントロール
インターフェース ControlValueAccessor (CVA) は、
カスタムフォームコンポーネント。シグナルフォームを使用すると、このパターンが簡素化されます。
劇的に。ただし、現在ではシグナルを活用することで、よりクリーンな CVA を構築することがすでに可能になっています。
// CVA attuale migliorato con Signals
@Component({
selector: 'app-star-rating',
standalone: true,
template: `
<div class="star-rating" [class.disabled]="isDisabled()">
@for (star of stars(); track star) {
<button
class="star"
[class.filled]="star <= currentValue()"
[class.hovered]="star <= hoveredStar()"
(mouseenter)="hoveredStar.set(star)"
(mouseleave)="hoveredStar.set(0)"
(click)="selectRating(star)"
[disabled]="isDisabled()">
★
</button>
}
<span class="label">{{ currentValue() }} / {{ maxStars() }}</span>
</div>
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => StarRatingComponent),
multi: true
}
]
})
export class StarRatingComponent implements ControlValueAccessor {
maxStars = input(5);
currentValue = signal(0);
hoveredStar = signal(0);
isDisabled = signal(false);
stars = computed(() =>
Array.from({ length: this.maxStars() }, (_, i) => i + 1)
);
private onChange: (value: number) => void = () => {};
private onTouched: () => void = () => {};
writeValue(value: number): void {
this.currentValue.set(value || 0);
}
registerOnChange(fn: (value: number) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.isDisabled.set(isDisabled);
}
selectRating(star: number): void {
if (this.isDisabled()) return;
this.currentValue.set(star);
this.onChange(star);
this.onTouched();
}
}
現在の CVA と将来のシグナル フォーム
| 待ってます | 現在のCVA | シグナルフォーム(将来) |
|---|---|---|
| 実装するインターフェース | ControlValueAccessor (4つの方法) |
インターフェイスはありません、ただ model() |
| 登録 | NG_VALUE_ACCESSOR プロバイダー |
バインディングによる自動 [(value)] |
| onChange/onTouched のコールバック | 手動、必須 | 自動、Signal 経由で応答 |
| ステータスが無効 | setDisabledState() 命令的な |
信号 isDisabled() 親から |
| 定型文 | 約 30 行の CVA コード | model() を含む最大 5 行 |
// Futuro: nessun ControlValueAccessor, nessun provider
@Component({
selector: 'app-star-rating',
standalone: true,
template: `
<div class="star-rating">
@for (star of stars(); track star) {
<button
class="star"
[class.filled]="star <= value()"
(click)="value.set(star)">
★
</button>
}
</div>
`
})
export class StarRatingComponent {
// model() gestisce tutto: two-way binding, notifiche, stato
value = model(0);
maxStars = input(5);
stars = computed(() =>
Array.from({ length: this.maxStars() }, (_, i) => i + 1)
);
// Fine. Nessun CVA, nessun provider, nessuna callback.
}
// Nel parent:
// <app-star-rating [(value)]="rating.value" />
計画された移行: 予想されること
シグナルフォームは現在 RFC 段階にあり、Angular チームは次のように伝えています。 紹介になります 徐々に、壊れない。リアクティブフォームは継続します いくつかのメジャー リリースでサポートされるため、段階的な移行が可能になります。
予想される移行タイムライン
| 段階 | 推定バージョン | 何を期待するか |
|---|---|---|
| RFC とフィードバック | 角度 19-20 | コミュニティからフィードバックを収集し、API を反復処理する |
| 開発者プレビュー | 角度 20-21 | API は利用可能ですが変更される可能性があります |
| 安定した | 角度 21-22 | 安定した API、公式移行図 |
| 推奨 | 角度 22+ | ドキュメントのデフォルトのアプローチとしてのシグナルフォーム |
| RF の廃止 | アンギュラー 24+ | 非推奨としてマークされたリアクティブ フォーム (拡張サポートあり) |
移行準備戦略
- 入力されたフォームを採用します。 まだ入力せずに FormControl を使用している場合は、最初のステップとして Angular 14 で導入された Typed Reactive Forms に移行してください。
- アメリカ合衆国
nonNullable: FormControls を設定するnonNullable: trueシグナルフォームの動作に合わせるため - フォームロジックを分離します。 フォームの作成と管理を専用のサービスに移行します。これにより、移行にはコンポーネントではなくサービスのみが含まれます。
- の使用を減らしてください
valueChanges: 可能な場合は、使用してくださいtoSignal(control.valueChanges)Signal の使用を開始するには - 純粋な検証関数を優先します。 複雑な依存関係を持つクラスとしてではなく、エラーを返す純粋な関数としてバリデータを作成します。
- AbstractControl 拡張機能は避けてください。 FormControl または FormGroup のサブクラスは作成しないでください。Signal Forms には同等のクラスが存在しないためです。
- 検証ルールを文書化します。 変換を容易にするために、フィールドとバリデータ間の明確なマッピングを維持します。
// Service che isola la logica del form
// Oggi: usa Reactive Forms internamente, espone Signal
// Domani: sostituisci l'implementazione con Signal Forms
@Injectable({ providedIn: 'root' })
export class ProfileFormService {
private fb = inject(FormBuilder);
// Implementazione interna con Reactive Forms
private _form = this.fb.nonNullable.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
bio: ['', Validators.maxLength(500)]
});
// API pubblica basata su Signal
readonly name = toSignal(
this._form.controls.name.valueChanges,
{ initialValue: '' }
);
readonly email = toSignal(
this._form.controls.email.valueChanges,
{ initialValue: '' }
);
readonly isValid = toSignal(
this._form.statusChanges.pipe(map(s => s === 'VALID')),
{ initialValue: false }
);
readonly isDirty = toSignal(
this._form.valueChanges.pipe(map(() => this._form.dirty)),
{ initialValue: false }
);
// Metodi pubblici che restano invariati dopo la migrazione
setValues(data: Partial<ProfileData>) {
this._form.patchValue(data);
}
getValues(): ProfileData {
return this._form.getRawValue();
}
reset() {
this._form.reset();
}
}
Angular のフォームのベスト プラクティス
テンプレート駆動型フォーム、リアクティブ フォーム、または シグナルフォームの準備をしています。いくつかのベストプラクティスは普遍的であり、 これらは、より堅牢で保守しやすく、将来に備えたフォームを作成するのに役立ちます。
Angular フォームに関する 10 のベスト プラクティス
- 適切なフォームの種類を選択してください: シンプルなフォーム用のテンプレート駆動型 (ログイン、 連絡先)、複雑なフォームのリアクティブ/シグナル (マルチステップ、動的、高度な検証付き)
- サービス中のロジックを分離します。 FormGroup をコンポーネント内に直接作成しないでください。 Signal を公開する専用サービスを使用して、テストと再利用を容易にする
- 宣言的な検証: バリデーターを純粋な再利用可能な関数として定義し、 コンポーネント内のインライン ロジックとしてではない
-
タッチ後にのみエラーを表示します: までエラーメッセージを表示しない
ユーザーがフィールドを操作しないとき (
isTouched()oisDirty()) - 積載ステータスの管理: 実行中に送信ボタンを無効にする 非同期検証と読み込みインジケーターの表示
-
アメリカ合衆国
nonNullable: FormControls を常に次のように設定しますnonNullable: true価値観を避けるためにnullリセット後の予期せぬ -
計算結果を優先します: フォームのステータス (有効性、エラー、
完了)
computed()とではなくeffect()osubscribe() - バリデーターを単独でテストします。 各バリデーターの単体テストを作成する コンポーネントから独立した純粋な関数としてカスタマイズされる
- 送信は慎重に管理してください。 二重送信を防止し、HTTP エラーを処理し、 ユーザーに明確なフィードバックを表示します (成功/エラー)
-
移行の準備をします。 リアクティブフォームを使用している場合は、徐々に変換してください
valueChanges信号中toSignal()将来の移行を促進するために
いつどのタイプのフォームを使用するか
| シナリオ | テンプレート駆動型 | 反応性フォーム | シグナルフォーム(将来) |
|---|---|---|---|
| 簡単ログイン・お問い合わせフォーム | アドバイス | 許容できる | アドバイス |
| 複数ステップのフォーム/ウィザード | 推奨されません | アドバイス | アドバイス |
| 動的フォーム (生成されたフィールド) | 推奨されません | アドバイス | アドバイス |
| 複雑なフィールド間検証 | 難しい | 管理可能 | 単純 |
| 豊富な派生状態を備えたフォーム | 限定 | RxJSを使用する場合 | ネイティブ (計算済み) |
| シグナルとの統合 | 部分的 | toSignal() を使用する | ネイティブ |
| パフォーマンス (変化検出) | Zone.js に依存 | Zone.js に依存 | きめの細かい |
フォームで避けるべきアンチパターン
- 使用しないでください
ngModelリアクティブフォームの場合: 同じフォーム内でテンプレート駆動型とリアクティブを混在させると、予期しない動作が発生する - クリーンアップを忘れないでください。 リアクティブ フォームを使用すると、
subscribe()avalueChangesで管理する必要がありますtakeUntilDestroyed()oDestroyRef - クライアント側だけを検証しないでください。 クライアント側の検証により UX は向上しますが、セキュリティのためにサーバー側の検証は必須です
- アクセシビリティを無視しないでください。 アメリカ合衆国
aria-describedbyエラーメッセージをフィールドにリンクするには、earia-invalid無効なフィールドを報告するには - 使用しないでください
getRawValue()理由もなく: フォームに無効なフィールドがない場合は、.valueそして十分な
概要と次のステップ
シグナルフォームは、フレームワークに向けた Angular の進化における自然なステップを表します 完全に信号ベースです。フォーム状態の RxJS への依存を排除し、 Signal Forms は、ボイラープレートとネイティブに統合されたきめ細かい応答性を約束します。 フォーム管理をよりシンプル、安全、効率的にします。
この記事の重要な概念
- リアクティブフォームの制限: RxJS への依存、過剰なボイラープレート、可変状態、厳格な AbstractControl 階層、Signals との統合なし
- シグナルフォーム RFC: 新しいプリミティブ (
formSignal,formRecord,formList) FormControl、FormGroup、FormArray を置き換えるシグナルに基づく - 反応状態:
isValid(),isDirty(),isTouched(),errors()計算された信号として常にコヒーレント - 簡略化された検証: 純粋な関数としてのバリデータ、非同期用の組み込みデバウンス、クロスフィールド リアクティブ
- formRecord を使用した構成: 状態は子から親に自動的に伝播され、アトミックなリセット、推論された型付けが行われます。
- model() + 信号形式: ControlValueAccessor を使用しない双方向バインディング、再利用可能な最小限のフォーム コンポーネント
- ハイブリッドアプローチ: 使用
toSignal()今すぐ Reactive Forms を使用して移行の準備をしましょう - 簡略化されたCVA: 将来、
model()ControlValueAccessor インターフェイスを完全に置き換えます - 段階的な移行: Reactive Forms は長期間サポートされ続け、移行は公式の回路図によって支援されます
シリーズの次の記事では、詳しく説明します。 SSR と増分水分補給、 Angular のパフォーマンスを実現するサーバーサイド レンダリング技術 優れたロード時間。増分ハイドレーションによりインタラクティブまでの時間が短縮されます。 ロジックをサーバーからクライアントに段階的に転送します。







