mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
feat(forms): introduce SignalFormControl for Reactive Forms compatibility
This commit introduces `SignalFormControl`, a bridge implementation that allows Signal-based forms to interoperate with existing Reactive Forms infrastructure. It extends `AbstractControl` with standard methods and reactive observables while handling state propagation to parent containers.
This commit is contained in:
parent
f29fcd882a
commit
3937afc316
3 changed files with 123 additions and 0 deletions
|
|
@ -14,3 +14,4 @@
|
|||
export * from './src/api/compat_form';
|
||||
export * from './src/api/compat_validation_error';
|
||||
export * from './src/api/di';
|
||||
export {SignalFormControl} from './src/signal_form_control/signal_form_control';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {inject, Injector, signal, WritableSignal} from '@angular/core';
|
||||
import {AbstractControl, FormControlStatus} from '@angular/forms';
|
||||
|
||||
import {compatForm} from '../api/compat_form';
|
||||
import {signalErrorsToValidationErrors} from '../../../src/api/rules';
|
||||
import {FormOptions} from '../../../src/api/structure';
|
||||
import {FieldState, FieldTree, SchemaFn} from '../../../src/api/types';
|
||||
import {normalizeFormArgs} from '../../../src/util/normalize_form_args';
|
||||
|
||||
/**
|
||||
* A `FormControl` that is backed by signal forms rules.
|
||||
*
|
||||
* This class provides a bridge between Signal Forms and Reactive Forms, allowing
|
||||
* signal-based controls to be used within a standard `FormGroup` or `FormArray`.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export class SignalFormControl<T> extends AbstractControl {
|
||||
/** Source FieldTree. */
|
||||
public readonly fieldTree: FieldTree<T>;
|
||||
/** The raw signal driving the control value. */
|
||||
public readonly sourceValue: WritableSignal<T>;
|
||||
|
||||
private readonly fieldState: FieldState<T>;
|
||||
|
||||
constructor(value: T, schemaOrOptions?: SchemaFn<T> | FormOptions, options?: FormOptions) {
|
||||
super(null, null);
|
||||
|
||||
const [model, schema, opts] = normalizeFormArgs<T>([signal(value), schemaOrOptions, options]);
|
||||
this.sourceValue = model;
|
||||
const injector = opts?.injector ?? inject(Injector);
|
||||
|
||||
this.fieldTree = schema
|
||||
? compatForm(this.sourceValue, schema, {injector})
|
||||
: compatForm(this.sourceValue, {injector});
|
||||
this.fieldState = this.fieldTree();
|
||||
|
||||
Object.defineProperty(this, 'value', {
|
||||
get: () => this.sourceValue(),
|
||||
});
|
||||
Object.defineProperty(this, 'errors', {
|
||||
get: () => signalErrorsToValidationErrors(this.fieldState.errors()),
|
||||
});
|
||||
}
|
||||
|
||||
override setValue(value: any): void {
|
||||
this.sourceValue.set(value);
|
||||
}
|
||||
|
||||
override patchValue(value: any): void {
|
||||
this.sourceValue.set(value);
|
||||
}
|
||||
|
||||
override getRawValue(): T {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
override reset(): void {
|
||||
this.fieldState.reset(this.sourceValue());
|
||||
}
|
||||
|
||||
override get status(): FormControlStatus {
|
||||
if (this.fieldState.valid()) {
|
||||
return 'VALID';
|
||||
}
|
||||
if (this.fieldState.invalid()) {
|
||||
return 'INVALID';
|
||||
}
|
||||
return 'PENDING';
|
||||
}
|
||||
|
||||
override get valid(): boolean {
|
||||
return this.fieldState.valid();
|
||||
}
|
||||
|
||||
override get invalid(): boolean {
|
||||
return this.fieldState.invalid();
|
||||
}
|
||||
|
||||
override updateValueAndValidity(_opts?: Object): void {}
|
||||
|
||||
/** @internal */
|
||||
_updateValue(): void {}
|
||||
|
||||
/** @internal */
|
||||
_forEachChild(_cb: (c: AbstractControl) => void): void {}
|
||||
|
||||
/** @internal */
|
||||
_anyControls(_condition: (c: AbstractControl) => boolean): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_allControlsDisabled(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_syncPendingControls(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {ValidationErrors} from '@angular/forms';
|
||||
import type {FormField} from '../../../directive/form_field_directive';
|
||||
import type {FieldTree} from '../../types';
|
||||
import type {StandardSchemaValidationError} from './standard_schema';
|
||||
|
|
@ -487,3 +488,14 @@ export type NgValidationError =
|
|||
| PatternValidationError
|
||||
| EmailValidationError
|
||||
| StandardSchemaValidationError;
|
||||
|
||||
export function signalErrorsToValidationErrors(errors: ValidationError[]): ValidationErrors | null {
|
||||
if (errors.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const errObj: ValidationErrors = {};
|
||||
for (const error of errors) {
|
||||
errObj[error.kind] = error;
|
||||
}
|
||||
return errObj;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue