@angular/forms 21.2.0-next.0 → 21.2.0-next.2

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,13 +1,64 @@
1
1
  /**
2
- * @license Angular v21.2.0-next.0
2
+ * @license Angular v21.2.0-next.2
3
3
  * (c) 2010-2026 Google LLC. https://angular.dev/
4
4
  * License: MIT
5
5
  */
6
6
 
7
+ import { FormGroup, FormArray, AbstractControl } from '@angular/forms';
7
8
  import { untracked, ɵRuntimeError as _RuntimeError, computed, runInInjectionContext, Injector, linkedSignal, signal, APP_ID, effect, inject } from '@angular/core';
8
- import { AbstractControl } from '@angular/forms';
9
9
  import { SIGNAL } from '@angular/core/primitives/signals';
10
10
 
11
+ class CompatValidationError {
12
+ kind = 'compat';
13
+ control;
14
+ fieldTree;
15
+ context;
16
+ message;
17
+ constructor({
18
+ context,
19
+ kind,
20
+ control
21
+ }) {
22
+ this.context = context;
23
+ this.kind = kind;
24
+ this.control = control;
25
+ }
26
+ }
27
+ function signalErrorsToValidationErrors(errors) {
28
+ if (errors.length === 0) {
29
+ return null;
30
+ }
31
+ const errObj = {};
32
+ for (const error of errors) {
33
+ errObj[error.kind] = error instanceof CompatValidationError ? error.context : error;
34
+ }
35
+ return errObj;
36
+ }
37
+ function reactiveErrorsToSignalErrors(errors, control) {
38
+ if (errors === null) {
39
+ return [];
40
+ }
41
+ return Object.entries(errors).map(([kind, context]) => {
42
+ return new CompatValidationError({
43
+ context,
44
+ kind,
45
+ control
46
+ });
47
+ });
48
+ }
49
+ function extractNestedReactiveErrors(control) {
50
+ const errors = [];
51
+ if (control.errors) {
52
+ errors.push(...reactiveErrorsToSignalErrors(control.errors, control));
53
+ }
54
+ if (control instanceof FormGroup || control instanceof FormArray) {
55
+ for (const c of Object.values(control.controls)) {
56
+ errors.push(...extractNestedReactiveErrors(c));
57
+ }
58
+ }
59
+ return errors;
60
+ }
61
+
11
62
  let boundPathDepth = 0;
12
63
  function getBoundPathDepth() {
13
64
  return boundPathDepth;
@@ -647,10 +698,19 @@ class FieldValidationState {
647
698
  }, ...(ngDevMode ? [{
648
699
  debugName: "asyncErrors"
649
700
  }] : []));
