diff --git a/packages/forms/signals/compat/public_api.ts b/packages/forms/signals/compat/public_api.ts index facf8d9ce19..9d77117ca64 100644 --- a/packages/forms/signals/compat/public_api.ts +++ b/packages/forms/signals/compat/public_api.ts @@ -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'; diff --git a/packages/forms/signals/compat/src/signal_form_control/signal_form_control.ts b/packages/forms/signals/compat/src/signal_form_control/signal_form_control.ts new file mode 100644 index 00000000000..735c8a37f7d --- /dev/null +++ b/packages/forms/signals/compat/src/signal_form_control/signal_form_control.ts @@ -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 extends AbstractControl { + /** Source FieldTree. */ + public readonly fieldTree: FieldTree; + /** The raw signal driving the control value. */ + public readonly sourceValue: WritableSignal; + + private readonly fieldState: FieldState; + + constructor(value: T, schemaOrOptions?: SchemaFn | FormOptions, options?: FormOptions) { + super(null, null); + + const [model, schema, opts] = normalizeFormArgs([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; + } +} diff --git a/packages/forms/signals/src/api/rules/validation/validation_errors.ts b/packages/forms/signals/src/api/rules/validation/validation_errors.ts index fce54fe2d29..f88a859fafe 100644 --- a/packages/forms/signals/src/api/rules/validation/validation_errors.ts +++ b/packages/forms/signals/src/api/rules/validation/validation_errors.ts @@ -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; +}