UNPKG

@angular/forms

Version:

Angular - directives and services for creating forms

660 lines (649 loc) 19.4 kB
/** * @license Angular v21.2.0 * (c) 2010-2026 Google LLC. https://angular.dev/ * License: MIT */ import { FieldNode, getInjectorFromOptions, FieldNodeState, FieldNodeStructure, calculateValidationSelfStatus, extractNestedReactiveErrors, BasicFieldAdapter, form, normalizeFormArgs, signalErrorsToValidationErrors } from './_validation_errors-chunk.mjs'; export { CompatValidationError } from './_validation_errors-chunk.mjs'; import { linkedSignal, untracked, runInInjectionContext, computed, signal, ɵRuntimeError as _RuntimeError, EventEmitter, inject, Injector, effect } from '@angular/core'; import { AbstractControl, ValueChangeEvent, StatusChangeEvent, TouchedChangeEvent, PristineChangeEvent, FormResetEvent } from '@angular/forms'; import { toSignal } from '@angular/core/rxjs-interop'; import { ReplaySubject } from 'rxjs'; import { map, takeUntil } from 'rxjs/operators'; import '@angular/core/primitives/signals'; class CompatFieldNode extends FieldNode { options; control; constructor(options) { super(options); this.options = options; this.control = this.options.control; } } function makeCreateDestroySubject() { let destroy$ = new ReplaySubject(1); return () => { destroy$.next(); destroy$.complete(); return destroy$ = new ReplaySubject(1); }; } function extractControlPropToSignal(options, makeSignal) { const injector = getInjectorFromOptions(options); const createDestroySubject = makeCreateDestroySubject(); const signalOfControlSignal = linkedSignal({ ...(ngDevMode ? { debugName: "signalOfControlSignal" } : {}), source: options.control, computation: control => { return untracked(() => { return runInInjectionContext(injector, () => makeSignal(control, createDestroySubject())); }); } }); return computed(() => signalOfControlSignal()()); } const getControlStatusSignal = (options, getValue) => { return extractControlPropToSignal(options, (c, destroy$) => toSignal(c.statusChanges.pipe(map(() => getValue(c)), takeUntil(destroy$)), { initialValue: getValue(c) })); }; const getControlEventsSignal = (options, getValue) => { return extractControlPropToSignal(options, (c, destroy$) => toSignal(c.events.pipe(map(() => { return getValue(c); }), takeUntil(destroy$)), { initialValue: getValue(c) })); }; class CompatNodeState extends FieldNodeState { compatNode; touched; dirty; disabled; control; constructor(compatNode, options) { super(compatNode); this.compatNode = compatNode; this.control = options.control; this.touched = getControlEventsSignal(options, c => c.touched); this.dirty = getControlEventsSignal(options, c => c.dirty); const controlDisabled = getControlStatusSignal(options, c => c.disabled); this.disabled = computed(() => { return controlDisabled() || this.disabledReasons().length > 0; }, ...(ngDevMode ? [{ debugName: "disabled" }] : [])); } markAsDirty() { this.control().markAsDirty(); } markAsTouched() { this.control().markAsTouched(); } markAsPristine() { this.control().markAsPristine(); } markAsUntouched() { this.control().markAsUntouched(); } } function getParentFromOptions(options) { if (options.kind === 'root') { return undefined; } return options.parent; } function getFieldManagerFromOptions(options) { if (options.kind === 'root') { return options.fieldManager; } return options.parent.structure.root.structure.fieldManager; } function getControlValueSignal(options) { const value = extractControlPropToSignal(options, (control, destroy$) => { return toSignal(control.valueChanges.pipe(map(() => control.getRawValue()), takeUntil(destroy$)), { initialValue: control.getRawValue() }); }); value.set = value => { options.control().setValue(value); }; value.update = fn => { value.set(fn(value())); }; return value; } class CompatStructure extends FieldNodeStructure { value; keyInParent; root; pathKeys; children = signal([], ...(ngDevMode ? [{ debugName: "children" }] : [])); childrenMap = computed(() => undefined, ...(ngDevMode ? [{ debugName: "childrenMap" }] : [])); parent; fieldManager; constructor(node, options) { super(options.logic, node, () => { throw new _RuntimeError(1911, ngDevMode && `Compat nodes don't have children.`); }); this.value = getControlValueSignal(options); this.parent = getParentFromOptions(options); this.root = this.parent?.structure.root ?? node; this.fieldManager = getFieldManagerFromOptions(options); const identityInParent = options.kind === 'child' ? options.identityInParent : undefined; const initialKeyInParent = options.kind === 'child' ? options.initialKeyInParent : undefined; this.keyInParent = this.createKeyInParent(options, identityInParent, initialKeyInParent); this.pathKeys = computed(() => this.parent ? [...this.parent.structure.pathKeys(), this.keyInParent()] : [], ...(ngDevMode ? [{ debugName: "pathKeys" }] : [])); } getChild() { return undefined; } } const EMPTY_ARRAY_SIGNAL = computed(() => [], ...(ngDevMode ? [{ debugName: "EMPTY_ARRAY_SIGNAL" }] : [])); const TRUE_SIGNAL = computed(() => true, ...(ngDevMode ? [{ debugName: "TRUE_SIGNAL" }] : [])); class CompatValidationState { syncValid; errors; pending; invalid; valid; parseErrors = computed(() => [], ...(ngDevMode ? [{ debugName: "parseErrors" }] : [])); constructor(options) { this.syncValid = getControlStatusSignal(options, c => c.status === 'VALID'); this.errors = getControlStatusSignal(options, extractNestedReactiveErrors); this.pending = getControlStatusSignal(options, c => c.pending); this.valid = getControlStatusSignal(options, c => { return c.valid; }); this.invalid = getControlStatusSignal(options, c => { return c.invalid; }); } asyncErrors = EMPTY_ARRAY_SIGNAL; errorSummary = EMPTY_ARRAY_SIGNAL; rawSyncTreeErrors = EMPTY_ARRAY_SIGNAL; syncErrors = EMPTY_ARRAY_SIGNAL; rawAsyncErrors = EMPTY_ARRAY_SIGNAL; shouldSkipValidation = TRUE_SIGNAL; status = computed(() => { return calculateValidationSelfStatus(this); }, ...(ngDevMode ? [{ debugName: "status" }] : [])); } class CompatFieldAdapter { basicAdapter = new BasicFieldAdapter(); newRoot(fieldManager, value, pathNode, adapter) { if (value() instanceof AbstractControl) { return createCompatNode({ kind: 'root', fieldManager, value, pathNode, logic: pathNode.builder.build(), fieldAdapter: adapter }); } return this.basicAdapter.newRoot(fieldManager, value, pathNode, adapter); } createNodeState(node, options) { if (!options.control) { return this.basicAdapter.createNodeState(node); } return new CompatNodeState(node, options); } createStructure(node, options) { if (!options.control) { return this.basicAdapter.createStructure(node, options); } return new CompatStructure(node, options); } createValidationState(node, options) { if (!options.control) { return this.basicAdapter.createValidationState(node); } return new CompatValidationState(options); } newChild(options) { const value = options.parent.value()[options.initialKeyInParent]; if (value instanceof AbstractControl) { return createCompatNode(options); } return new FieldNode(options); } } function createCompatNode(options) { const control = options.kind === 'root' ? options.value : computed(() => { return options.parent.value()[options.initialKeyInParent]; }); return new CompatFieldNode({ ...options, control }); } function compatForm(...args) { const [model, maybeSchema, maybeOptions] = normalizeFormArgs(args); const options = { ...maybeOptions, adapter: new CompatFieldAdapter() }; const schema = maybeSchema || (() => {}); return form(model, schema, options); } const NG_STATUS_CLASSES = { 'ng-touched': ({ state }) => state().touched(), 'ng-untouched': ({ state }) => !state().touched(), 'ng-dirty': ({ state }) => state().dirty(), 'ng-pristine': ({ state }) => !state().dirty(), 'ng-valid': ({ state }) => state().valid(), 'ng-invalid': ({ state }) => state().invalid(), 'ng-pending': ({ state }) => state().pending() }; class SignalFormControl extends AbstractControl { fieldTree; sourceValue; fieldState; initialValue; pendingParentNotifications = 0; onChangeCallbacks = []; onDisabledChangeCallbacks = []; valueChanges = new EventEmitter(); statusChanges = new EventEmitter(); constructor(value, schemaOrOptions, options) { super(null, null); const [model, schema, opts] = normalizeFormArgs([signal(value), schemaOrOptions, options]); this.sourceValue = model; this.initialValue = value; const injector = opts?.injector ?? inject(Injector); const rawTree = schema ? compatForm(this.sourceValue, schema, { injector }) : compatForm(this.sourceValue, { injector }); this.fieldTree = wrapFieldTreeForSyncUpdates(rawTree, () => this.parent?.updateValueAndValidity({ sourceControl: this })); this.fieldState = this.fieldTree(); this.defineCompatProperties(); effect(() => { const value = this.sourceValue(); untracked(() => { this.notifyParentUnlessPending(); this.valueChanges.emit(value); this.emitControlEvent(new ValueChangeEvent(value, this)); }); }, { injector }); effect(() => { const status = this.status; untracked(() => { this.statusChanges.emit(status); }); this.emitControlEvent(new StatusChangeEvent(status, this)); }, { injector }); effect(() => { const isDisabled = this.disabled; untracked(() => { for (const fn of this.onDisabledChangeCallbacks) { fn(isDisabled); } }); }, { injector }); effect(() => { const isTouched = this.fieldState.touched(); this.emitControlEvent(new TouchedChangeEvent(isTouched, this)); const parent = this.parent; if (!parent) { return; } if (!isTouched) { parent.markAsUntouched(); } else { parent.markAsTouched(); } }, { injector }); effect(() => { const isDirty = this.fieldState.dirty(); this.emitControlEvent(new PristineChangeEvent(!isDirty, this)); const parent = this.parent; if (!parent) { return; } if (isDirty) { parent.markAsDirty(); } else { parent.markAsPristine(); } }, { injector }); } defineCompatProperties() { const valueProp = getClosureSafeProperty({ value: getClosureSafeProperty }); Object.defineProperty(this, valueProp, { get: () => this.sourceValue() }); const errorsProp = getClosureSafeProperty({ errors: getClosureSafeProperty }); Object.defineProperty(this, errorsProp, { get: () => signalErrorsToValidationErrors(this.fieldState.errors()) }); } emitControlEvent(event) { untracked(() => { this._events.next(event); }); } setValue(value, options) { this.updateValue(value, options); } patchValue(value, options) { this.updateValue(value, options); } updateValue(value, options) { const parent = this.scheduleParentUpdate(options); this.sourceValue.set(value); if (parent) { this.updateParentValueAndValidity(parent, options?.emitEvent); } if (options?.emitModelToViewChange !== false) { for (const fn of this.onChangeCallbacks) { fn(value, true); } } } registerOnChange(fn) { this.onChangeCallbacks.push(fn); } _unregisterOnChange(fn) { removeListItem(this.onChangeCallbacks, fn); } registerOnDisabledChange(fn) { this.onDisabledChangeCallbacks.push(fn); } _unregisterOnDisabledChange(fn) { removeListItem(this.onDisabledChangeCallbacks, fn); } getRawValue() { return this.value; } reset(value, options) { if (isFormControlState(value)) { throw unsupportedDisableEnableError(); } const resetValue = value ?? this.initialValue; this.fieldState.reset(resetValue); if (value !== undefined) { this.updateValue(value, options); } else if (!options?.onlySelf) { const parent = this.parent; if (parent) { this.updateParentValueAndValidity(parent, options?.emitEvent); } } if (options?.emitEvent !== false) { this.emitControlEvent(new FormResetEvent(this)); } } scheduleParentUpdate(options) { const parent = options?.onlySelf ? null : this.parent; if (options?.onlySelf || parent) { this.pendingParentNotifications++; } return parent; } notifyParentUnlessPending() { if (this.pendingParentNotifications > 0) { this.pendingParentNotifications--; return; } const parent = this.parent; if (parent) { this.updateParentValueAndValidity(parent); } } updateParentValueAndValidity(parent, emitEvent) { parent.updateValueAndValidity({ emitEvent, sourceControl: this }); } propagateToParent(opts, fn) { const parent = this.parent; if (parent && !opts?.onlySelf) { fn(parent); } } get status() { if (this.fieldState.disabled()) { return 'DISABLED'; } if (this.fieldState.valid()) { return 'VALID'; } if (this.fieldState.invalid()) { return 'INVALID'; } return 'PENDING'; } get valid() { return this.fieldState.valid(); } get invalid() { return this.fieldState.invalid(); } get pending() { return this.fieldState.pending(); } get disabled() { return this.fieldState.disabled(); } get enabled() { return !this.disabled; } get dirty() { return this.fieldState.dirty(); } set dirty(_) { throw unsupportedFeatureError(ngDevMode && 'Setting dirty directly is not supported. Instead use markAsDirty().'); } get pristine() { return !this.dirty; } set pristine(_) { throw unsupportedFeatureError(ngDevMode && 'Setting pristine directly is not supported. Instead use reset().'); } get touched() { return this.fieldState.touched(); } set touched(_) { throw unsupportedFeatureError(ngDevMode && 'Setting touched directly is not supported. Instead use markAsTouched() or reset().'); } get untouched() { return !this.touched; } set untouched(_) { throw unsupportedFeatureError(ngDevMode && 'Setting untouched directly is not supported. Instead use reset().'); } markAsTouched(opts) { this.fieldState.markAsTouched(); this.propagateToParent(opts, parent => parent.markAsTouched(opts)); } markAsDirty(opts) { this.fieldState.markAsDirty(); this.propagateToParent(opts, parent => parent.markAsDirty(opts)); } markAsPristine(opts) { this.fieldState.markAsPristine(); this.propagateToParent(opts, parent => parent.markAsPristine(opts)); } markAsUntouched(opts) { this.fieldState.markAsUntouched(); this.propagateToParent(opts, parent => parent.markAsUntouched(opts)); } updateValueAndValidity(_opts) {} _updateValue() {} _forEachChild(_cb) {} _anyControls(_condition) { return false; } _allControlsDisabled() { return this.disabled; } _syncPendingControls() { return false; } disable(_opts) { throw unsupportedDisableEnableError(); } enable(_opts) { throw unsupportedDisableEnableError(); } setValidators(_validators) { throw unsupportedValidatorsError(); } setAsyncValidators(_validators) { throw unsupportedValidatorsError(); } addValidators(_validators) { throw unsupportedValidatorsError(); } addAsyncValidators(_validators) { throw unsupportedValidatorsError(); } removeValidators(_validators) { throw unsupportedValidatorsError(); } removeAsyncValidators(_validators) { throw unsupportedValidatorsError(); } clearValidators() { throw unsupportedValidatorsError(); } clearAsyncValidators() { throw unsupportedValidatorsError(); } setErrors(_errors, _opts) { throw unsupportedFeatureError(ngDevMode && 'Imperatively setting errors is not supported in signal forms. Errors are derived from validation rules.'); } markAsPending(_opts) { throw unsupportedFeatureError(ngDevMode && 'Imperatively marking as pending is not supported in signal forms. Pending state is derived from async validation status.'); } } class CachingWeakMap { map = new WeakMap(); getOrCreate(key, create) { const cached = this.map.get(key); if (cached) { return cached; } const value = create(); this.map.set(key, value); return value; } } function wrapFieldTreeForSyncUpdates(tree, onUpdate) { const treeCache = new CachingWeakMap(); const stateCache = new CachingWeakMap(); const wrapState = state => { const { value } = state; const wrappedValue = Object.assign((...a) => value(...a), { set: v => { value.set(v); onUpdate(); }, update: fn => { value.update(fn); onUpdate(); } }); return Object.create(state, { value: { get: () => wrappedValue } }); }; const wrapTree = t => { return treeCache.getOrCreate(t, () => { return new Proxy(t, { get(target, prop, receiver) { const val = Reflect.get(target, prop, receiver); if (typeof val === 'function' && typeof prop === 'string') { return wrapTree(val); } return val; }, apply(target, _, args) { const state = target(...args); return stateCache.getOrCreate(state, () => wrapState(state)); } }); }); }; return wrapTree(tree); } function isFormControlState(formState) { return typeof formState === 'object' && formState !== null && Object.keys(formState).length === 2 && 'value' in formState && 'disabled' in formState; } function unsupportedFeatureError(message) { return new _RuntimeError(1920, message ?? false); } function unsupportedDisableEnableError() { return unsupportedFeatureError(ngDevMode && 'Imperatively changing enabled/disabled status in form control is not supported in signal forms. Instead use a "disabled" rule to derive the disabled status from a signal.'); } function unsupportedValidatorsError() { return unsupportedFeatureError(ngDevMode && 'Dynamically adding and removing validators is not supported in signal forms. Instead use the "applyWhen" rule to conditionally apply validators based on a signal.'); } function removeListItem(list, el) { const index = list.indexOf(el); if (index > -1) list.splice(index, 1); } function getClosureSafeProperty(objWithPropertyToExtract) { for (let key in objWithPropertyToExtract) { if (objWithPropertyToExtract[key] === getClosureSafeProperty) { return key; } } throw Error(typeof ngDevMode === 'undefined' || ngDevMode ? 'Could not find renamed property on target object.' : ''); } export { NG_STATUS_CLASSES, SignalFormControl, compatForm }; //# sourceMappingURL=signals-compat.mjs.map