mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
Introduce a highly decoupled FVC and CVA custom control reset mechanism, and implement the framework-wide automatic `transformedValue` and native controls clearing bridge for both new Signal Forms and legacy forms (Template-driven and Reactive). 1. Custom Control Reset Propagation (Bug #2): - Establish agnostic custom control resetting via `FormFieldBindingOptions.reset` in `FormField`. - Ensure that `FieldNode.reset()` unconditionally triggers `writeValue` updates on CVA custom controls. - Protect against duplicate writes during subsequent change detection updates in `control_cva.ts` by verifying and tracking previous written values in the local bindings cache. 2. Unified Framework-wide FormControl Integration: - Introduce a monorepo-wide private InjectionToken `ɵFORM_CONTROL_INTEGRATION` and `ɵFormControlIntegration` interface to act as the single, decoupled bridge for hooking up FVC parse errors and receiving control resets across both Signal and legacy forms architectures. - Simplify Signal Forms: make `FormField` implements `ɵFormControlIntegration` directly, removing the intermediate context object and reducing DI boilerplate down to a clean `useExisting: FormField` provider. Triggers the `onReset` callback directly inside `FormField.reset()`. - Upgrade Legacy Forms: `NG_CONTROL_INTEGRATION_PROVIDER` provides the renamed token. `NgControl` handles the event subscription internally (`set onReset(callback)`) to recursively listen to `control.events` (`FormResetEvent`) lazily only when assigned, resolving all `FormControl` swapping timing and lifecycle cleanup races automatically. 3. Automatic `transformedValue` and Native Controls Utility Clearing: - Make `Parser.reset()` method required in the interface for a cleaner and non-defensive execution. - Wire `transformedValue` into the new integration token `ɵFORM_CONTROL_INTEGRATION` to clear validation parsing states on resets. - Lazily resets the UI-facing `rawValue` linked signal utilizing the original native `linkedSignal.set` callback (`originalSet`), correctly bypassing the UI-to-model parser loopback and preventing redundant model writes during `reset()`. - Wire up Native Controls (`control_native.ts\Device`): Hook `parent.onReset` inside native element creation to automatically trigger the native `parser.reset()` and force DOM writes (`setNativeControlValue`) back down to the DOM input value during resets, ensuring native elements with pending parsing validation errors are successfully cleared and synced on form resets. TAG=agy CONV=8b4cee1e-2117-42a4-b242-c8ec7bf01752
66 lines
2 KiB
TypeScript
66 lines
2 KiB
TypeScript
/**
|
|
* @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 {type Signal, linkedSignal} from '@angular/core';
|
|
import type {ValidationError} from '../api/rules';
|
|
import {normalizeErrors} from '../api/rules/validation/util';
|
|
import type {ParseResult} from '../api/transformed_value';
|
|
|
|
/**
|
|
* An object that handles parsing raw UI values into model values.
|
|
*/
|
|
export interface Parser<TRaw> {
|
|
/**
|
|
* Errors encountered during the last parse attempt.
|
|
*/
|
|
errors: Signal<readonly ValidationError.WithoutFieldTree[]>;
|
|
/**
|
|
* Parses the given raw value and updates the underlying model value if successful.
|
|
*/
|
|
setRawValue: (rawValue: TRaw) => void;
|
|
/**
|
|
* Resets the parser errors.
|
|
*/
|
|
readonly reset: () => void;
|
|
}
|
|
|
|
/**
|
|
* Creates a {@link Parser} that synchronizes a raw value with an underlying model value.
|
|
*
|
|
* @param getValue Function to get the current model value.
|
|
* @param setValue Function to update the model value.
|
|
* @param parse Function to parse the raw value into a {@link ParseResult}.
|
|
* @returns A {@link Parser} instance.
|
|
*/
|
|
export function createParser<TValue, TRaw>(
|
|
getValue: () => TValue,
|
|
setValue: (value: TValue) => void,
|
|
parse: (raw: TRaw) => ParseResult<TValue>,
|
|
): Parser<TRaw> {
|
|
const errors = linkedSignal({
|
|
source: getValue,
|
|
computation: () => [] as readonly ValidationError.WithoutFieldTree[],
|
|
});
|
|
|
|
const setRawValue = (rawValue: TRaw) => {
|
|
const result = parse(rawValue);
|
|
errors.set(normalizeErrors(result.error));
|
|
if (result.value !== undefined) {
|
|
setValue(result.value);
|
|
}
|
|
// `errors` is a linked signal sourced from the model value; write parse errors after
|
|
// model updates so `{value, errors}` results do not get reset by the recomputation.
|
|
errors.set(normalizeErrors(result.error));
|
|
};
|
|
|
|
const reset = () => {
|
|
errors.set([]);
|
|
};
|
|
|
|
return {errors: errors.asReadonly(), setRawValue, reset};
|
|
}
|