@angular/forms 21.2.0-next.3 → 21.2.0

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,11 +1,11 @@
1
1
  /**
2
- * @license Angular v21.2.0-next.3
2
+ * @license Angular v21.2.0
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, signal, linkedSignal, inject, ɵRuntimeError as _RuntimeError, untracked, input, Renderer2, DestroyRef, computed, Injector, ElementRef, afterRenderEffect, effect, Directive } from '@angular/core';
8
+ import { InjectionToken, ɵisPromise as _isPromise, resource, linkedSignal, inject, ɵRuntimeError as _RuntimeError, untracked, input, Renderer2, DestroyRef, computed, Injector, ElementRef, signal, afterRenderEffect, effect, ɵformatRuntimeError as _formatRuntimeError, Directive } from '@angular/core';
9
9
  import { assertPathIsCurrent, FieldPathNode, addDefaultField, metadata, createMetadataKey, MAX, MAX_LENGTH, MIN, MIN_LENGTH, PATTERN, REQUIRED, createManagedMetadataKey, DEBOUNCER, signalErrorsToValidationErrors, submit } from './_validation_errors-chunk.mjs';
10
10
  export { MetadataKey, MetadataReducer, apply, applyEach, applyWhen, applyWhenValue, form, schema } from './_validation_errors-chunk.mjs';
11
11
  import { httpResource } from '@angular/common/http';
@@ -68,6 +68,15 @@ function isEmpty(value) {
68
68
  }
69
69
  return value === '' || value === false || value == null;
70
70
  }
71
+ function normalizeErrors(error) {
72
+ if (error === undefined) {
73
+ return [];
74
+ }
75
+ if (Array.isArray(error)) {
76
+ return error;
77
+ }
78
+ return [error];
79
+ }
71
80
 
72
81
  function validate(path, logic) {
73
82
  assertPathIsCurrent(path);
@@ -155,6 +164,9 @@ class PatternValidationError extends BaseNgValidationError {
155
164
  class EmailValidationError extends BaseNgValidationError {
156
165
  kind = 'email';
157
166
  }
167
+ class NativeInputParseError extends BaseNgValidationError {
168
+ kind = 'parse';
169
+ }
158
170
  const NgValidationError = BaseNgValidationError;
159
171
 
160
172
  const EMAIL_REGEXP = /^(?=.{1,254}$)(?=.{1,64}@)[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
@@ -467,38 +479,54 @@ function immediate() {}
467
479
 
468
480
  const FORM_FIELD_PARSE_ERRORS = new InjectionToken(typeof ngDevMode !== 'undefined' && ngDevMode ? 'FORM_FIELD_PARSE_ERRORS' : '');
469
481
 
482
+ function createParser(getValue, setValue, parse) {
483
+ const errors = linkedSignal({
484
+ ...(ngDevMode ? {
485
+ debugName: "errors"
486
+ } : {}),
487
+ source: getValue,
488
+ computation: () => []
489
+ });
490
+ const setRawValue = rawValue => {
491
+ const result = parse(rawValue);
492
+ errors.set(normalizeErrors(result.error));
493
+ if (result.value !== undefined) {
494
+ setValue(result.value);
495
+ }
496
+ errors.set(normalizeErrors(result.error));
497
+ };
498
+ return {
499
+ errors: errors.asReadonly(),
500
+ setRawValue
501
+ };
502
+ }
503
+
470
504
  function transformedValue(value, options) {
471
505
  const {
472
506
  parse,
473
507
  format
474
508
  } = options;
475
- const parseErrors = signal([], ...(ngDevMode ? [{
476
- debugName: "parseErrors"
477
- }] : []));
478
- const rawValue = linkedSignal(() => format(value()), ...(ngDevMode ? [{
479
- debugName: "rawValue"
480
- }] : []));
509
+ const parser = createParser(value, value.set, parse);
481
510
  const formFieldParseErrors = inject(FORM_FIELD_PARSE_ERRORS, {
482
511
  self: true,
483
512
  optional: true
484
513
  });
485
514
  if (formFieldParseErrors) {
486
- formFieldParseErrors.set(parseErrors);
515
+ formFieldParseErrors.set(parser.errors);
487
516
  }
517
+ const rawValue = linkedSignal(() => format(value()), ...(ngDevMode ? [{
518
+ debugName: "rawValue"
519
+ }] : []));
488
520
  const result = rawValue;
521
+ result.parseErrors = parser.errors;
489
522
  const originalSet = result.set.bind(result);
490
523
  result.set = newRawValue => {
491
- const result = parse(newRawValue);
492
- parseErrors.set(result.errors ?? []);
493
- if (result.value !== undefined) {
494
- value.set(result.value);
495
- }
524
+ parser.setRawValue(newRawValue);
496
525
  originalSet(newRawValue);
497
526
  };
498
527
  result.update = updateFn => {
499
528
  result.set(updateFn(rawValue()));
500
529
  };
501
- result.parseErrors = parseErrors.asReadonly();
502
530
  return result;
503
531
  }
504
532
 
@@ -621,29 +649,46 @@ function isTextualFormElement(element) {
621
649
  return element.tagName === 'INPUT' || element.tagName === 'TEXTAREA';
622
650
  }
623
651
  function getNativeControlValue(element, currentValue) {
652
+ let modelValue;
653
+ if (element.validity.badInput) {
654
+ return {
655
+ error: new NativeInputParseError()
656
+ };
657
+ }
624
658
  switch (element.type) {
625
659
  case 'checkbox':
626
- return element.checked;
660
+ return {
661
+ value: element.checked
662
+ };
627
663
  case 'number':
628
664
  case 'range':
629
665
  case 'datetime-local':
630
- if (typeof untracked(currentValue) === 'number') {
631
- return element.valueAsNumber;
666
+ modelValue = untracked(currentValue);
667
+ if (typeof modelValue === 'number' || modelValue === null) {
668
+ return {
669
+ value: element.value === '' ? null : element.valueAsNumber
670
+ };
632
671
  }
633
672
  break;
634
673
  case 'date':
635
674
  case 'month':
636
675
  case 'time':
637
676
  case 'week':
638
- const value = untracked(currentValue);
639
- if (value === null || value instanceof Date) {
640
- return element.valueAsDate;
641
- } else if (typeof value === 'number') {
642
- return element.valueAsNumber;
677
+ modelValue = untracked(currentValue);
678
+ if (modelValue === null || modelValue instanceof Date) {
679
+ return {
680
+ value: element.valueAsDate
681
+ };
682
+ } else if (typeof modelValue === 'number') {
683
+ return {
684
+ value: element.valueAsNumber
685
+ };
643
686
  }
644
687
  break;
645
688
  }
646
- return element.value;
689
+ return {
690
+ value: element.value
691
+ };
647
692
  }
648
693
  function setNativeControlValue(element, value) {
649
694
  switch (element.type) {
@@ -659,6 +704,9 @@ function setNativeControlValue(element, value) {
659
704
  if (typeof value === 'number') {
660
705
  setNativeNumberControlValue(element, value);
661
706
  return;
707
+ } else if (value === null) {
708
+ element.value = '';
709
+ return;
662
710
  }
663
711
  break;
664
712
  case 'date':
@@ -803,13 +851,12 @@ function isRelevantSelectMutation(mutation) {
803
851
  return false;
804
852
  }
805
853
 
806
- function nativeControlCreate(host, parent) {
854
+ function nativeControlCreate(host, parent, parseErrorsSource) {
807
855
  let updateMode = false;
808
856
  const input = parent.nativeFormElement;
809
- host.listenToDom('input', () => {
810
- const state = parent.state();
811
- state.controlValue.set(getNativeControlValue(input, state.value));
812
- });
857
+ const parser = createParser(() => parent.state().value(), rawValue => parent.state().controlValue.set(rawValue), () => getNativeControlValue(input, parent.state().value));
858
+ parseErrorsSource.set(parser.errors);
859
+ host.listenToDom('input', () => parser.setRawValue(undefined));
813
860
  host.listenToDom('blur', () => parent.state().markAsTouched());
814
861
  parent.registerAsBinding();
815
862
  if (input.tagName === 'SELECT') {
@@ -843,15 +890,15 @@ function nativeControlCreate(host, parent) {
843
890
  const ɵNgFieldDirective = Symbol();
844
891
  const FORM_FIELD = new InjectionToken(typeof ngDevMode !== 'undefined' && ngDevMode ? 'FORM_FIELD' : '');
845
892
  class FormField {
846
- fieldTree = input.required({
893
+ field = input.required({
847
894
  ...(ngDevMode ? {
848
- debugName: "fieldTree"
895
+ debugName: "field"
849
896
  } : {}),
850
897
  alias: 'formField'
851
898
  });
852
899
  renderer = inject(Renderer2);
853
900
  destroyRef = inject(DestroyRef);
854
- state = computed(() => this.fieldTree()(), ...(ngDevMode ? [{
901
+ state = computed(() => this.field()(), ...(ngDevMode ? [{
855
902
  debugName: "state"
856
903
  }] : []));
857
904
  injector = inject(Injector);
@@ -877,7 +924,7 @@ class FormField {
877
924
  }
878
925
  parseErrors = computed(() => this.parseErrorsSource()?.().map(err => ({
879
926
  ...err,
880
- fieldTree: untracked(this.fieldTree),
927
+ fieldTree: untracked(this.state).fieldTree,
881
928
  formField: this
882
929
  })) ?? [], ...(ngDevMode ? [{
883
930
  debugName: "parseErrors"
@@ -917,12 +964,12 @@ class FormField {
917
964
  }
918
965
  registerAsBinding(bindingOptions) {
919
966
  if (this.isFieldBinding) {
920
- throw new _RuntimeError(1913, ngDevMode && 'FormField already registered as a binding');
967
+ throw new _RuntimeError(1913, typeof ngDevMode !== 'undefined' && ngDevMode && 'FormField already registered as a binding');
921
968
  }
922
969
  this.isFieldBinding = true;
923
970
  this.installClassBindingEffect();
924
971
  if (bindingOptions?.focus) {
925
- this.focuser = bindingOptions.focus;
972
+ this.focuser = focusOptions => bindingOptions.focus(focusOptions);
926
973
  }
927
974
  effect(onCleanup => {
928
975
  const fieldNode = this.state();
@@ -933,6 +980,17 @@ class FormField {
933
980
  }, {
934
981
  injector: this.injector
935
982
  });
983
+ if (typeof ngDevMode !== 'undefined' && ngDevMode) {
984
+ effect(() => {
985
+ const fieldNode = this.state();
986
+ if (fieldNode.hidden()) {
987
+ const path = fieldNode.structure.pathKeys().join('.') || '<root>';
988
+ console.warn(_formatRuntimeError(1916, `Field '${path}' is hidden but is being rendered. ` + `Hidden fields should be removed from the DOM using @if.`));
989
+ }
990
+ }, {
991
+ injector: this.injector
992
+ });
993
+ }
936
994
  }
937
995
  [ɵNgFieldDirective];
938
996
  ɵngControlCreate(host) {
@@ -944,9 +1002,9 @@ class FormField {
944
1002
  } else if (host.customControl) {
945
1003
  this.ɵngControlUpdate = customControlCreate(host, this);
946
1004
  } else if (this.elementIsNativeFormElement) {
947
- this.ɵngControlUpdate = nativeControlCreate(host, this);
1005
+ this.ɵngControlUpdate = nativeControlCreate(host, this, this.parseErrorsSource);
948
1006
  } else {
949
- throw new _RuntimeError(1914, 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.`);
1007
+ 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.`);
950
1008
  }
951
1009
  }
952
1010
  ɵngControlUpdate;
@@ -972,7 +1030,7 @@ class FormField {
972
1030
  }
973
1031
  static ɵfac = i0.ɵɵngDeclareFactory({
974
1032
  minVersion: "12.0.0",
975
- version: "21.2.0-next.3",
1033
+ version: "21.2.0",
976
1034
  ngImport: i0,
977
1035
  type: FormField,
978
1036
  deps: [],
@@ -980,13 +1038,13 @@ class FormField {
980
1038
  });
981
1039
  static ɵdir = i0.ɵɵngDeclareDirective({
982
1040
  minVersion: "17.1.0",
983
- version: "21.2.0-next.3",
1041
+ version: "21.2.0",
984
1042
  type: FormField,
985
1043
  isStandalone: true,
986
1044
  selector: "[formField]",
987
1045
  inputs: {
988
- fieldTree: {
989
- classPropertyName: "fieldTree",
1046
+ field: {
1047
+ classPropertyName: "field",
990
1048
  publicName: "formField",
991
1049
  isSignal: true,
992
1050
  isRequired: true,
@@ -1012,7 +1070,7 @@ class FormField {
1012
1070
  }
1013
1071
  i0.ɵɵngDeclareClassMetadata({
1014
1072
  minVersion: "12.0.0",
1015
- version: "21.2.0-next.3",
1073
+ version: "21.2.0",
1016
1074
  ngImport: i0,
1017
1075
  type: FormField,
1018
1076
  decorators: [{
@@ -1033,7 +1091,7 @@ i0.ɵɵngDeclareClassMetadata({
1033
1091
  }]
1034
1092
  }],
1035
1093
  propDecorators: {
1036
- fieldTree: [{
1094
+ field: [{
1037
1095
  type: i0.Input,
1038
1096
  args: [{
1039
1097
  isSignal: true,
@@ -1057,7 +1115,7 @@ class FormRoot {
1057
1115
  }
1058
1116
  static ɵfac = i0.ɵɵngDeclareFactory({
1059
1117
  minVersion: "12.0.0",
1060
- version: "21.2.0-next.3",
1118
+ version: "21.2.0",
1061
1119
  ngImport: i0,
1062
1120
  type: FormRoot,
1063
1121
  deps: [],
@@ -1065,7 +1123,7 @@ class FormRoot {
1065
1123
  });
1066
1124
  static ɵdir = i0.ɵɵngDeclareDirective({
1067
1125
  minVersion: "17.1.0",
1068
- version: "21.2.0-next.3",
1126
+ version: "21.2.0",
1069
1127
  type: FormRoot,
1070
1128
  isStandalone: true,
1071
1129
  selector: "form[formRoot]",
@@ -1091,7 +1149,7 @@ class FormRoot {
1091
1149
  }
1092
1150
  i0.ɵɵngDeclareClassMetadata({
1093
1151
  minVersion: "12.0.0",
1094
- version: "21.2.0-next.3",
1152
+ version: "21.2.0",
1095
1153
  ngImport: i0,
1096
1154
  type: FormRoot,
1097
1155
  decorators: [{
@@ -1116,5 +1174,5 @@ i0.ɵɵngDeclareClassMetadata({
1116
1174
  }
1117
1175
  });
1118
1176
 
1119
- export { BaseNgValidationError, EmailValidationError, FORM_FIELD, FormField, FormRoot, MAX, MAX_LENGTH, MIN, MIN_LENGTH, MaxLengthValidationError, MaxValidationError, MinLengthValidationError, MinValidationError, 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 };
1177
+ 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 };
1120
1178
  //# sourceMappingURL=signals.mjs.map