mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
refactor(forms): add unsupported method errors and docs
- Add disable, enable methods that throw with helpful messages - Add validator methods (set/add/remove/clear) that throw - Add setErrors, markAsPending methods that throw - Add setters for dirty/pristine/touched/untouched that throw - Add JSDoc with @usageNotes examples - Add comprehensive unit tests for SignalFormControl - Add FormGroup/FormArray integration tests - Add web tests for CVA directive lifecycle - Update migration docs with SignalFormControl usage
This commit is contained in:
@@ -83,7 +83,8 @@ In the template, use standard reactive syntax by binding the underlying control:
|
||||
|
||||
### Integrating a `FormGroup` into a signal form
|
||||
|
||||
You can also wrap an entire `FormGroup`. This is common when a reusable sub-section of a form—such as an **Address Block**—is still managed by legacy Reactive Forms.
|
||||
You can also wrap an entire `FormGroup`. This is common when a reusable sub-section of a form—such as an **Address Block
|
||||
**—is still managed by legacy Reactive Forms.
|
||||
|
||||
```typescript
|
||||
import {signal} from '@angular/core';
|
||||
@@ -206,11 +207,148 @@ const formValue = computed(() => ({
|
||||
|
||||
## Bottom-up migration
|
||||
|
||||
This is coming soon.
|
||||
### Integrating a Signal Form into a `FormGroup`
|
||||
|
||||
You can use `SignalFormControl` to expose a signal-based form as a standard `FormControl`. This is useful when you want
|
||||
to migrate leaf nodes of a form to Signals while keeping the parent `FormGroup` structure.
|
||||
|
||||
```typescript
|
||||
import {Component, inject, Injector, signal} from '@angular/core';
|
||||
import {ReactiveFormsModule, FormGroup} from '@angular/forms';
|
||||
import {SignalFormControl} from '@angular/forms/signals/compat';
|
||||
import {required} from '@angular/forms/signals';
|
||||
|
||||
@Component({
|
||||
// ...
|
||||
imports: [ReactiveFormsModule],
|
||||
})
|
||||
export class UserProfile {
|
||||
private injector = inject(Injector);
|
||||
|
||||
// 1. Create a SignalFormControl, use signal form rules.
|
||||
// Note: SignalFormControl requires an Injector
|
||||
emailControl = new SignalFormControl('', this.injector, (p) => {
|
||||
required(p, {message: 'Email is required'});
|
||||
});
|
||||
|
||||
// 2. Use it in a legacy FormGroup
|
||||
form = new FormGroup({
|
||||
email: this.emailControl,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
The `SignalFormControl` synchronizes values and validation status bi-directionally:
|
||||
|
||||
- **Signal -> Control**: Changing `email.set(...)` updates `emailControl.value` and the parent `form.value`.
|
||||
- **Control -> Signal**: Typing in the input (updating `emailControl`) updates the `email` signal.
|
||||
- **Validation**: Schema validators (like `required`) propagate errors to `emailControl.errors`.
|
||||
|
||||
### Disabling/Enabling control.
|
||||
|
||||
Imperative APIs for changing the enabled/disabled state (like `enable()`, `disable()`) are intentionally not supported
|
||||
in `SignalFormControl`. This is because the state of the control should be derived from the signal state and rules.
|
||||
|
||||
Attempting to call disable/enable would throw an error.
|
||||
|
||||
```typescript {avoid}
|
||||
import {signal, effect} from '@angular/core';
|
||||
|
||||
export class UserProfile {
|
||||
readonly emailControl = new SignalFormControl('', this.injector);
|
||||
|
||||
readonly isLoading = signal(false);
|
||||
|
||||
constructor() {
|
||||
// This will throw an error
|
||||
effect(() => {
|
||||
if (this.isLoading()) {
|
||||
this.emailControl.disable();
|
||||
} else {
|
||||
this.emailControl.enable();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Instead, use disabled rule:
|
||||
|
||||
```typescript {prefer}
|
||||
import {signal} from '@angular/core';
|
||||
import {SignalFormControl} from '@angular/forms/signals/compat';
|
||||
import {disabled} from '@angular/forms/signals';
|
||||
|
||||
export class UserProfile {
|
||||
readonly isLoading = signal(false);
|
||||
|
||||
readonly emailControl = new SignalFormControl('', this.injector, (p) => {
|
||||
// The control becomes disabled whenever isLoading is true
|
||||
disabled(p, () => this.isLoading());
|
||||
});
|
||||
|
||||
async saveData() {
|
||||
this.isLoading.set(true);
|
||||
// ... perform save ...
|
||||
this.isLoading.set(false);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Dynamic manipulation
|
||||
|
||||
Imperative APIs for adding or removing validators (like `addValidators()`, `removeValidators()`, `setValidators()`) are intentionally not supported in `SignalFormControl`.
|
||||
|
||||
Attempting to call these methods will throw an error.
|
||||
|
||||
```typescript {avoid}
|
||||
export class UserProfile {
|
||||
readonly emailControl = new SignalFormControl('', this.injector);
|
||||
readonly isRequired = signal(false);
|
||||
|
||||
toggleRequired() {
|
||||
this.isRequired.update((v) => !v);
|
||||
// This will throw an error
|
||||
if (this.isRequired()) {
|
||||
this.emailControl.addValidators(Validators.required);
|
||||
} else {
|
||||
this.emailControl.removeValidators(Validators.required);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Instead, use `applyWhen` rule to conditionally apply validators:
|
||||
|
||||
```typescript {prefer}
|
||||
import {signal} from '@angular/core';
|
||||
import {SignalFormControl} from '@angular/forms/signals/compat';
|
||||
import {applyWhen, required} from '@angular/forms/signals';
|
||||
|
||||
export class UserProfile {
|
||||
readonly isRequired = signal(false);
|
||||
|
||||
readonly emailControl = new SignalFormControl('', this.injector, (p) => {
|
||||
// The control becomes required whenever isRequired is true
|
||||
applyWhen(
|
||||
p,
|
||||
() => this.isRequired(),
|
||||
(p) => {
|
||||
required(p);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Manual Error Selection
|
||||
|
||||
The `setErrors()` and `markAsPending()` methods are not supported. In Signal Forms, errors are derived from validation rules and async validation status. If you need to report an error, it should be done declaratively via a validation rule in the schema.
|
||||
|
||||
## Automatic status classes
|
||||
|
||||
Reactive/Template Forms automatically adds [class attributes](/guide/forms/template-driven-forms#track-control-states) (such as `.ng-valid` or `.ng-dirty`) to facilitate styling control states. Signal Forms does not do that.
|
||||
Reactive/Template Forms automatically adds [class attributes](/guide/forms/template-driven-forms#track-control-states) (
|
||||
such as `.ng-valid` or `.ng-dirty`) to facilitate styling control states. Signal Forms does not do that.
|
||||
|
||||
If you want to preserve this behavior, you can provide the `NG_STATUS_CLASSES` preset:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user