@angular/forms 22.0.0-next.3 → 22.0.0-next.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,16 @@
1
1
  /**
2
- * @license Angular v22.0.0-next.3
2
+ * @license Angular v22.0.0-next.5
3
3
  * (c) 2010-2026 Google LLC. https://angular.dev/
4
4
  * License: MIT
5
5
  */
6
6
 
7
7
  import * as i0 from '@angular/core';
8
- import { InjectionToken, ɵisPromise as _isPromise, resource, linkedSignal, inject, ɵRuntimeError as _RuntimeError, untracked, input, computed, Renderer2, DestroyRef, Injector, ElementRef, signal, afterRenderEffect, effect, ɵformatRuntimeError as _formatRuntimeError, Directive } from '@angular/core';
9
- import { assertPathIsCurrent, FieldPathNode, addDefaultField, metadata, createMetadataKey, MAX, MAX_LENGTH, MIN, MIN_LENGTH, PATTERN, REQUIRED, createManagedMetadataKey, DEBOUNCER, signalErrorsToValidationErrors, submit } from './_validation_errors-chunk.mjs';
8
+ import { InjectionToken, debounced, resource, ɵisPromise as _isPromise, linkedSignal, inject, ɵRuntimeError as _RuntimeError, untracked, CSP_NONCE, PLATFORM_ID, Injectable, forwardRef, input, computed, Renderer2, DestroyRef, Injector, ElementRef, signal, afterRenderEffect, effect, ɵformatRuntimeError as _formatRuntimeError, Directive } from '@angular/core';
9
+ import { ɵFORM_FIELD_PARSE_ERRORS as _FORM_FIELD_PARSE_ERRORS, Validators, ɵsetNativeDomProperty as _setNativeDomProperty, ɵisNativeFormElement as _isNativeFormElement, ɵisNumericFormElement as _isNumericFormElement, ɵisTextualFormElement as _isTextualFormElement, NG_VALUE_ACCESSOR, NgControl } from '@angular/forms';
10
+ import { assertPathIsCurrent, FieldPathNode, addDefaultField, metadata, createMetadataKey, MAX, MAX_LENGTH, MIN, MIN_LENGTH, PATTERN, REQUIRED, createManagedMetadataKey, IS_ASYNC_VALIDATION_RESOURCE, DEBOUNCER, signalErrorsToValidationErrors, submit } from './_validation_errors-chunk.mjs';
10
11
  export { MetadataKey, MetadataReducer, apply, applyEach, applyWhen, applyWhenValue, form, schema } from './_validation_errors-chunk.mjs';
12
+ import { DOCUMENT, isPlatformBrowser } from '@angular/common';
11
13
  import { httpResource } from '@angular/common/http';
12
- import { Validators, NG_VALUE_ACCESSOR, NgControl } from '@angular/forms';
13
14
  import '@angular/core/primitives/signals';
14
15
 
15
16
  const SIGNAL_FORMS_CONFIG = new InjectionToken(typeof ngDevMode !== 'undefined' && ngDevMode ? 'SIGNAL_FORMS_CONFIG' : '');
