@angular/forms 22.0.0-next.4 → 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,5 +1,5 @@
1
1
  /**
2
- * @license Angular v22.0.0-next.4
2
+ * @license Angular v22.0.0-next.5
3
3
  * (c) 2010-2026 Google LLC. https://angular.dev/
4
4
  * License: MIT
5
5
  */
@@ -1,14 +1,15 @@
1
1
  /**
2
- * @license Angular v22.0.0-next.4
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, resource, ɵisPromise as _isPromise, linkedSignal, inject, ɵRuntimeError as _RuntimeError, untracked, input, computed, Renderer2, DestroyRef, Injector, ElementRef, signal, afterRenderEffect, effect, ɵformatRuntimeError as _formatRuntimeError, Directive } from '@angular/core';
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
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, DEBOUNCER, signalErrorsToValidationErrors, submit } from './_validation_errors-chunk.mjs';
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';
11
11
  export { MetadataKey, MetadataReducer, apply, applyEach, applyWhen, applyWhenValue, form, schema } from './_validation_errors-chunk.mjs';
12
+ import { DOCUMENT, isPlatformBrowser } from '@angular/common';
12
13
  import { httpResource } from '@angular/common/http';
13
14
  import '@angular/core/primitives/signals';
14
15
 
@@ -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
@@ -654,9 +663,9 @@ function bindingUpdated(bindings, key, value) {
654
663
  return false;
655
664
  }
656
665
 
657
- function getNativeControlValue(element, currentValue) {
666
+ function getNativeControlValue(element, currentValue, validityMonitor) {
658
667
  let modelValue;
659
- if (element.validity.badInput) {
668
+ if (isInput(element) && validityMonitor.isBadInput(element)) {
660
669
  return {
661
670
  error: new NativeInputParseError()
662
671
  };
@@ -692,6 +701,25 @@ function getNativeControlValue(element, currentValue) {
692
701
  }
693
702
  break;
694
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
+ }
695
723
  return {
696
724
  value: element.value
697
725
  };
@@ -727,6 +755,16 @@ function setNativeControlValue(element, value) {
727
755
  return;
728
756
  }
729
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
+ }
730
768
  element.value = value;
731
769
  }
732
770
  function setNativeNumberControlValue(element, value) {
@@ -736,6 +774,12 @@ function setNativeNumberControlValue(element, value) {
736
774
  element.valueAsNumber = value;
737
775
  }
738
776
  }
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';
782
+ }
739
783
 
740
784
  function customControlCreate(host, parent) {
741
785
  host.listenToCustomControlModel(value => parent.state().controlValue.set(value));
@@ -831,13 +875,16 @@ function isRelevantSelectMutation(mutation) {
831
875
  return false;
832
876
  }
833
877
 
834
- function nativeControlCreate(host, parent, parseErrorsSource) {
878
+ function nativeControlCreate(host, parent, parseErrorsSource, validityMonitor) {
835
879
  let updateMode = false;
836
880
  const input = parent.nativeFormElement;
837
- 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));
838
882
  parseErrorsSource.set(parser.errors);
839
883
  host.listenToDom('input', () => parser.setRawValue(undefined));
840
884
  host.listenToDom('blur', () => parent.state().markAsTouched());
885
+ if (isInput(input) && inputRequiresValidityTracking(input)) {
886
+ validityMonitor.watchValidity(input, () => parser.setRawValue(undefined));
887
+ }
841
888
  parent.registerAsBinding();
842
889
  if (input.tagName === 'SELECT') {
843
890
  observeSelectMutations(input, () => {
@@ -867,6 +914,112 @@ function nativeControlCreate(host, parent, parseErrorsSource) {
867
914
  };
868
915
  }
869
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
+
870
1023
  const ɵNgFieldDirective = Symbol();
871
1024
  const FORM_FIELD = new InjectionToken(typeof ngDevMode !== 'undefined' && ngDevMode ? 'FORM_FIELD' : '');
872
1025
  class FormField {
@@ -895,6 +1048,7 @@ class FormField {
895
1048
  config = inject(SIGNAL_FORMS_CONFIG, {
896
1049
  optional: true
897
1050
  });
1051
+ validityMonitor = inject(InputValidityMonitor);
898
1052
  parseErrorsSource = signal(undefined, ...(ngDevMode ? [{
899
1053
  debugName: "parseErrorsSource"
900
1054
  }] : []));
@@ -982,7 +1136,7 @@ class FormField {
982
1136
  } else if (host.customControl) {
983
1137
  this.ɵngControlUpdate = customControlCreate(host, this);
984
1138
  } else if (this.elementIsNativeFormElement) {
985
- this.ɵngControlUpdate = nativeControlCreate(host, this, this.parseErrorsSource);
1139
+ this.ɵngControlUpdate = nativeControlCreate(host, this, this.parseErrorsSource, this.validityMonitor);
986
1140
  } else {
987
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.`);
988
1142
  }
@@ -1010,7 +1164,7 @@ class FormField {
1010
1164
  }
1011
1165
  static ɵfac = i0.ɵɵngDeclareFactory({
1012
1166
  minVersion: "12.0.0",
1013
- version: "22.0.0-next.4",
1167
+ version: "22.0.0-next.5",
1014
1168
  ngImport: i0,
1015
1169
  type: FormField,
1016
1170
  deps: [],
@@ -1018,7 +1172,7 @@ class FormField {
1018
1172
  });
1019
1173
  static ɵdir = i0.ɵɵngDeclareDirective({
1020
1174
  minVersion: "17.1.0",
1021
- version: "22.0.0-next.4",
1175
+ version: "22.0.0-next.5",
1022
1176
  type: FormField,
1023
1177
  isStandalone: true,
1024
1178
  selector: "[formField]",
@@ -1050,7 +1204,7 @@ class FormField {
1050
1204
  }
1051
1205
  i0.ɵɵngDeclareClassMetadata({
1052
1206
  minVersion: "12.0.0",
1053
- version: "22.0.0-next.4",
1207
+ version: "22.0.0-next.5",
1054
1208
  ngImport: i0,
1055
1209
  type: FormField,
1056
1210
  decorators: [{
@@ -1091,11 +1245,17 @@ class FormRoot {
1091
1245
  });
1092
1246
  onSubmit(event) {
1093
1247
  event.preventDefault();
1094
- 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
+ });
1095
1255
  }
1096
1256
  static ɵfac = i0.ɵɵngDeclareFactory({
1097
1257
  minVersion: "12.0.0",
1098
- version: "22.0.0-next.4",
1258
+ version: "22.0.0-next.5",
1099
1259
  ngImport: i0,
1100
1260
  type: FormRoot,
1101
1261
  deps: [],
@@ -1103,7 +1263,7 @@ class FormRoot {
1103
1263
  });
1104
1264
  static ɵdir = i0.ɵɵngDeclareDirective({
1105
1265
  minVersion: "17.1.0",
1106
- version: "22.0.0-next.4",
1266
+ version: "22.0.0-next.5",
1107
1267
  type: FormRoot,
1108
1268
  isStandalone: true,
1109
1269
  selector: "form[formRoot]",
@@ -1129,7 +1289,7 @@ class FormRoot {
1129
1289
  }
1130
1290
  i0.ɵɵngDeclareClassMetadata({
1131
1291
  minVersion: "12.0.0",
1132
- version: "22.0.0-next.4",
1292
+ version: "22.0.0-next.5",
1133
1293
  ngImport: i0,
1134
1294
  type: FormRoot,
1135
1295
  decorators: [{
@@ -1154,5 +1314,5 @@ i0.ɵɵngDeclareClassMetadata({
1154
1314
  }
1155
1315
  });
1156
1316
 
1157
- 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 };
1158
1318
  //# sourceMappingURL=signals.mjs.map