650
- errors = computed(() => [...this.syncErrors(), ...this.asyncErrors().filter(err => err !== 'pending')], ...(ngDevMode ? [{
701
+ parseErrors = computed(() => this.node.formFieldBindings().flatMap(field => field.parseErrors()), ...(ngDevMode ? [{
702
+ debugName: "parseErrors"
703
+ }] : []));
704
+ errors = computed(() => [...this.parseErrors(), ...this.syncErrors(), ...this.asyncErrors().filter(err => err !== 'pending')], ...(ngDevMode ? [{
651
705
  debugName: "errors"
652
706
  }] : []));
653
- errorSummary = computed(() => this.node.structure.reduceChildren(this.errors(), (child, result) => [...result, ...child.errorSummary()]), ...(ngDevMode ? [{
707
+ errorSummary = computed(() => {
708
+ const errors = this.node.structure.reduceChildren(this.errors(), (child, result) => [...result, ...child.errorSummary()]);
709
+ if (typeof ngServerMode === 'undefined' || !ngServerMode) {
710
+ untracked(() => errors.sort(compareErrorPosition));
711
+ }
712
+ return errors;
713
+ }, ...(ngDevMode ? [{
654
714
  debugName: "errorSummary"
655
715
  }] : []));
656
716
  pending = computed(() => this.node.structure.reduceChildren(this.asyncErrors().includes('pending'), (child, value) => value || child.validationState.asyncErrors().includes('pending')), ...(ngDevMode ? [{
@@ -701,6 +761,20 @@ function addDefaultField(errors, fieldTree) {
701
761
  }
702
762
  return errors;
703
763
  }
764
+ function getFirstBoundElement(error) {
765
+ if (error.formField) return error.formField.element;
766
+ return error.fieldTree().formFieldBindings().reduce((el, binding) => {
767
+ if (!el || !binding.element) return el ?? binding.element;
768
+ return el.compareDocumentPosition(binding.element) & Node.DOCUMENT_POSITION_PRECEDING ? binding.element : el;
769
+ }, undefined);
770
+ }
771
+ function compareErrorPosition(a, b) {
772
+ const aEl = getFirstBoundElement(a);
773
+ const bEl = getFirstBoundElement(b);
774
+ if (aEl === bEl) return 0;
775
+ if (aEl === undefined || bEl === undefined) return aEl === undefined ? 1 : -1;
776
+ return aEl.compareDocumentPosition(bEl) & Node.DOCUMENT_POSITION_PRECEDING ? 1 : -1;
777
+ }
704
778
 
705
779
  const DEBOUNCER = createMetadataKey();
706
780
 
@@ -1163,6 +1237,7 @@ class FieldNode {
1163
1237
  nodeState;
1164
1238
  submitState;
1165
1239
  fieldAdapter;
1240
+ controlValue;
1166
1241
  _context = undefined;
1167
1242
  get context() {
1168
1243
  return this._context ??= new FieldNodeContext(this);
@@ -1177,6 +1252,7 @@ class FieldNode {
1177
1252
  this.nodeState = this.fieldAdapter.createNodeState(this, options);
1178
1253
  this.metadataState = new FieldMetadataState(this);
1179
1254
  this.submitState = new FieldSubmitState(this);
1255
+ this.controlValue = this.controlValueSignal();
1180
1256
  }
1181
1257
  focusBoundControl(options) {
1182
1258
  this.getBindingForFocus()?.focus(options);
@@ -1202,18 +1278,15 @@ class FieldNode {
1202
1278
  get value() {
1203
1279
  return this.structure.value;
1204
1280
  }
1205
- _controlValue = linkedSignal(() => this.value(), ...(ngDevMode ? [{
1206
- debugName: "_controlValue"
1207
- }] : []));
1208
- get controlValue() {
1209
- return this._controlValue.asReadonly();
1210
- }
1211
1281
  get keyInParent() {
1212
1282
  return this.structure.keyInParent;
1213
1283
  }
1214
1284
  get errors() {
1215
1285
  return this.validationState.errors;
1216
1286
  }
1287
+ get parseErrors() {
1288
+ return this.validationState.parseErrors;
1289
+ }
1217
1290
  get errorSummary() {
1218
1291
  return this.validationState.errorSummary;
1219
1292
  }
@@ -1286,6 +1359,12 @@ class FieldNode {
1286
1359
  markAsDirty() {
1287
1360
  this.nodeState.markAsDirty();
1288
1361
  }
1362
+ markAsPristine() {
1363
+ this.nodeState.markAsPristine();
1364
+ }
1365
+ markAsUntouched() {
1366
+ this.nodeState.markAsUntouched();
1367
+ }
1289
1368
  reset(value) {
1290
1369
  untracked(() => this._reset(value));
1291
1370
  }
@@ -1299,12 +1378,25 @@ class FieldNode {
1299
1378
  child._reset();
1300
1379
  }
1301
1380
  }
1302
- setControlValue(newValue) {
1303
- untracked(() => {
1304
- this._controlValue.set(newValue);
1381
+ controlValueSignal() {
1382
+ const controlValue = linkedSignal(this.value, ...(ngDevMode ? [{
1383
+ debugName: "controlValue"
1384
+ }] : []));
1385
+ const {
1386
+ set,
1387
+ update
1388
+ } = controlValue;
1389
+ controlValue.set = newValue => {
1390
+ set(newValue);
1305
1391
  this.markAsDirty();
1306
1392
  this.debounceSync();
1307
- });
1393
+ };
1394
+ controlValue.update = updateFn => {
1395
+ update(updateFn);
1396
+ this.markAsDirty();
1397
+ this.debounceSync();
1398
+ };
1399
+ return controlValue;
1308
1400
  }
1309
1401
  sync() {
1310
1402
  this.value.set(this.controlValue());
@@ -1317,8 +1409,10 @@ class FieldNode {
1317
1409
  }
1318
1410
  }
1319
1411
  async debounceSync() {
1320
- this.pendingSync()?.abort();
1321
- const debouncer = this.nodeState.debouncer();
1412
+ const debouncer = untracked(() => {
1413
+ this.pendingSync()?.abort();
1414
+ return this.nodeState.debouncer();
1415
+ });
1322
1416
  if (debouncer) {
1323
1417
  const controller = new AbortController();
1324
1418
  const promise = debouncer(controller.signal);
@@ -1476,9 +1570,11 @@ class BasicFieldAdapter {
1476
1570
  class FormFieldManager {
1477
1571
  injector;
1478
1572
  rootName;
1479
- constructor(injector, rootName) {
1573
+ submitOptions;
1574
+ constructor(injector, rootName, submitOptions) {
1480
1575
  this.injector = injector;
1481
1576
  this.rootName = rootName ?? `${this.injector.get(APP_ID)}.form${nextFormId++}`;
1577
+ this.submitOptions = submitOptions;
1482
1578
  }
1483
1579
  structures = new Set();
1484
1580
  createFieldManagementEffect(root) {
@@ -1526,7 +1622,7 @@ function form(...args) {
1526
1622
  const [model, schema, options] = normalizeFormArgs(args);
1527
1623
  const injector = options?.injector ?? inject(Injector);
1528
1624
  const pathNode = runInInjectionContext(injector, () => SchemaImpl.rootCompile(schema));
1529
- const fieldManager = new FormFieldManager(injector, options?.name);
1625
+ const fieldManager = new FormFieldManager(injector, options?.name, options?.submission);
1530
1626
  const adapter = options?.adapter ?? new BasicFieldAdapter();
1531
1627
  const fieldRoot = FieldNode.newRoot(fieldManager, model, pathNode, adapter);
1532
1628
  fieldManager.createFieldManagementEffect(fieldRoot.structure);
@@ -1555,16 +1651,39 @@ function applyWhenValue(path, predicate, schema) {
1555
1651
  value
1556
1652
  }) => predicate(value()), schema);
1557
1653
  }
1558
- async function submit(form, action) {
1654
+ async function submit(form, options) {
1559
1655
  const node = form();
1560
- markAllAsTouched(node);
1561
- if (node.invalid()) {
1562
- return;
1563
- }
1564
- node.submitState.selfSubmitting.set(true);
1656
+ const opts = typeof options === 'function' ? {
1657
+ action: options
1658
+ } : {
1659
+ ...(node.structure.fieldManager.submitOptions ?? {}),
1660
+ ...(options ?? {})
1661
+ };
1662
+ const action = opts?.action;
1663
+ if (!action) {
1664
+ throw new _RuntimeError(1915, ngDevMode && 'Cannot submit form with no submit action. Specify the action when creating the form, or as an additional argument to `submit()`.');
1665
+ }
1666
+ const onInvalid = opts?.onInvalid;
1667
+ const ignoreValidators = opts?.ignoreValidators ?? 'pending';
1668
+ let shouldRunAction = true;
1669
+ untracked(() => {
1670
+ markAllAsTouched(node);
1671
+ if (ignoreValidators === 'none') {
1672
+ shouldRunAction = node.valid();
1673
+ } else if (ignoreValidators === 'pending') {
1674
+ shouldRunAction = !node.invalid();
1675
+ }
1676
+ });
1565
1677
  try {
1566
- const errors = await action(form);
1567
- errors && setSubmissionErrors(node, errors);
1678
+ if (shouldRunAction) {
1679
+ node.submitState.selfSubmitting.set(true);
1680
+ const errors = await untracked(() => action?.(form));
1681
+ errors && setSubmissionErrors(node, errors);
1682
+ return !errors || isArray(errors) && errors.length === 0;
1683
+ } else {
1684
+ untracked(() => onInvalid?.(form));
1685
+ }
1686
+ return false;
1568
1687
  } finally {
1569
1688
  node.submitState.selfSubmitting.set(false);
1570
1689
  }
@@ -1592,11 +1711,14 @@ function schema(fn) {
1592
1711
  return SchemaImpl.create(fn);
1593
1712
  }
1594
1713
  function markAllAsTouched(node) {
1714
+ if (node.validationState.shouldSkipValidation()) {
1715
+ return;
1716
+ }
1595
1717
  node.markAsTouched();
1596
1718
  for (const child of node.structure.children()) {
1597
1719
  markAllAsTouched(child);
1598
1720
  }
1599
1721
  }
1600
1722
 
1601
- export { BasicFieldAdapter, DEBOUNCER, FieldNode, FieldNodeState, FieldNodeStructure, FieldPathNode, MAX, MAX_LENGTH, MIN, MIN_LENGTH, MetadataKey, MetadataReducer, PATTERN, REQUIRED, addDefaultField, apply, applyEach, applyWhen, applyWhenValue, assertPathIsCurrent, calculateValidationSelfStatus, createManagedMetadataKey, createMetadataKey, form, getInjectorFromOptions, metadata, normalizeFormArgs, schema, submit };
1723
+ export { BasicFieldAdapter, CompatValidationError, DEBOUNCER, FieldNode, FieldNodeState, FieldNodeStructure, FieldPathNode, MAX, MAX_LENGTH, MIN, MIN_LENGTH, MetadataKey, MetadataReducer, PATTERN, REQUIRED, addDefaultField, apply, applyEach, applyWhen, applyWhenValue, assertPathIsCurrent, calculateValidationSelfStatus, createManagedMetadataKey, createMetadataKey, extractNestedReactiveErrors, form, getInjectorFromOptions, metadata, normalizeFormArgs, schema, signalErrorsToValidationErrors, submit };
1602
1724
  //# sourceMappingURL=_structure-chunk.mjs.map