@@ -344,7 +345,14 @@ function required(path, config) {
344
345
  function validateAsync(path, opts) {
345
346
  assertPathIsCurrent(path);
346
347
  const pathNode = FieldPathNode.unwrapFieldPath(path);
347
- const RESOURCE = createManagedMetadataKey(opts.factory);
348
+ const RESOURCE = createManagedMetadataKey((_state, params) => {
349
+ if (opts.debounce !== undefined) {
350
+ const debouncedResource = debounced(() => params(), opts.debounce);
351
+ return opts.factory(debouncedResource.value);
352
+ }
353
+ return opts.factory(params);
354
+ });
355
+ RESOURCE[IS_ASYNC_VALIDATION_RESOURCE] = true;
348
356
  metadata(path, RESOURCE, ctx => {
349
357
  const node = ctx.stateOf(path);
350
358
  const validationState = node.validationState;
@@ -445,6 +453,7 @@ class StandardSchemaValidationError extends BaseNgValidationError {
445
453
  function validateHttp(path, opts) {
446
454
  validateAsync(path, {
447
455
  params: opts.request,
456
+ debounce: opts.debounce,
448
457
  factory: request => httpResource(request, opts.options),
449
458
  onSuccess: opts.onSuccess,
450
459
  onError: opts.onError
@@ -498,8 +507,6 @@ function debounceUntilBlur() {
498
507
  }
499
508
  function immediate() {}
500
509
 
501
- const FORM_FIELD_PARSE_ERRORS = new InjectionToken(typeof ngDevMode !== 'undefined' && ngDevMode ? 'FORM_FIELD_PARSE_ERRORS' : '');
502
-
503
510
  function createParser(getValue, setValue, parse) {
504
511
  const errors = linkedSignal({
505
512
  ...(ngDevMode ? {
@@ -528,7 +535,7 @@ function transformedValue(value, options) {
528
535
  format
529
536
  } = options;
530
537
  const parser = createParser(value, value.set, parse);
531
- const formFieldParseErrors = inject(FORM_FIELD_PARSE_ERRORS, {
538
+ const formFieldParseErrors = inject(_FORM_FIELD_PARSE_ERRORS, {
532
539
  self: true,
533
540
  optional: true
534
541
  });
@@ -656,22 +663,9 @@ function bindingUpdated(bindings, key, value) {
656
663
  return false;
657
664
  }
658
665
 
659
- function isNativeFormElement(element) {
660
- return element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA';
661
- }
662
- function isNumericFormElement(element) {
663
- if (element.tagName !== 'INPUT') {
664
- return false;
665
- }
666
- const type = element.type;
667
- return type === 'date' || type === 'datetime-local' || type === 'month' || type === 'number' || type === 'range' || type === 'time' || type === 'week';
668
- }
669
- function isTextualFormElement(element) {
670
- return element.tagName === 'INPUT' || element.tagName === 'TEXTAREA';
671
- }
672
- function getNativeControlValue(element, currentValue) {
666
+ function getNativeControlValue(element, currentValue, validityMonitor) {
673
667
  let modelValue;
674
- if (element.validity.badInput) {
668
+ if (isInput(element) && validityMonitor.isBadInput(element)) {
675
669
  return {
676
670
  error: new NativeInputParseError()
677
671
  };
@@ -707,6 +701,25 @@ function getNativeControlValue(element, currentValue) {
707
701
  }
708
702
  break;
709
703
  }
704
+ if (element.tagName === 'INPUT' && element.type === 'text') {
705
+ modelValue ??= untracked(currentValue);
706
+ if (typeof modelValue === 'number' || modelValue === null) {
707
+ if (element.value === '') {
708
+ return {
709
+ value: null
710
+ };
711
+ }
712
+ const parsed = Number(element.value);
713
+ if (Number.isNaN(parsed)) {
714
+ return {
715
+ error: new NativeInputParseError()
716
+ };
717
+ }
718
+ return {
719
+ value: parsed
720
+ };
721
+ }
722
+ }
710
723
  return {
711
724
  value: element.value
712
725
  };
@@ -742,6 +755,16 @@ function setNativeControlValue(element, value) {
742
755
  return;
743
756
  }
744
757
  }
758
+ if (element.tagName === 'INPUT' && element.type === 'text') {
759
+ if (typeof value === 'number') {
760
+ element.value = isNaN(value) ? '' : String(value);
761
+ return;
762
+ }
763
+ if (value === null) {
764
+ element.value = '';
765
+ return;
766
+ }
767
+ }
745
768
  element.value = value;
746
769
  }
747
770
  function setNativeNumberControlValue(element, value) {
@@ -751,31 +774,11 @@ function setNativeNumberControlValue(element, value) {
751
774
  element.valueAsNumber = value;
752
775
  }
753
776
  }
754
- function setNativeDomProperty(renderer, element, name, value) {
755
- switch (name) {
756
- case 'name':
757
- renderer.setAttribute(element, name, value);
758
- break;
759
- case 'disabled':
760
- case 'readonly':
761
- case 'required':
762
- if (value) {
763
- renderer.setAttribute(element, name, '');
764
- } else {
765
- renderer.removeAttribute(element, name);
766
- }
767
- break;
768
- case 'max':
769
- case 'min':
770
- case 'minLength':
771
- case 'maxLength':
772
- if (value !== undefined) {
773
- renderer.setAttribute(element, name, value.toString());
774
- } else {
775
- renderer.removeAttribute(element, name);
776
- }
777
- break;
778
- }
777
+ function isInput(element) {
778
+ return element.tagName === 'INPUT';
779
+ }
780
+ function inputRequiresValidityTracking(input) {
781
+ return input.type === 'date' || input.type === 'datetime-local' || input.type === 'month' || input.type === 'time' || input.type === 'week';
779
782
  }
780
783
 
781
784
  function customControlCreate(host, parent) {
@@ -799,7 +802,7 @@ function customControlCreate(host, parent) {
799
802
  if (bindingUpdated(bindings, name, value)) {
800
803
  host.setInputOnDirectives(name, value);
801
804
  if (parent.elementAcceptsNativeProperty(name) && !host.customControlHasInput(name)) {
802
- setNativeDomProperty(parent.renderer, parent.nativeFormElement, name, value);
805
+ _setNativeDomProperty(parent.renderer, parent.nativeFormElement, name, value);
803
806
  }
804
807
  }
805
808
  }
@@ -824,7 +827,7 @@ function cvaControlCreate(host, parent) {
824
827
  if (name === 'disabled' && parent.controlValueAccessor.setDisabledState) {
825
828
  untracked(() => parent.controlValueAccessor.setDisabledState(value));
826
829
  } else if (!propertyWasSet && parent.elementAcceptsNativeProperty(name)) {
827
- setNativeDomProperty(parent.renderer, parent.nativeFormElement, name, value);
830
+ _setNativeDomProperty(parent.renderer, parent.nativeFormElement, name, value);
828
831
  }
829
832
  }
830
833
  }
@@ -872,13 +875,16 @@ function isRelevantSelectMutation(mutation) {
872
875
  return false;
873
876
  }
874
877
 
875
- function nativeControlCreate(host, parent, parseErrorsSource) {
878
+ function nativeControlCreate(host, parent, parseErrorsSource, validityMonitor) {
876
879
  let updateMode = false;
877
880
  const input = parent.nativeFormElement;
878
- const parser = createParser(() => parent.state().value(), rawValue => parent.state().controlValue.set(rawValue), () => getNativeControlValue(input, parent.state().value));
881
+ const parser = createParser(() => parent.state().value(), rawValue => parent.state().controlValue.set(rawValue), _rawValue => getNativeControlValue(input, parent.state().value, validityMonitor));
879
882
  parseErrorsSource.set(parser.errors);
880
883
  host.listenToDom('input', () => parser.setRawValue(undefined));
881
884
  host.listenToDom('blur', () => parent.state().markAsTouched());
885
+ if (isInput(input) && inputRequiresValidityTracking(input)) {
886
+ validityMonitor.watchValidity(input, () => parser.setRawValue(undefined));
887
+ }
882
888
  parent.registerAsBinding();
883
889
  if (input.tagName === 'SELECT') {
884
890
  observeSelectMutations(input, () => {
@@ -900,7 +906,7 @@ function nativeControlCreate(host, parent, parseErrorsSource) {
900
906
  if (bindingUpdated(bindings, name, value)) {
901
907
  host.setInputOnDirectives(name, value);
902
908
  if (parent.elementAcceptsNativeProperty(name)) {
903
- setNativeDomProperty(parent.renderer, input, name, value);
909
+ _setNativeDomProperty(parent.renderer, input, name, value);
904
910
  }
905
911
  }
906
912
  }
@@ -908,6 +914,112 @@ function nativeControlCreate(host, parent, parseErrorsSource) {
908
914
  };
909
915
  }
910
916
 
917
+ class InputValidityMonitor {
918
+ static ɵfac = i0.ɵɵngDeclareFactory({
919
+ minVersion: "12.0.0",
920
+ version: "22.0.0-next.5",
921
+ ngImport: i0,
922
+ type: InputValidityMonitor,
923
+ deps: [],
924
+ target: i0.ɵɵFactoryTarget.Injectable
925
+ });
926
+ static ɵprov = i0.ɵɵngDeclareInjectable({
927
+ minVersion: "12.0.0",
928
+ version: "22.0.0-next.5",
929
+ ngImport: i0,
930
+ type: InputValidityMonitor,
931
+ providedIn: 'root',
932
+ useClass: i0.forwardRef(() => AnimationInputValidityMonitor)
933
+ });
934
+ }
935
+ i0.ɵɵngDeclareClassMetadata({
936
+ minVersion: "12.0.0",
937
+ version: "22.0.0-next.5",
938
+ ngImport: i0,
939
+ type: InputValidityMonitor,
940
+ decorators: [{
941
+ type: Injectable,
942
+ args: [{
943
+ providedIn: 'root',
944
+ useClass: forwardRef(() => AnimationInputValidityMonitor)
945
+ }]
946
+ }]
947
+ });
948
+ class AnimationInputValidityMonitor extends InputValidityMonitor {
949
+ document = inject(DOCUMENT);
950
+ cspNonce = inject(CSP_NONCE, {
951
+ optional: true
952
+ });
953
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
954
+ injectedStyles = new WeakMap();
955
+ watchValidity(element, callback) {
956
+ if (!this.isBrowser) {
957
+ return;
958
+ }
959
+ const rootNode = element.getRootNode();
960
+ if (!this.injectedStyles.has(rootNode)) {
961
+ this.injectedStyles.set(rootNode, this.createTransitionStyle(rootNode));
962
+ }
963
+ element.addEventListener('animationstart', event => {
964
+ const animationEvent = event;
965
+ if (animationEvent.animationName === 'ng-valid' || animationEvent.animationName === 'ng-invalid') {
966
+ callback();
967
+ }
968
+ });
969
+ }
970
+ isBadInput(element) {
971
+ return element.validity?.badInput ?? false;
972
+ }
973
+ createTransitionStyle(rootNode) {
974
+ const element = this.document.createElement('style');
975
+ if (this.cspNonce) {
976
+ element.nonce = this.cspNonce;
977
+ }
978
+ element.textContent = `
979
+ @keyframes ng-valid {}
980
+ @keyframes ng-invalid {}
981
+ input:valid, textarea:valid {
982
+ animation: ng-valid 0.001s;
983
+ }
984
+ input:invalid, textarea:invalid {
985
+ animation: ng-invalid 0.001s;
986
+ }
987
+ `;
988
+ if (rootNode.nodeType === 9) {
989
+ rootNode.head?.appendChild(element);
990
+ } else {
991
+ rootNode.appendChild(element);
992
+ }
993
+ return element;
994
+ }
995
+ ngOnDestroy() {
996
+ this.injectedStyles.get(this.document)?.remove();
997
+ }
998
+ static ɵfac = i0.ɵɵngDeclareFactory({
999
+ minVersion: "12.0.0",
1000
+ version: "22.0.0-next.5",
1001
+ ngImport: i0,
1002
+ type: AnimationInputValidityMonitor,
1003
+ deps: null,
1004
+ target: i0.ɵɵFactoryTarget.Injectable
1005
+ });
1006
+ static ɵprov = i0.ɵɵngDeclareInjectable({
1007
+ minVersion: "12.0.0",
1008
+ version: "22.0.0-next.5",
1009
+ ngImport: i0,
1010
+ type: AnimationInputValidityMonitor
1011
+ });
1012
+ }
1013
+ i0.ɵɵngDeclareClassMetadata({
1014
+ minVersion: "12.0.0",
1015
+ version: "22.0.0-next.5",
1016
+ ngImport: i0,
1017
+ type: AnimationInputValidityMonitor,
1018
+ decorators: [{
1019
+ type: Injectable
1020
+ }]
1021
+ });
1022
+
911
1023
  const ɵNgFieldDirective = Symbol();
912
1024
  const FORM_FIELD = new InjectionToken(typeof ngDevMode !== 'undefined' && ngDevMode ? 'FORM_FIELD' : '');
913
1025
  class FormField {
@@ -924,9 +1036,9 @@ class FormField {
924
1036
  destroyRef = inject(DestroyRef);
925
1037
  injector = inject(Injector);
926
1038
  element = inject(ElementRef).nativeElement;
927
- elementIsNativeFormElement = isNativeFormElement(this.element);
928
- elementAcceptsNumericValues = isNumericFormElement(this.element);
929
- elementAcceptsTextualValues = isTextualFormElement(this.element);
1039
+ elementIsNativeFormElement = _isNativeFormElement(this.element);
1040
+ elementAcceptsNumericValues = _isNumericFormElement(this.element);
1041
+ elementAcceptsTextualValues = _isTextualFormElement(this.element);
930
1042
  nativeFormElement = this.elementIsNativeFormElement ? this.element : undefined;
931
1043
  focuser = options => this.element.focus(options);
932
1044
  controlValueAccessors = inject(NG_VALUE_ACCESSOR, {
@@ -936,6 +1048,7 @@ class FormField {
936
1048
  config = inject(SIGNAL_FORMS_CONFIG, {
937
1049
  optional: true
938
1050
  });
1051
+ validityMonitor = inject(InputValidityMonitor);
939
1052
  parseErrorsSource = signal(undefined, ...(ngDevMode ? [{
940
1053
  debugName: "parseErrorsSource"
941
1054
  }] : []));
@@ -1023,7 +1136,7 @@ class FormField {
1023
1136
  } else if (host.customControl) {
1024
1137
  this.ɵngControlUpdate = customControlCreate(host, this);
1025
1138
  } else if (this.elementIsNativeFormElement) {
1026
- this.ɵngControlUpdate = nativeControlCreate(host, this, this.parseErrorsSource);
1139
+ this.ɵngControlUpdate = nativeControlCreate(host, this, this.parseErrorsSource, this.validityMonitor);
1027
1140
  } else {
1028
1141
  throw new _RuntimeError(1914, typeof ngDevMode !== 'undefined' && ngDevMode && `${host.descriptor} is an invalid [formField] directive host. The host must be a native form control ` + `(such as <input>', '<select>', or '<textarea>') or a custom form control with a 'value' or ` + `'checked' model.`);
1029
1142
  }
@@ -1051,7 +1164,7 @@ class FormField {
1051
1164
  }
1052
1165
  static ɵfac = i0.ɵɵngDeclareFactory({
1053
1166
  minVersion: "12.0.0",
1054
- version: "22.0.0-next.3",
1167
+ version: "22.0.0-next.5",
1055
1168
  ngImport: i0,
1056
1169
  type: FormField,
1057
1170
  deps: [],
@@ -1059,7 +1172,7 @@ class FormField {
1059
1172
  });
1060
1173
  static ɵdir = i0.ɵɵngDeclareDirective({
1061
1174
  minVersion: "17.1.0",
1062
- version: "22.0.0-next.3",
1175
+ version: "22.0.0-next.5",
1063
1176
  type: FormField,
1064
1177
  isStandalone: true,
1065
1178
  selector: "[formField]",
@@ -1079,7 +1192,7 @@ class FormField {
1079
1192
  provide: NgControl,
1080
1193
  useFactory: () => inject(FormField).interopNgControl
1081
1194
  }, {
1082
- provide: FORM_FIELD_PARSE_ERRORS,
1195
+ provide: _FORM_FIELD_PARSE_ERRORS,
1083
1196
  useFactory: () => inject(FormField).parseErrorsSource
1084
1197
  }],
1085
1198
  exportAs: ["formField"],
@@ -1091,7 +1204,7 @@ class FormField {
1091
1204
  }
1092
1205
  i0.ɵɵngDeclareClassMetadata({
1093
1206
  minVersion: "12.0.0",
1094
- version: "22.0.0-next.3",
1207
+ version: "22.0.0-next.5",
1095
1208
  ngImport: i0,
1096
1209
  type: FormField,
1097
1210
  decorators: [{
@@ -1106,7 +1219,7 @@ i0.ɵɵngDeclareClassMetadata({
1106
1219
  provide: NgControl,
1107
1220
  useFactory: () => inject(FormField).interopNgControl
1108
1221
  }, {
1109
- provide: FORM_FIELD_PARSE_ERRORS,
1222
+ provide: _FORM_FIELD_PARSE_ERRORS,
1110
1223
  useFactory: () => inject(FormField).parseErrorsSource
1111
1224
  }]
1112
1225
  }]
@@ -1132,11 +1245,17 @@ class FormRoot {
1132
1245
  });
1133
1246
  onSubmit(event) {
1134
1247
  event.preventDefault();
1135
- submit(this.fieldTree());
1248
+ untracked(() => {
1249
+ const fieldTree = this.fieldTree();
1250
+ const node = fieldTree();
1251
+ if (node.structure.fieldManager.submitOptions) {
1252
+ submit(fieldTree);
1253
+ }
1254
+ });
1136
1255
  }
1137
1256
  static ɵfac = i0.ɵɵngDeclareFactory({
1138
1257
  minVersion: "12.0.0",
1139
- version: "22.0.0-next.3",
1258
+ version: "22.0.0-next.5",
1140
1259
  ngImport: i0,
1141
1260
  type: FormRoot,
1142
1261
  deps: [],
@@ -1144,7 +1263,7 @@ class FormRoot {
1144
1263
  });
1145
1264
  static ɵdir = i0.ɵɵngDeclareDirective({
1146
1265
  minVersion: "17.1.0",
1147
- version: "22.0.0-next.3",
1266
+ version: "22.0.0-next.5",
1148
1267
  type: FormRoot,
1149
1268
  isStandalone: true,
1150
1269
  selector: "form[formRoot]",
@@ -1170,7 +1289,7 @@ class FormRoot {
1170
1289
  }
1171
1290
  i0.ɵɵngDeclareClassMetadata({
1172
1291
  minVersion: "12.0.0",
1173
- version: "22.0.0-next.3",
1292
+ version: "22.0.0-next.5",
1174
1293
  ngImport: i0,
1175
1294
  type: FormRoot,
1176
1295
  decorators: [{
@@ -1195,5 +1314,5 @@ i0.ɵɵngDeclareClassMetadata({
1195
1314
  }
1196
1315
  });
1197
1316
 
1198
- export { BaseNgValidationError, EmailValidationError, FORM_FIELD, FormField, FormRoot, MAX, MAX_LENGTH, MIN, MIN_LENGTH, MaxLengthValidationError, MaxValidationError, MinLengthValidationError, MinValidationError, NativeInputParseError, NgValidationError, PATTERN, PatternValidationError, REQUIRED, RequiredValidationError, StandardSchemaValidationError, createManagedMetadataKey, createMetadataKey, debounce, disabled, email, emailError, hidden, max, maxError, maxLength, maxLengthError, metadata, min, minError, minLength, minLengthError, pattern, patternError, provideSignalFormsConfig, readonly, required, requiredError, standardSchemaError, submit, transformedValue, validate, validateAsync, validateHttp, validateStandardSchema, validateTree, ɵNgFieldDirective };
1317
+ export { BaseNgValidationError, EmailValidationError, FORM_FIELD, FormField, FormRoot, IS_ASYNC_VALIDATION_RESOURCE, MAX, MAX_LENGTH, MIN, MIN_LENGTH, MaxLengthValidationError, MaxValidationError, MinLengthValidationError, MinValidationError, NativeInputParseError, NgValidationError, PATTERN, PatternValidationError, REQUIRED, RequiredValidationError, StandardSchemaValidationError, createManagedMetadataKey, createMetadataKey, debounce, disabled, email, emailError, hidden, max, maxError, maxLength, maxLengthError, metadata, min, minError, minLength, minLengthError, pattern, patternError, provideSignalFormsConfig, readonly, required, requiredError, standardSchemaError, submit, transformedValue, validate, validateAsync, validateHttp, validateStandardSchema, validateTree, ɵNgFieldDirective };
1199
1318
  //# sourceMappingURL=signals.mjs.map