@angular/forms 21.0.0-next.2 → 21.0.0-next.4

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,14 +1,14 @@
1
1
  /**
2
- * @license Angular v21.0.0-next.2
2
+ * @license Angular v21.0.0-next.4
3
3
  * (c) 2010-2025 Google LLC. https://angular.io/
4
4
  * License: MIT
5
5
  */
6
6
 
7
7
  import { httpResource } from '@angular/common/http';
8
8
  import * as i0 from '@angular/core';
9
- import { computed, untracked, runInInjectionContext, linkedSignal, Injector, signal, APP_ID, effect, inject, ɵSIGNAL as _SIGNAL, Renderer2, ElementRef, afterNextRender, DestroyRef, Directive, Input, reflectComponentType, OutputEmitterRef, EventEmitter } from '@angular/core';
10
- import { SIGNAL } from '@angular/core/primitives/signals';
9
+ import { computed, untracked, ɵSIGNAL as _SIGNAL, inject, Injector, Renderer2, signal, ElementRef, effect, afterNextRender, DestroyRef, Directive, Input, reflectComponentType, OutputEmitterRef, EventEmitter, runInInjectionContext, linkedSignal, APP_ID, ɵisPromise as _isPromise, resource } from '@angular/core';
11
10
  import { Validators, NG_VALUE_ACCESSOR, NgControl } from '@angular/forms';
11
+ import { SIGNAL } from '@angular/core/primitives/signals';
12
12
 
13
13
  /**
14
14
  * A version of `Array.isArray` that handles narrowing of readonly arrays properly.
@@ -1071,13 +1071,19 @@ function assertPathIsCurrent(path) {
1071
1071
  /**
1072
1072
  * Represents a property that may be defined on a field when it is created using a `property` rule
1073
1073
  * in the schema. A particular `Property` can only be defined on a particular field **once**.
1074
+ *
1075
+ * @experimental 21.0.0
1074
1076
  */
1075
1077
  class Property {
1076
1078
  brand;
1077
1079
  /** Use {@link createProperty}. */
1078
1080
  constructor() { }
1079
1081
  }
1080
- /** Creates a {@link Property}. */
1082
+ /**
1083
+ * Creates a {@link Property}.
1084
+ *
1085
+ * @experimental 21.0.0
1086
+ */
1081
1087
  function createProperty() {
1082
1088
  return new Property();
1083
1089
  }
@@ -1086,6 +1092,8 @@ function createProperty() {
1086
1092
  * function. A value can be contributed to the aggregated value for a field using an
1087
1093
  * `aggregateProperty` rule in the schema. There may be multiple rules in a schema that contribute
1088
1094
  * values to the same `AggregateProperty` of the same field.
1095
+ *
1096
+ * @experimental 21.0.0
1089
1097
  */
1090
1098
  class AggregateProperty {
1091
1099
  reduce;
@@ -1102,18 +1110,24 @@ class AggregateProperty {
1102
1110
  * the given `reduce` and `getInitial` functions.
1103
1111
  * @param reduce The reducer function.
1104
1112
  * @param getInitial A function that gets the initial value for the reduce operation.
1113
+ *
1114
+ * @experimental 21.0.0
1105
1115
  */
1106
1116
  function reducedProperty(reduce, getInitial) {
1107
1117
  return new AggregateProperty(reduce, getInitial);
1108
1118
  }
1109
1119
  /**
1110
1120
  * Creates an aggregate property that reduces its individual values into a list.
1121
+ *
1122
+ * @experimental 21.0.0
1111
1123
  */
1112
1124
  function listProperty() {
1113
1125
  return reducedProperty((acc, item) => (item === undefined ? acc : [...acc, item]), () => []);
1114
1126
  }
1115
1127
  /**
1116
1128
  * Creates an aggregate property that reduces its individual values by taking their min.
1129
+ *
1130
+ * @experimental 21.0.0
1117
1131
  */
1118
1132
  function minProperty() {
1119
1133
  return reducedProperty((prev, next) => {
@@ -1128,6 +1142,8 @@ function minProperty() {
1128
1142
  }
1129
1143
  /**
1130
1144
  * Creates an aggregate property that reduces its individual values by taking their max.
1145
+ *
1146
+ * @experimental 21.0.0
1131
1147
  */
1132
1148
  function maxProperty() {
1133
1149
  return reducedProperty((prev, next) => {
@@ -1142,38 +1158,54 @@ function maxProperty() {
1142
1158
  }
1143
1159
  /**
1144
1160
  * Creates an aggregate property that reduces its individual values by logically or-ing them.
1161
+ *
1162
+ * @experimental 21.0.0
1145
1163
  */
1146
1164
  function orProperty() {
1147
1165
  return reducedProperty((prev, next) => prev || next, () => false);
1148
1166
  }
1149
1167
  /**
1150
1168
  * Creates an aggregate property that reduces its individual values by logically and-ing them.
1169
+ *
1170
+ * @experimental 21.0.0
1151
1171
  */
1152
1172
  function andProperty() {
1153
1173
  return reducedProperty((prev, next) => prev && next, () => true);
1154
1174
  }
1155
1175
  /**
1156
1176
  * An aggregate property representing whether the field is required.
1177
+ *
1178
+ * @experimental 21.0.0
1157
1179
  */
1158
1180
  const REQUIRED = orProperty();
1159
1181
  /**
1160
1182
  * An aggregate property representing the min value of the field.
1183
+ *
1184
+ * @experimental 21.0.0
1161
1185
  */
1162
1186
  const MIN = maxProperty();
1163
1187
  /**
1164
1188
  * An aggregate property representing the max value of the field.
1189
+ *
1190
+ * @experimental 21.0.0
1165
1191
  */
1166
1192
  const MAX = minProperty();
1167
1193
  /**
1168
1194
  * An aggregate property representing the min length of the field.
1195
+ *
1196
+ * @experimental 21.0.0
1169
1197
  */
1170
1198
  const MIN_LENGTH = maxProperty();
1171
1199
  /**
1172
1200
  * An aggregate property representing the max length of the field.
1201
+ *
1202
+ * @experimental 21.0.0
1173
1203
  */
1174
1204
  const MAX_LENGTH = minProperty();
1175
1205
  /**
1176
1206
  * An aggregate property representing the patterns the field must match.
1207
+ *
1208
+ * @experimental 21.0.0
1177
1209
  */
1178
1210
  const PATTERN = listProperty();
1179
1211
 
@@ -1186,6 +1218,8 @@ const PATTERN = listProperty();
1186
1218
  * and `false` when it is not disabled.
1187
1219
  * @template TValue The type of value stored in the field the logic is bound to.
1188
1220
  * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
1221
+ *
1222
+ * @experimental 21.0.0
1189
1223
  */
1190
1224
  function disabled(path, logic) {
1191
1225
  assertPathIsCurrent(path);
@@ -1212,6 +1246,8 @@ function disabled(path, logic) {
1212
1246
  * @param logic A reactive function that returns `true` when the field is readonly.
1213
1247
  * @template TValue The type of value stored in the field the logic is bound to.
1214
1248
  * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
1249
+ *
1250
+ * @experimental 21.0.0
1215
1251
  */
1216
1252
  function readonly(path, logic = () => true) {
1217
1253
  assertPathIsCurrent(path);
@@ -1234,6 +1270,8 @@ function readonly(path, logic = () => true) {
1234
1270
  * @param logic A reactive function that returns `true` when the field is hidden.
1235
1271
  * @template TValue The type of value stored in the field the logic is bound to.
1236
1272
  * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
1273
+ *
1274
+ * @experimental 21.0.0
1237
1275
  */
1238
1276
  function hidden(path, logic) {
1239
1277
  assertPathIsCurrent(path);
@@ -1247,6 +1285,8 @@ function hidden(path, logic) {
1247
1285
  * @param logic A `Validator` that returns the current validation errors.
1248
1286
  * @template TValue The type of value stored in the field the logic is bound to.
1249
1287
  * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
1288
+ *
1289
+ * @experimental 21.0.0
1250
1290
  */
1251
1291
  function validate(path, logic) {
1252
1292
  assertPathIsCurrent(path);
@@ -1261,6 +1301,8 @@ function validate(path, logic) {
1261
1301
  * Errors returned by the validator may specify a target field to indicate an error on a child field.
1262
1302
  * @template TValue The type of value stored in the field the logic is bound to.
1263
1303
  * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
1304
+ *
1305
+ * @experimental 21.0.0
1264
1306
  */
1265
1307
  function validateTree(path, logic) {
1266
1308
  assertPathIsCurrent(path);
@@ -1276,6 +1318,8 @@ function validateTree(path, logic) {
1276
1318
  * @template TValue The type of value stored in the field the logic is bound to.
1277
1319
  * @template TPropItem The type of value the property aggregates over.
1278
1320
  * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
1321
+ *
1322
+ * @experimental 21.0.0
1279
1323
  */
1280
1324
  function aggregateProperty(path, prop, logic) {
1281
1325
  assertPathIsCurrent(path);
@@ -1308,6 +1352,8 @@ function property(path, ...rest) {
1308
1352
  * @template TParams The type of parameters to the resource.
1309
1353
  * @template TResult The type of result returned by the resource
1310
1354
  * @template TPathKind The kind of path being validated (a root path, child path, or item of an array)
1355
+ *
1356
+ * @experimental 21.0.0
1311
1357
  */
1312
1358
  function validateAsync(path, opts) {
1313
1359
  assertPathIsCurrent(path);
@@ -1353,6 +1399,8 @@ function validateAsync(path, opts) {
1353
1399
  * @template TValue The type of value stored in the field being validated.
1354
1400
  * @template TResult The type of result returned by the httpResource
1355
1401
  * @template TPathKind The kind of path being validated (a root path, child path, or item of an array)
1402
+ *
1403
+ * @experimental 21.0.0
1356
1404
  */
1357
1405
  function validateHttp(path, opts) {
1358
1406
  validateAsync(path, {
@@ -1363,332 +1411,852 @@ function validateHttp(path, opts) {
1363
1411
  }
1364
1412
 
1365
1413
  /**
1366
- * `FieldContext` implementation, backed by a `FieldNode`.
1414
+ * A fake version of `NgControl` provided by the `Control` directive. This allows interoperability
1415
+ * with a wider range of components designed to work with reactive forms, in particular ones that
1416
+ * inject the `NgControl`. The interop control does not implement *all* properties and methods of
1417
+ * the real `NgControl`, but does implement some of the most commonly used ones that have a clear
1418
+ * equivalent in signal forms.
1367
1419
  */
1368
- class FieldNodeContext {
1369
- node;
1370
- /**
1371
- * Cache of paths that have been resolved for this context.
1372
- *
1373
- * For each resolved path we keep track of a signal of field that it maps to rather than a static
1374
- * field, since it theoretically could change. In practice for the current system it should not
1375
- * actually change, as they only place we currently track fields moving within the parent
1376
- * structure is for arrays, and paths do not currently support array indexing.
1377
- */
1378
- cache = new WeakMap();
1379
- constructor(
1380
- /** The field node this context corresponds to. */
1381
- node) {
1382
- this.node = node;
1420
+ class InteropNgControl {
1421
+ field;
1422
+ constructor(field) {
1423
+ this.field = field;
1383
1424
  }
1384
- /**
1385
- * Resolves a target path relative to this context.
1386
- * @param target The path to resolve
1387
- * @returns The field corresponding to the target path.
1388
- */
1389
- resolve(target) {
1390
- if (!this.cache.has(target)) {
1391
- const resolver = computed(() => {
1392
- const targetPathNode = FieldPathNode.unwrapFieldPath(target);
1393
- // First, find the field where the root our target path was merged in.
1394
- // We determine this by walking up the field tree from the current field and looking for
1395
- // the place where the LogicNodeBuilder from the target path's root was merged in.
1396
- // We always make sure to walk up at least as far as the depth of the path we were bound to.
1397
- // This ensures that we do not accidentally match on the wrong application of a recursively
1398
- // applied schema.
1399
- let field = this.node;
1400
- let stepsRemaining = getBoundPathDepth();
1401
- while (stepsRemaining > 0 || !field.structure.logic.hasLogic(targetPathNode.root.logic)) {
1402
- stepsRemaining--;
1403
- field = field.structure.parent;
1404
- if (field === undefined) {
1405
- throw new Error('Path is not part of this field tree.');
1406
- }
1407
- }
1408
- // Now, we can navigate to the target field using the relative path in the target path node
1409
- // to traverse down from the field we just found.
1410
- for (let key of targetPathNode.keys) {
1411
- field = field.structure.getChild(key);
1412
- if (field === undefined) {
1413
- throw new Error(`Cannot resolve path .${targetPathNode.keys.join('.')} relative to field ${[
1414
- '<root>',
1415
- ...this.node.structure.pathKeys(),
1416
- ].join('.')}.`);
1417
- }
1418
- }
1419
- return field.fieldProxy;
1420
- }, ...(ngDevMode ? [{ debugName: "resolver" }] : []));
1421
- this.cache.set(target, resolver);
1422
- }
1423
- return this.cache.get(target)();
1425
+ control = this;
1426
+ get value() {
1427
+ return this.field().value();
1424
1428
  }
1425
- get field() {
1426
- return this.node.fieldProxy;
1429
+ get valid() {
1430
+ return this.field().valid();
1427
1431
  }
1428
- get state() {
1429
- return this.node;
1432
+ get invalid() {
1433
+ return this.field().invalid();
1430
1434
  }
1431
- get value() {
1432
- return this.node.structure.value;
1435
+ get pending() {
1436
+ return this.field().pending();
1433
1437
  }
1434
- get key() {
1435
- return this.node.structure.keyInParent;
1438
+ get disabled() {
1439
+ return this.field().disabled();
1436
1440
  }
1437
- index = computed(() => {
1438
- // Attempt to read the key first, this will throw an error if we're on a root field.
1439
- const key = this.key();
1440
- // Assert that the parent is actually an array.
1441
- if (!isArray(untracked(this.node.structure.parent.value))) {
1442
- throw new Error(`RuntimeError: cannot access index, parent field is not an array`);
1443
- }
1444
- // Return the key as a number if we are indeed inside an array field.
1445
- return Number(key);
1446
- }, ...(ngDevMode ? [{ debugName: "index" }] : []));
1447
- fieldOf = (p) => this.resolve(p);
1448
- stateOf = (p) => this.resolve(p)();
1449
- valueOf = (p) => this.resolve(p)().value();
1450
- }
1451
-
1452
- /**
1453
- * Tracks custom properties associated with a `FieldNode`.
1454
- */
1455
- class FieldPropertyState {
1456
- node;
1457
- /** A map of all `Property` and `AggregateProperty` that have been defined for this field. */
1458
- properties = new Map();
1459
- constructor(node) {
1460
- this.node = node;
1461
- // Field nodes (and thus their property state) are created in a linkedSignal in order to mirror
1462
- // the structure of the model data. We need to run the property factories untracked so that they
1463
- // do not cause recomputation of the linkedSignal.
1464
- untracked(() =>
1465
- // Property factories are run in the form's injection context so they can create resources
1466
- // and inject DI dependencies.
1467
- runInInjectionContext(this.node.structure.injector, () => {
1468
- for (const [key, factory] of this.node.logicNode.logic.getPropertyFactoryEntries()) {
1469
- this.properties.set(key, factory(this.node.context));
1470
- }
1471
- }));
1441
+ get enabled() {
1442
+ return !this.field().disabled();
1472
1443
  }
1473
- /** Gets the value of a `Property` or `AggregateProperty` for the field. */
1474
- get(prop) {
1475
- if (prop instanceof Property) {
1476
- return this.properties.get(prop);
1444
+ get errors() {
1445
+ const errors = this.field().errors();
1446
+ if (errors.length === 0) {
1447
+ return null;
1477
1448
  }
1478
- if (!this.properties.has(prop)) {
1479
- const logic = this.node.logicNode.logic.getAggregateProperty(prop);
1480
- const result = computed(() => logic.compute(this.node.context), ...(ngDevMode ? [{ debugName: "result" }] : []));
1481
- this.properties.set(prop, result);
1449
+ const errObj = {};
1450
+ for (const error of errors) {
1451
+ errObj[error.kind] = error;
1482
1452
  }
1483
- return this.properties.get(prop);
1453
+ return errObj;
1484
1454
  }
1485
- /**
1486
- * Checks whether the current property state has the given property.
1487
- * @param prop
1488
- * @returns
1489
- */
1490
- has(prop) {
1491
- if (prop instanceof AggregateProperty) {
1492
- // For aggregate properties, they get added to the map lazily, on first access, so we can't
1493
- // rely on checking presence in the properties map. Instead we check if there is any logic for
1494
- // the given property.
1495
- return this.node.logicNode.logic.hasAggregateProperty(prop);
1455
+ get pristine() {
1456
+ return !this.field().dirty();
1457
+ }
1458
+ get dirty() {
1459
+ return this.field().dirty();
1460
+ }
1461
+ get touched() {
1462
+ return this.field().touched();
1463
+ }
1464
+ get untouched() {
1465
+ return !this.field().touched();
1466
+ }
1467
+ get status() {
1468
+ if (this.field().disabled()) {
1469
+ return 'DISABLED';
1496
1470
  }
1497
- else {
1498
- // Non-aggregate proeprties get added to our properties map on construction, so we can just
1499
- // refer to their presence in the map.
1500
- return this.properties.has(prop);
1471
+ if (this.field().valid()) {
1472
+ return 'VALID';
1473
+ }
1474
+ if (this.field().invalid()) {
1475
+ return 'INVALID';
1476
+ }
1477
+ if (this.field().pending()) {
1478
+ return 'PENDING';
1479
+ }
1480
+ throw Error('AssertionError: unknown form control status');
1481
+ }
1482
+ valueAccessor = null;
1483
+ hasValidator(validator) {
1484
+ // This addresses a common case where users look for the presence of `Validators.required` to
1485
+ // determine whether or not to show a required "*" indicator in the UI.
1486
+ if (validator === Validators.required) {
1487
+ return this.field().property(REQUIRED)();
1501
1488
  }
1489
+ return false;
1490
+ }
1491
+ updateValueAndValidity() {
1492
+ // No-op since value and validity are always up to date in signal forms.
1493
+ // We offer this method so that reactive forms code attempting to call it doesn't error.
1502
1494
  }
1503
1495
  }
1504
1496
 
1505
- /**
1506
- * Proxy handler which implements `Field<T>` on top of `FieldNode`.
1507
- */
1508
- const FIELD_PROXY_HANDLER = {
1509
- get(getTgt, p) {
1510
- const tgt = getTgt();
1511
- // First, check whether the requested property is a defined child node of this node.
1512
- const child = tgt.structure.getChild(p);
1513
- if (child !== undefined) {
1514
- // If so, return the child node's `Field` proxy, allowing the developer to continue navigating
1515
- // the form structure.
1516
- return child.fieldProxy;
1517
- }
1518
- // Otherwise, we need to consider whether the properties they're accessing are related to array
1519
- // iteration. We're specifically interested in `length`, but we only want to pass this through
1520
- // if the value is actually an array.
1521
- //
1522
- // We untrack the value here to avoid spurious reactive notifications. In reality, we've already
1523
- // incurred a dependency on the value via `tgt.getChild()` above.
1524
- const value = untracked(tgt.value);
1525
- if (isArray(value)) {
1526
- // Allow access to the length for field arrays, it should be the same as the length of the data.
1527
- if (p === 'length') {
1528
- return tgt.value().length;
1529
- }
1530
- // Allow access to the iterator. This allows the user to spread the field array into a
1531
- // standard array in order to call methods like `filter`, `map`, etc.
1532
- if (p === Symbol.iterator) {
1533
- return Array.prototype[p];
1534
- }
1535
- // Note: We can consider supporting additional array methods if we want in the future,
1536
- // but they should be thoroughly tested. Just forwarding the method directly from the
1537
- // `Array` prototype results in broken behavior for some methods like `map`.
1538
- }
1539
- // Otherwise, this property doesn't exist.
1497
+ // TODO: These utilities to be replaced with proper integration into framework.
1498
+ function privateGetComponentInstance(injector) {
1499
+ assertIsNodeInjector(injector);
1500
+ if (injector._tNode.directiveStart === 0 || injector._tNode.componentOffset === -1) {
1540
1501
  return undefined;
1541
- },
1542
- };
1543
-
1544
- /**
1545
- * Creates a writable signal for a specific property on a source writeable signal.
1546
- * @param source A writeable signal to derive from
1547
- * @param prop A signal of a property key of the source value
1548
- * @returns A writeable signal for the given property of the source value.
1549
- * @template S The source value type
1550
- * @template K The key type for S
1551
- */
1552
- function deepSignal(source, prop) {
1553
- // Memoize the property.
1554
- const read = computed(() => source()[prop()]);
1555
- read[SIGNAL] = source[SIGNAL];
1556
- read.set = (value) => {
1557
- source.update((current) => valueForWrite(current, value, prop()));
1558
- };
1559
- read.update = (fn) => {
1560
- read.set(fn(untracked(read)));
1561
- };
1562
- read.asReadonly = () => read;
1563
- return read;
1502
+ }
1503
+ return injector._lView[injector._tNode.directiveStart + injector._tNode.componentOffset];
1564
1504
  }
1565
- /**
1566
- * Gets an updated root value to use when setting a value on a deepSignal with the given path.
1567
- * @param sourceValue The current value of the deepSignal's source.
1568
- * @param newPropValue The value being written to the deepSignal's property
1569
- * @param prop The deepSignal's property key
1570
- * @returns An updated value for the deepSignal's source
1571
- */
1572
- function valueForWrite(sourceValue, newPropValue, prop) {
1573
- if (isArray(sourceValue)) {
1574
- const newValue = [...sourceValue];
1575
- newValue[prop] = newPropValue;
1576
- return newValue;
1505
+ function privateSetComponentInput(inputSignal, value) {
1506
+ inputSignal[_SIGNAL].applyValueToInputSignal(inputSignal[_SIGNAL], value);
1507
+ }
1508
+ function privateIsSignalInput(value) {
1509
+ return isInputSignal(value);
1510
+ }
1511
+ function privateIsModelInput(value) {
1512
+ return isInputSignal(value) && isObject(value) && 'subscribe' in value;
1513
+ }
1514
+ function privateRunEffect(ref) {
1515
+ ref[_SIGNAL].run();
1516
+ }
1517
+ function assertIsNodeInjector(injector) {
1518
+ if (!('_tNode' in injector)) {
1519
+ throw new Error('Expected a Node Injector');
1577
1520
  }
1578
- else {
1579
- return { ...sourceValue, [prop]: newPropValue };
1521
+ }
1522
+ function isInputSignal(value) {
1523
+ if (!isObject(value) || !(_SIGNAL in value)) {
1524
+ return false;
1580
1525
  }
1526
+ const node = value[_SIGNAL];
1527
+ return isObject(node) && 'applyValueToInputSignal' in node;
1581
1528
  }
1582
1529
 
1583
- /** Structural component of a `FieldNode` which tracks its path, parent, and children. */
1584
- class FieldNodeStructure {
1585
- logic;
1586
- /** Added to array elements for tracking purposes. */
1587
- // TODO: given that we don't ever let a field move between parents, is it safe to just extract
1588
- // this to a shared symbol for all fields, rather than having a separate one per parent?
1589
- identitySymbol = Symbol();
1590
- /** Lazily initialized injector. Do not access directly, access via `injector` getter instead. */
1591
- _injector = undefined;
1592
- /** Lazily initialized injector. */
1593
- get injector() {
1594
- this._injector ??= Injector.create({
1595
- providers: [],
1596
- parent: this.fieldManager.injector,
1597
- });
1598
- return this._injector;
1530
+ /**
1531
+ * Binds a form `Field` to a UI control that edits it. A UI control can be one of several things:
1532
+ * 1. A native HTML input or textarea
1533
+ * 2. A signal forms custom control that implements `FormValueControl` or `FormCheckboxControl`
1534
+ * 3. A component that provides a ControlValueAccessor. This should only be used to backwards
1535
+ * compatibility with reactive forms. Prefer options (1) and (2).
1536
+ *
1537
+ * This directive has several responsibilities:
1538
+ * 1. Two-way binds the field's value with the UI control's value
1539
+ * 2. Binds additional forms related state on the field to the UI control (disabled, required, etc.)
1540
+ * 3. Relays relevant events on the control to the field (e.g. marks field touched on blur)
1541
+ * 4. Provides a fake `NgControl` that implements a subset of the features available on the reactive
1542
+ * forms `NgControl`. This is provided to improve interoperability with controls designed to work
1543
+ * with reactive forms. It should not be used by controls written for signal forms.
1544
+ *
1545
+ * @experimental 21.0.0
1546
+ */
1547
+ class Control {
1548
+ /** The injector for this component. */
1549
+ injector = inject(Injector);
1550
+ renderer = inject(Renderer2);
1551
+ /** Whether state synchronization with the field has been setup yet. */
1552
+ initialized = false;
1553
+ /** The field that is bound to this control. */
1554
+ field = signal(undefined, ...(ngDevMode ? [{ debugName: "field" }] : []));
1555
+ // If `[control]` is applied to a custom UI control, it wants to synchronize state in the field w/
1556
+ // the inputs of that custom control. This is difficult to do in user-land. We use `effect`, but
1557
+ // effects don't run before the lifecycle hooks of the component. This is usually okay, but has
1558
+ // one significant issue: the UI control's required inputs won't be set in time for those
1559
+ // lifecycle hooks to run.
1560
+ //
1561
+ // Eventually we can build custom functionality for the `Control` directive into the framework,
1562
+ // but for now we work around this limitation with a hack. We use an `@Input` instead of a
1563
+ // signal-based `input()` for the `[control]` to hook the exact moment inputs are being set,
1564
+ // before the important lifecycle hooks of the UI control. We can then initialize all our effects
1565
+ // and force them to run immediately, ensuring all required inputs have values.
1566
+ set _field(value) {
1567
+ this.field.set(value);
1568
+ if (!this.initialized) {
1569
+ this.initialize();
1570
+ }
1599
1571
  }
1600
- constructor(
1601
- /** The logic to apply to this field. */
1602
- logic) {
1603
- this.logic = logic;
1572
+ /** The field state of the bound field. */
1573
+ state = computed(() => this.field()(), ...(ngDevMode ? [{ debugName: "state" }] : []));
1574
+ /** The HTMLElement this directive is attached to. */
1575
+ el = inject(ElementRef);
1576
+ /** The NG_VALUE_ACCESSOR array for the host component. */
1577
+ cvaArray = inject(NG_VALUE_ACCESSOR, { optional: true });
1578
+ /** The Cached value for the lazily created interop NgControl. */
1579
+ _ngControl;
1580
+ /** A fake NgControl provided for better interop with reactive forms. */
1581
+ get ngControl() {
1582
+ return (this._ngControl ??= new InteropNgControl(() => this.state()));
1604
1583
  }
1605
- /** Gets the child fields of this field. */
1606
- children() {
1607
- return this.childrenMap()?.values() ?? [];
1584
+ /** The ControlValueAccessor for the host component. */
1585
+ get cva() {
1586
+ return this.cvaArray?.[0] ?? this._ngControl?.valueAccessor ?? undefined;
1608
1587
  }
1609
- /** Retrieve a child `FieldNode` of this node by property key. */
1610
- getChild(key) {
1611
- const map = this.childrenMap();
1612
- const value = this.value();
1613
- if (!map || !isObject(value)) {
1614
- return undefined;
1588
+ /** Initializes state synchronization between the field and the host UI control. */
1589
+ initialize() {
1590
+ this.initialized = true;
1591
+ const injector = this.injector;
1592
+ const cmp = privateGetComponentInstance(injector);
1593
+ // If component has a `control` input, we assume that it will handle binding the field to the
1594
+ // appropriate native/custom control in its template, so we do not attempt to bind any inputs on
1595
+ // this component.
1596
+ if (cmp && isShadowedControlComponent(cmp)) {
1597
+ return;
1615
1598
  }
1616
- if (isArray(value)) {
1617
- const childValue = value[key];
1618
- if (isObject(childValue) && childValue.hasOwnProperty(this.identitySymbol)) {
1619
- // For arrays, we want to use the tracking identity of the value instead of the raw property
1620
- // as our index into the `childrenMap`.
1621
- key = childValue[this.identitySymbol];
1622
- }
1599
+ if (cmp && isFormUiControl(cmp)) {
1600
+ // If we're binding to a component that follows the standard form ui control contract,
1601
+ // set up state synchronization based on the contract.
1602
+ this.setupCustomUiControl(cmp);
1623
1603
  }
1624
- return map.get((typeof key === 'number' ? key.toString() : key));
1625
- }
1626
- /** Destroys the field when it is no longer needed. */
1627
- destroy() {
1628
- this.injector.destroy();
1629
- }
1630
- }
1631
- /** The structural component of a `FieldNode` that is the root of its field tree. */
1632
- class RootFieldNodeStructure extends FieldNodeStructure {
1633
- node;
1634
- fieldManager;
1635
- value;
1636
- get parent() {
1637
- return undefined;
1638
- }
1639
- get root() {
1640
- return this.node;
1641
- }
1642
- get pathKeys() {
1643
- return ROOT_PATH_KEYS;
1644
- }
1645
- get keyInParent() {
1646
- return ROOT_KEY_IN_PARENT;
1647
- }
1648
- childrenMap;
1649
- /**
1650
- * Creates the structure for the root node of a field tree.
1651
- *
1652
- * @param node The full field node that this structure belongs to
1653
- * @param pathNode The path corresponding to this node in the schema
1654
- * @param logic The logic to apply to this field
1655
- * @param fieldManager The field manager for this field
1656
- * @param value The value signal for this field
1657
- * @param adapter Adapter that knows how to create new fields and appropriate state.
1658
- * @param createChildNode A factory function to create child nodes for this field.
1659
- */
1660
- constructor(
1661
- /** The full field node that corresponds to this structure. */
1662
- node, pathNode, logic, fieldManager, value, adapter, createChildNode) {
1663
- super(logic);
1664
- this.node = node;
1665
- this.fieldManager = fieldManager;
1666
- this.value = value;
1667
- this.childrenMap = makeChildrenMapSignal(node, value, this.identitySymbol, pathNode, logic, adapter, createChildNode);
1668
- }
1669
- }
1670
- /** The structural component of a child `FieldNode` within a field tree. */
1671
- class ChildFieldNodeStructure extends FieldNodeStructure {
1672
- parent;
1673
- root;
1674
- pathKeys;
1675
- keyInParent;
1676
- value;
1677
- childrenMap;
1678
- get fieldManager() {
1679
- return this.root.structure.fieldManager;
1604
+ else if (this.cva !== undefined) {
1605
+ // If we're binding to a component that doesn't follow the standard contract, but provides a
1606
+ // control value accessor, set up state synchronization based on th CVA.
1607
+ this.setupControlValueAccessor(this.cva);
1608
+ }
1609
+ else if (this.el.nativeElement instanceof HTMLInputElement ||
1610
+ this.el.nativeElement instanceof HTMLTextAreaElement ||
1611
+ this.el.nativeElement instanceof HTMLSelectElement) {
1612
+ // If we're binding to a native html input, set up state synchronization with its native
1613
+ // properties / attributes.
1614
+ this.setupNativeInput(this.el.nativeElement);
1615
+ }
1616
+ else {
1617
+ throw new Error(`Unhandled control?`);
1618
+ }
1619
+ // Register this control on the field it is currently bound to. We do this at the end of
1620
+ // initialization so that it only runs if we are actually syncing with this control
1621
+ // (as opposed to just passing the field through to its `control` input).
1622
+ effect((onCleanup) => {
1623
+ const fieldNode = this.state();
1624
+ fieldNode.nodeState.controls.update((controls) => [...controls, this]);
1625
+ onCleanup(() => {
1626
+ fieldNode.nodeState.controls.update((controls) => controls.filter((c) => c !== this));
1627
+ });
1628
+ }, { injector: this.injector });
1680
1629
  }
1681
1630
  /**
1682
- * Creates the structure for a child field node in a field tree.
1683
- *
1684
- * @param node The full field node that this structure belongs to
1685
- * @param pathNode The path corresponding to this node in the schema
1686
- * @param logic The logic to apply to this field
1687
- * @param parent The parent field node for this node
1688
- * @param identityInParent The identity used to track this field in its parent
1689
- * @param initialKeyInParent The key of this field in its parent at the time of creation
1690
- * @param adapter Adapter that knows how to create new fields and appropriate state.
1691
- * @param createChildNode A factory function to create child nodes for this field.
1631
+ * Set up state synchronization between the field and a native <input>, <textarea>, or <select>.
1632
+ */
1633
+ setupNativeInput(input) {
1634
+ const inputType = input instanceof HTMLTextAreaElement
1635
+ ? 'text'
1636
+ : input instanceof HTMLSelectElement
1637
+ ? 'select'
1638
+ : input.type;
1639
+ input.addEventListener('input', () => {
1640
+ switch (inputType) {
1641
+ case 'checkbox':
1642
+ this.state().value.set(input.checked);
1643
+ break;
1644
+ case 'radio':
1645
+ // The `input` event only fires when a radio button becomes selected, so write its `value`
1646
+ // into the state.
1647
+ this.state().value.set(input.value);
1648
+ break;
1649
+ case 'number':
1650
+ case 'range':
1651
+ case 'datetime-local':
1652
+ // We can read a `number` or a `string` from this input type.
1653
+ // Prefer whichever is consistent with the current type.
1654
+ if (typeof this.state().value() === 'number') {
1655
+ this.state().value.set(input.valueAsNumber);
1656
+ }
1657
+ else {
1658
+ this.state().value.set(input.value);
1659
+ }
1660
+ break;
1661
+ case 'date':
1662
+ case 'month':
1663
+ case 'week':
1664
+ case 'time':
1665
+ // We can read a `Date | null` or a `number` or a `string` from this input type.
1666
+ // Prefer whichever is consistent with the current type.
1667
+ if (isDateOrNull(this.state().value())) {
1668
+ this.state().value.set(input.valueAsDate);
1669
+ }
1670
+ else if (typeof this.state().value() === 'number') {
1671
+ this.state().value.set(input.valueAsNumber);
1672
+ }
1673
+ else {
1674
+ this.state().value.set(input.value);
1675
+ }
1676
+ break;
1677
+ default:
1678
+ this.state().value.set(input.value);
1679
+ break;
1680
+ }
1681
+ this.state().markAsDirty();
1682
+ });
1683
+ input.addEventListener('blur', () => this.state().markAsTouched());
1684
+ this.maybeSynchronize(() => this.state().readonly(), this.withBooleanAttribute(input, 'readonly'));
1685
+ // TODO: consider making a global configuration option for using aria-disabled instead.
1686
+ this.maybeSynchronize(() => this.state().disabled(), this.withBooleanAttribute(input, 'disabled'));
1687
+ this.maybeSynchronize(() => this.state().name(), this.withAttribute(input, 'name'));
1688
+ this.maybeSynchronize(this.propertySource(REQUIRED), this.withBooleanAttribute(input, 'required'));
1689
+ this.maybeSynchronize(this.propertySource(MIN), this.withAttribute(input, 'min'));
1690
+ this.maybeSynchronize(this.propertySource(MIN_LENGTH), this.withAttribute(input, 'minLength'));
1691
+ this.maybeSynchronize(this.propertySource(MAX), this.withAttribute(input, 'max'));
1692
+ this.maybeSynchronize(this.propertySource(MAX_LENGTH), this.withAttribute(input, 'maxLength'));
1693
+ switch (inputType) {
1694
+ case 'checkbox':
1695
+ this.maybeSynchronize(() => this.state().value(), (value) => (input.checked = value));
1696
+ break;
1697
+ case 'radio':
1698
+ this.maybeSynchronize(() => this.state().value(), (value) => {
1699
+ // Although HTML behavior is to clear the input already, we do this just in case.
1700
+ // It seems like it might be necessary in certain environments (e.g. Domino).
1701
+ input.checked = input.value === value;
1702
+ });
1703
+ break;
1704
+ case 'select':
1705
+ this.maybeSynchronize(() => this.state().value(), (value) => {
1706
+ // A select will not take a value unil the value's option has rendered.
1707
+ afterNextRender(() => (input.value = value), { injector: this.injector });
1708
+ });
1709
+ break;
1710
+ case 'number':
1711
+ case 'range':
1712
+ case 'datetime-local':
1713
+ // This input type can receive a `number` or a `string`.
1714
+ this.maybeSynchronize(() => this.state().value(), (value) => {
1715
+ if (typeof value === 'number') {
1716
+ input.valueAsNumber = value;
1717
+ }
1718
+ else {
1719
+ input.value = value;
1720
+ }
1721
+ });
1722
+ break;
1723
+ case 'date':
1724
+ case 'month':
1725
+ case 'week':
1726
+ case 'time':
1727
+ // This input type can receive a `Date | null` or a `number` or a `string`.
1728
+ this.maybeSynchronize(() => this.state().value(), (value) => {
1729
+ if (isDateOrNull(value)) {
1730
+ input.valueAsDate = value;
1731
+ }
1732
+ else if (typeof value === 'number') {
1733
+ input.valueAsNumber = value;
1734
+ }
1735
+ else {
1736
+ input.value = value;
1737
+ }
1738
+ });
1739
+ break;
1740
+ default:
1741
+ this.maybeSynchronize(() => this.state().value(), (value) => {
1742
+ input.value = value;
1743
+ });
1744
+ break;
1745
+ }
1746
+ }
1747
+ /** Set up state synchronization between the field and a ControlValueAccessor. */
1748
+ setupControlValueAccessor(cva) {
1749
+ cva.registerOnChange((value) => this.state().value.set(value));
1750
+ cva.registerOnTouched(() => this.state().markAsTouched());
1751
+ this.maybeSynchronize(() => this.state().value(), (value) => cva.writeValue(value));
1752
+ if (cva.setDisabledState) {
1753
+ this.maybeSynchronize(() => this.state().disabled(), (value) => cva.setDisabledState(value));
1754
+ }
1755
+ cva.writeValue(this.state().value());
1756
+ cva.setDisabledState?.(this.state().disabled());
1757
+ }
1758
+ /** Set up state synchronization between the field and a FormUiControl. */
1759
+ setupCustomUiControl(cmp) {
1760
+ // Handle the property side of the model binding. How we do this depends on the shape of the
1761
+ // component. There are 2 options:
1762
+ // * it provides a `value` model (most controls that edit a single value)
1763
+ // * it provides a `checked` model with no `value` signal (custom checkbox)
1764
+ let cleanupValue;
1765
+ if (isFormValueControl(cmp)) {
1766
+ // <custom-input [(value)]="state().value">
1767
+ this.maybeSynchronize(() => this.state().value(), withInput(cmp.value));
1768
+ cleanupValue = cmp.value.subscribe((newValue) => this.state().value.set(newValue));
1769
+ }
1770
+ else if (isFormCheckboxControl(cmp)) {
1771
+ // <custom-checkbox [(checked)]="state().value" />
1772
+ this.maybeSynchronize(() => this.state().value(), withInput(cmp.checked));
1773
+ cleanupValue = cmp.checked.subscribe((newValue) => this.state().value.set(newValue));
1774
+ }
1775
+ else {
1776
+ throw new Error(`Unknown custom control subtype`);
1777
+ }
1778
+ this.maybeSynchronize(() => this.state().name(), withInput(cmp.name));
1779
+ this.maybeSynchronize(() => this.state().disabled(), withInput(cmp.disabled));
1780
+ this.maybeSynchronize(() => this.state().disabledReasons(), withInput(cmp.disabledReasons));
1781
+ this.maybeSynchronize(() => this.state().readonly(), withInput(cmp.readonly));
1782
+ this.maybeSynchronize(() => this.state().hidden(), withInput(cmp.hidden));
1783
+ this.maybeSynchronize(() => this.state().errors(), withInput(cmp.errors));
1784
+ if (privateIsModelInput(cmp.touched) || privateIsSignalInput(cmp.touched)) {
1785
+ this.maybeSynchronize(() => this.state().touched(), withInput(cmp.touched));
1786
+ }
1787
+ this.maybeSynchronize(() => this.state().dirty(), withInput(cmp.dirty));
1788
+ this.maybeSynchronize(() => this.state().invalid(), withInput(cmp.invalid));
1789
+ this.maybeSynchronize(() => this.state().pending(), withInput(cmp.pending));
1790
+ this.maybeSynchronize(this.propertySource(REQUIRED), withInput(cmp.required));
1791
+ this.maybeSynchronize(this.propertySource(MIN), withInput(cmp.min));
1792
+ this.maybeSynchronize(this.propertySource(MIN_LENGTH), withInput(cmp.minLength));
1793
+ this.maybeSynchronize(this.propertySource(MAX), withInput(cmp.max));
1794
+ this.maybeSynchronize(this.propertySource(MAX_LENGTH), withInput(cmp.maxLength));
1795
+ this.maybeSynchronize(this.propertySource(PATTERN), withInput(cmp.pattern));
1796
+ let cleanupTouch;
1797
+ let cleanupDefaultTouch;
1798
+ if (privateIsModelInput(cmp.touched) || isOutputRef(cmp.touched)) {
1799
+ cleanupTouch = cmp.touched.subscribe(() => this.state().markAsTouched());
1800
+ }
1801
+ else {
1802
+ // If the component did not give us a touch event stream, use the standard touch logic,
1803
+ // marking it touched when the focus moves from inside the host element to outside.
1804
+ const listener = (event) => {
1805
+ const newActiveEl = event.relatedTarget;
1806
+ if (!this.el.nativeElement.contains(newActiveEl)) {
1807
+ this.state().markAsTouched();
1808
+ }
1809
+ };
1810
+ this.el.nativeElement.addEventListener('focusout', listener);
1811
+ cleanupDefaultTouch = () => this.el.nativeElement.removeEventListener('focusout', listener);
1812
+ }
1813
+ // Cleanup for output binding subscriptions:
1814
+ this.injector.get(DestroyRef).onDestroy(() => {
1815
+ cleanupValue?.unsubscribe();
1816
+ cleanupTouch?.unsubscribe();
1817
+ cleanupDefaultTouch?.();
1818
+ });
1819
+ }
1820
+ /** Synchronize a value from a reactive source to a given sink. */
1821
+ maybeSynchronize(source, sink) {
1822
+ if (!sink) {
1823
+ return undefined;
1824
+ }
1825
+ const ref = effect(() => {
1826
+ const value = source();
1827
+ untracked(() => sink(value));
1828
+ }, ...(ngDevMode ? [{ debugName: "ref", injector: this.injector }] : [{ injector: this.injector }]));
1829
+ // Run the effect immediately to ensure sinks which are required inputs are set before they can
1830
+ // be observed. See the note on `_field` for more details.
1831
+ privateRunEffect(ref);
1832
+ }
1833
+ /** Creates a reactive value source by reading the given AggregateProperty from the field. */
1834
+ propertySource(key) {
1835
+ const metaSource = computed(() => this.state().hasProperty(key) ? this.state().property(key) : key.getInitial, ...(ngDevMode ? [{ debugName: "metaSource" }] : []));
1836
+ return () => metaSource()?.();
1837
+ }
1838
+ /** Creates a (non-boolean) value sync that writes the given attribute of the given element. */
1839
+ withAttribute(element, attribute) {
1840
+ return (value) => {
1841
+ if (value !== undefined) {
1842
+ this.renderer.setAttribute(element, attribute, value.toString());
1843
+ }
1844
+ else {
1845
+ this.renderer.removeAttribute(element, attribute);
1846
+ }
1847
+ };
1848
+ }
1849
+ /** Creates a boolean value sync that writes the given attribute of the given element. */
1850
+ withBooleanAttribute(element, attribute) {
1851
+ return (value) => {
1852
+ if (value) {
1853
+ this.renderer.setAttribute(element, attribute, '');
1854
+ }
1855
+ else {
1856
+ this.renderer.removeAttribute(element, attribute);
1857
+ }
1858
+ };
1859
+ }
1860
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0-next.4", ngImport: i0, type: Control, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1861
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.0-next.4", type: Control, isStandalone: true, selector: "[control]", inputs: { _field: ["control", "_field"] }, providers: [
1862
+ {
1863
+ provide: NgControl,
1864
+ useFactory: () => inject(Control).ngControl,
1865
+ },
1866
+ ], ngImport: i0 });
1867
+ }
1868
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.0-next.4", ngImport: i0, type: Control, decorators: [{
1869
+ type: Directive,
1870
+ args: [{
1871
+ selector: '[control]',
1872
+ providers: [
1873
+ {
1874
+ provide: NgControl,
1875
+ useFactory: () => inject(Control).ngControl,
1876
+ },
1877
+ ],
1878
+ }]
1879
+ }], propDecorators: { _field: [{
1880
+ type: Input,
1881
+ args: [{ required: true, alias: 'control' }]
1882
+ }] } });
1883
+ /** Creates a value sync from an input signal. */
1884
+ function withInput(input) {
1885
+ return input ? (value) => privateSetComponentInput(input, value) : undefined;
1886
+ }
1887
+ /**
1888
+ * Checks whether the given component matches the contract for either FormValueControl or
1889
+ * FormCheckboxControl.
1890
+ */
1891
+ function isFormUiControl(cmp) {
1892
+ const castCmp = cmp;
1893
+ return ((isFormValueControl(castCmp) || isFormCheckboxControl(castCmp)) &&
1894
+ (castCmp.readonly === undefined || privateIsSignalInput(castCmp.readonly)) &&
1895
+ (castCmp.disabled === undefined || privateIsSignalInput(castCmp.disabled)) &&
1896
+ (castCmp.disabledReasons === undefined || privateIsSignalInput(castCmp.disabledReasons)) &&
1897
+ (castCmp.errors === undefined || privateIsSignalInput(castCmp.errors)) &&
1898
+ (castCmp.invalid === undefined || privateIsSignalInput(castCmp.invalid)) &&
1899
+ (castCmp.pending === undefined || privateIsSignalInput(castCmp.pending)) &&
1900
+ (castCmp.touched === undefined ||
1901
+ privateIsModelInput(castCmp.touched) ||
1902
+ privateIsSignalInput(castCmp.touched) ||
1903
+ isOutputRef(castCmp.touched)) &&
1904
+ (castCmp.dirty === undefined || privateIsSignalInput(castCmp.dirty)) &&
1905
+ (castCmp.min === undefined || privateIsSignalInput(castCmp.min)) &&
1906
+ (castCmp.minLength === undefined || privateIsSignalInput(castCmp.minLength)) &&
1907
+ (castCmp.max === undefined || privateIsSignalInput(castCmp.max)) &&
1908
+ (castCmp.maxLength === undefined || privateIsSignalInput(castCmp.maxLength)));
1909
+ }
1910
+ /** Checks whether the given FormUiControl is a FormValueControl. */
1911
+ function isFormValueControl(cmp) {
1912
+ return privateIsModelInput(cmp.value);
1913
+ }
1914
+ /** Checks whether the given FormUiControl is a FormCheckboxControl. */
1915
+ function isFormCheckboxControl(cmp) {
1916
+ return (privateIsModelInput(cmp.checked) &&
1917
+ cmp.value === undefined);
1918
+ }
1919
+ /** Checks whether the given component has an input called `control`. */
1920
+ function isShadowedControlComponent(cmp) {
1921
+ const mirror = reflectComponentType(cmp.constructor);
1922
+ return mirror?.inputs.some((input) => input.templateName === 'control') ?? false;
1923
+ }
1924
+ /** Checks whether the given object is an output ref. */
1925
+ function isOutputRef(value) {
1926
+ return value instanceof OutputEmitterRef || value instanceof EventEmitter;
1927
+ }
1928
+ /** Checks if a given value is a Date or null */
1929
+ function isDateOrNull(value) {
1930
+ return value === null || value instanceof Date;
1931
+ }
1932
+
1933
+ /**
1934
+ * `FieldContext` implementation, backed by a `FieldNode`.
1935
+ */
1936
+ class FieldNodeContext {
1937
+ node;
1938
+ /**
1939
+ * Cache of paths that have been resolved for this context.
1940
+ *
1941
+ * For each resolved path we keep track of a signal of field that it maps to rather than a static
1942
+ * field, since it theoretically could change. In practice for the current system it should not
1943
+ * actually change, as they only place we currently track fields moving within the parent
1944
+ * structure is for arrays, and paths do not currently support array indexing.
1945
+ */
1946
+ cache = new WeakMap();
1947
+ constructor(
1948
+ /** The field node this context corresponds to. */
1949
+ node) {
1950
+ this.node = node;
1951
+ }
1952
+ /**
1953
+ * Resolves a target path relative to this context.
1954
+ * @param target The path to resolve
1955
+ * @returns The field corresponding to the target path.
1956
+ */
1957
+ resolve(target) {
1958
+ if (!this.cache.has(target)) {
1959
+ const resolver = computed(() => {
1960
+ const targetPathNode = FieldPathNode.unwrapFieldPath(target);
1961
+ // First, find the field where the root our target path was merged in.
1962
+ // We determine this by walking up the field tree from the current field and looking for
1963
+ // the place where the LogicNodeBuilder from the target path's root was merged in.
1964
+ // We always make sure to walk up at least as far as the depth of the path we were bound to.
1965
+ // This ensures that we do not accidentally match on the wrong application of a recursively
1966
+ // applied schema.
1967
+ let field = this.node;
1968
+ let stepsRemaining = getBoundPathDepth();
1969
+ while (stepsRemaining > 0 || !field.structure.logic.hasLogic(targetPathNode.root.logic)) {
1970
+ stepsRemaining--;
1971
+ field = field.structure.parent;
1972
+ if (field === undefined) {
1973
+ throw new Error('Path is not part of this field tree.');
1974
+ }
1975
+ }
1976
+ // Now, we can navigate to the target field using the relative path in the target path node
1977
+ // to traverse down from the field we just found.
1978
+ for (let key of targetPathNode.keys) {
1979
+ field = field.structure.getChild(key);
1980
+ if (field === undefined) {
1981
+ throw new Error(`Cannot resolve path .${targetPathNode.keys.join('.')} relative to field ${[
1982
+ '<root>',
1983
+ ...this.node.structure.pathKeys(),
1984
+ ].join('.')}.`);
1985
+ }
1986
+ }
1987
+ return field.fieldProxy;
1988
+ }, ...(ngDevMode ? [{ debugName: "resolver" }] : []));
1989
+ this.cache.set(target, resolver);
1990
+ }
1991
+ return this.cache.get(target)();
1992
+ }
1993
+ get field() {
1994
+ return this.node.fieldProxy;
1995
+ }
1996
+ get state() {
1997
+ return this.node;
1998
+ }
1999
+ get value() {
2000
+ return this.node.structure.value;
2001
+ }
2002
+ get key() {
2003
+ return this.node.structure.keyInParent;
2004
+ }
2005
+ index = computed(() => {
2006
+ // Attempt to read the key first, this will throw an error if we're on a root field.
2007
+ const key = this.key();
2008
+ // Assert that the parent is actually an array.
2009
+ if (!isArray(untracked(this.node.structure.parent.value))) {
2010
+ throw new Error(`RuntimeError: cannot access index, parent field is not an array`);
2011
+ }
2012
+ // Return the key as a number if we are indeed inside an array field.
2013
+ return Number(key);
2014
+ }, ...(ngDevMode ? [{ debugName: "index" }] : []));
2015
+ fieldOf = (p) => this.resolve(p);
2016
+ stateOf = (p) => this.resolve(p)();
2017
+ valueOf = (p) => this.resolve(p)().value();
2018
+ }
2019
+
2020
+ /**
2021
+ * Tracks custom properties associated with a `FieldNode`.
2022
+ */
2023
+ class FieldPropertyState {
2024
+ node;
2025
+ /** A map of all `Property` and `AggregateProperty` that have been defined for this field. */
2026
+ properties = new Map();
2027
+ constructor(node) {
2028
+ this.node = node;
2029
+ // Field nodes (and thus their property state) are created in a linkedSignal in order to mirror
2030
+ // the structure of the model data. We need to run the property factories untracked so that they
2031
+ // do not cause recomputation of the linkedSignal.
2032
+ untracked(() =>
2033
+ // Property factories are run in the form's injection context so they can create resources
2034
+ // and inject DI dependencies.
2035
+ runInInjectionContext(this.node.structure.injector, () => {
2036
+ for (const [key, factory] of this.node.logicNode.logic.getPropertyFactoryEntries()) {
2037
+ this.properties.set(key, factory(this.node.context));
2038
+ }
2039
+ }));
2040
+ }
2041
+ /** Gets the value of a `Property` or `AggregateProperty` for the field. */
2042
+ get(prop) {
2043
+ if (prop instanceof Property) {
2044
+ return this.properties.get(prop);
2045
+ }
2046
+ if (!this.properties.has(prop)) {
2047
+ const logic = this.node.logicNode.logic.getAggregateProperty(prop);
2048
+ const result = computed(() => logic.compute(this.node.context), ...(ngDevMode ? [{ debugName: "result" }] : []));
2049
+ this.properties.set(prop, result);
2050
+ }
2051
+ return this.properties.get(prop);
2052
+ }
2053
+ /**
2054
+ * Checks whether the current property state has the given property.
2055
+ * @param prop
2056
+ * @returns
2057
+ */
2058
+ has(prop) {
2059
+ if (prop instanceof AggregateProperty) {
2060
+ // For aggregate properties, they get added to the map lazily, on first access, so we can't
2061
+ // rely on checking presence in the properties map. Instead we check if there is any logic for
2062
+ // the given property.
2063
+ return this.node.logicNode.logic.hasAggregateProperty(prop);
2064
+ }
2065
+ else {
2066
+ // Non-aggregate proeprties get added to our properties map on construction, so we can just
2067
+ // refer to their presence in the map.
2068
+ return this.properties.has(prop);
2069
+ }
2070
+ }
2071
+ }
2072
+
2073
+ /**
2074
+ * Proxy handler which implements `Field<T>` on top of `FieldNode`.
2075
+ */
2076
+ const FIELD_PROXY_HANDLER = {
2077
+ get(getTgt, p) {
2078
+ const tgt = getTgt();
2079
+ // First, check whether the requested property is a defined child node of this node.
2080
+ const child = tgt.structure.getChild(p);
2081
+ if (child !== undefined) {
2082
+ // If so, return the child node's `Field` proxy, allowing the developer to continue navigating
2083
+ // the form structure.
2084
+ return child.fieldProxy;
2085
+ }
2086
+ // Otherwise, we need to consider whether the properties they're accessing are related to array
2087
+ // iteration. We're specifically interested in `length`, but we only want to pass this through
2088
+ // if the value is actually an array.
2089
+ //
2090
+ // We untrack the value here to avoid spurious reactive notifications. In reality, we've already
2091
+ // incurred a dependency on the value via `tgt.getChild()` above.
2092
+ const value = untracked(tgt.value);
2093
+ if (isArray(value)) {
2094
+ // Allow access to the length for field arrays, it should be the same as the length of the data.
2095
+ if (p === 'length') {
2096
+ return tgt.value().length;
2097
+ }
2098
+ // Allow access to the iterator. This allows the user to spread the field array into a
2099
+ // standard array in order to call methods like `filter`, `map`, etc.
2100
+ if (p === Symbol.iterator) {
2101
+ return Array.prototype[p];
2102
+ }
2103
+ // Note: We can consider supporting additional array methods if we want in the future,
2104
+ // but they should be thoroughly tested. Just forwarding the method directly from the
2105
+ // `Array` prototype results in broken behavior for some methods like `map`.
2106
+ }
2107
+ // Otherwise, this property doesn't exist.
2108
+ return undefined;
2109
+ },
2110
+ };
2111
+
2112
+ /**
2113
+ * Creates a writable signal for a specific property on a source writeable signal.
2114
+ * @param source A writeable signal to derive from
2115
+ * @param prop A signal of a property key of the source value
2116
+ * @returns A writeable signal for the given property of the source value.
2117
+ * @template S The source value type
2118
+ * @template K The key type for S
2119
+ */
2120
+ function deepSignal(source, prop) {
2121
+ // Memoize the property.
2122
+ const read = computed(() => source()[prop()]);
2123
+ read[SIGNAL] = source[SIGNAL];
2124
+ read.set = (value) => {
2125
+ source.update((current) => valueForWrite(current, value, prop()));
2126
+ };
2127
+ read.update = (fn) => {
2128
+ read.set(fn(untracked(read)));
2129
+ };
2130
+ read.asReadonly = () => read;
2131
+ return read;
2132
+ }
2133
+ /**
2134
+ * Gets an updated root value to use when setting a value on a deepSignal with the given path.
2135
+ * @param sourceValue The current value of the deepSignal's source.
2136
+ * @param newPropValue The value being written to the deepSignal's property
2137
+ * @param prop The deepSignal's property key
2138
+ * @returns An updated value for the deepSignal's source
2139
+ */
2140
+ function valueForWrite(sourceValue, newPropValue, prop) {
2141
+ if (isArray(sourceValue)) {
2142
+ const newValue = [...sourceValue];
2143
+ newValue[prop] = newPropValue;
2144
+ return newValue;
2145
+ }
2146
+ else {
2147
+ return { ...sourceValue, [prop]: newPropValue };
2148
+ }
2149
+ }
2150
+
2151
+ /** Structural component of a `FieldNode` which tracks its path, parent, and children. */
2152
+ class FieldNodeStructure {
2153
+ logic;
2154
+ /** Added to array elements for tracking purposes. */
2155
+ // TODO: given that we don't ever let a field move between parents, is it safe to just extract
2156
+ // this to a shared symbol for all fields, rather than having a separate one per parent?
2157
+ identitySymbol = Symbol();
2158
+ /** Lazily initialized injector. Do not access directly, access via `injector` getter instead. */
2159
+ _injector = undefined;
2160
+ /** Lazily initialized injector. */
2161
+ get injector() {
2162
+ this._injector ??= Injector.create({
2163
+ providers: [],
2164
+ parent: this.fieldManager.injector,
2165
+ });
2166
+ return this._injector;
2167
+ }
2168
+ constructor(
2169
+ /** The logic to apply to this field. */
2170
+ logic) {
2171
+ this.logic = logic;
2172
+ }
2173
+ /** Gets the child fields of this field. */
2174
+ children() {
2175
+ return this.childrenMap()?.values() ?? [];
2176
+ }
2177
+ /** Retrieve a child `FieldNode` of this node by property key. */
2178
+ getChild(key) {
2179
+ const map = this.childrenMap();
2180
+ const value = this.value();
2181
+ if (!map || !isObject(value)) {
2182
+ return undefined;
2183
+ }
2184
+ if (isArray(value)) {
2185
+ const childValue = value[key];
2186
+ if (isObject(childValue) && childValue.hasOwnProperty(this.identitySymbol)) {
2187
+ // For arrays, we want to use the tracking identity of the value instead of the raw property
2188
+ // as our index into the `childrenMap`.
2189
+ key = childValue[this.identitySymbol];
2190
+ }
2191
+ }
2192
+ return map.get((typeof key === 'number' ? key.toString() : key));
2193
+ }
2194
+ /** Destroys the field when it is no longer needed. */
2195
+ destroy() {
2196
+ this.injector.destroy();
2197
+ }
2198
+ }
2199
+ /** The structural component of a `FieldNode` that is the root of its field tree. */
2200
+ class RootFieldNodeStructure extends FieldNodeStructure {
2201
+ node;
2202
+ fieldManager;
2203
+ value;
2204
+ get parent() {
2205
+ return undefined;
2206
+ }
2207
+ get root() {
2208
+ return this.node;
2209
+ }
2210
+ get pathKeys() {
2211
+ return ROOT_PATH_KEYS;
2212
+ }
2213
+ get keyInParent() {
2214
+ return ROOT_KEY_IN_PARENT;
2215
+ }
2216
+ childrenMap;
2217
+ /**
2218
+ * Creates the structure for the root node of a field tree.
2219
+ *
2220
+ * @param node The full field node that this structure belongs to
2221
+ * @param pathNode The path corresponding to this node in the schema
2222
+ * @param logic The logic to apply to this field
2223
+ * @param fieldManager The field manager for this field
2224
+ * @param value The value signal for this field
2225
+ * @param adapter Adapter that knows how to create new fields and appropriate state.
2226
+ * @param createChildNode A factory function to create child nodes for this field.
2227
+ */
2228
+ constructor(
2229
+ /** The full field node that corresponds to this structure. */
2230
+ node, pathNode, logic, fieldManager, value, adapter, createChildNode) {
2231
+ super(logic);
2232
+ this.node = node;
2233
+ this.fieldManager = fieldManager;
2234
+ this.value = value;
2235
+ this.childrenMap = makeChildrenMapSignal(node, value, this.identitySymbol, pathNode, logic, adapter, createChildNode);
2236
+ }
2237
+ }
2238
+ /** The structural component of a child `FieldNode` within a field tree. */
2239
+ class ChildFieldNodeStructure extends FieldNodeStructure {
2240
+ parent;
2241
+ root;
2242
+ pathKeys;
2243
+ keyInParent;
2244
+ value;
2245
+ childrenMap;
2246
+ get fieldManager() {
2247
+ return this.root.structure.fieldManager;
2248
+ }
2249
+ /**
2250
+ * Creates the structure for a child field node in a field tree.
2251
+ *
2252
+ * @param node The full field node that this structure belongs to
2253
+ * @param pathNode The path corresponding to this node in the schema
2254
+ * @param logic The logic to apply to this field
2255
+ * @param parent The parent field node for this node
2256
+ * @param identityInParent The identity used to track this field in its parent
2257
+ * @param initialKeyInParent The key of this field in its parent at the time of creation
2258
+ * @param adapter Adapter that knows how to create new fields and appropriate state.
2259
+ * @param createChildNode A factory function to create child nodes for this field.
1692
2260
  */
1693
2261
  constructor(node, pathNode, logic, parent, identityInParent, initialKeyInParent, adapter, createChildNode) {
1694
2262
  super(logic);
@@ -2051,14 +2619,12 @@ class FieldNodeState {
2051
2619
  * Marks this specific field as touched.
2052
2620
  */
2053
2621
  markAsTouched() {
2054
- // TODO: should this be noop for fields that are hidden/disabled/readonly
2055
2622
  this.selfTouched.set(true);
2056
2623
  }
2057
2624
  /**
2058
2625
  * Marks this specific field as dirty.
2059
2626
  */
2060
2627
  markAsDirty() {
2061
- // TODO: should this be noop for fields that are hidden/disabled/readonly
2062
2628
  this.selfDirty.set(true);
2063
2629
  }
2064
2630
  /**
@@ -2082,20 +2648,24 @@ class FieldNodeState {
2082
2648
  * Whether this field is considered dirty.
2083
2649
  *
2084
2650
  * A field is considered dirty if one of the following is true:
2085
- * - It was directly dirtied
2651
+ * - It was directly dirtied and is interactive
2086
2652
  * - One of its children is considered dirty
2087
2653
  */
2088
2654
  dirty = computed(() => {
2089
- return reduceChildren(this.node, this.selfDirty(), (child, value) => value || child.nodeState.dirty(), shortCircuitTrue);
2655
+ const selfDirtyValue = this.selfDirty() && !this.isNonInteractive();
2656
+ return reduceChildren(this.node, selfDirtyValue, (child, value) => value || child.nodeState.dirty(), shortCircuitTrue);
2090
2657
  }, ...(ngDevMode ? [{ debugName: "dirty" }] : []));
2091
2658
  /**
2092
2659
  * Whether this field is considered touched.
2093
2660
  *
2094
2661
  * A field is considered touched if one of the following is true:
2095
- * - It was directly touched
2662
+ * - It was directly touched and is interactive
2096
2663
  * - One of its children is considered touched
2097
2664
  */
2098
- touched = computed(() => reduceChildren(this.node, this.selfTouched(), (child, value) => value || child.nodeState.touched(), shortCircuitTrue), ...(ngDevMode ? [{ debugName: "touched" }] : []));
2665
+ touched = computed(() => {
2666
+ const selfTouchedValue = this.selfTouched() && !this.isNonInteractive();
2667
+ return reduceChildren(this.node, selfTouchedValue, (child, value) => value || child.nodeState.touched(), shortCircuitTrue);
2668
+ }, ...(ngDevMode ? [{ debugName: "touched" }] : []));
2099
2669
  /**
2100
2670
  * The reasons for this field's disablement. This includes disabled reasons for any parent field
2101
2671
  * that may have been disabled, indirectly causing this field to be disabled as well.
@@ -2141,6 +2711,14 @@ class FieldNodeState {
2141
2711
  }
2142
2712
  return `${parent.name()}.${this.node.structure.keyInParent()}`;
2143
2713
  }, ...(ngDevMode ? [{ debugName: "name" }] : []));
2714
+ /** Whether this field is considered non-interactive.
2715
+ *
2716
+ * A field is considered non-interactive if one of the following is true:
2717
+ * - It is hidden
2718
+ * - It is disabled
2719
+ * - It is readonly
2720
+ */
2721
+ isNonInteractive = computed(() => this.hidden() || this.disabled() || this.readonly(), ...(ngDevMode ? [{ debugName: "isNonInteractive" }] : []));
2144
2722
  }
2145
2723
 
2146
2724
  /**
@@ -2321,6 +2899,8 @@ function form(...args) {
2321
2899
  * @param schema A schema for an element of the array, or function that binds logic to an
2322
2900
  * element of the array.
2323
2901
  * @template TValue The data type of the item field to apply the schema to.
2902
+ *
2903
+ * @experimental 21.0.0
2324
2904
  */
2325
2905
  function applyEach(path, schema) {
2326
2906
  assertPathIsCurrent(path);
@@ -2344,6 +2924,8 @@ function applyEach(path, schema) {
2344
2924
  * @param path The target path to apply the schema to.
2345
2925
  * @param schema The schema to apply to the property
2346
2926
  * @template TValue The data type of the field to apply the schema to.
2927
+ *
2928
+ * @experimental 21.0.0
2347
2929
  */
2348
2930
  function apply(path, schema) {
2349
2931
  assertPathIsCurrent(path);
@@ -2357,6 +2939,8 @@ function apply(path, schema) {
2357
2939
  * @param logic A `LogicFn<T, boolean>` that returns `true` when the schema should be applied.
2358
2940
  * @param schema The schema to apply to the field when the `logic` function returns `true`.
2359
2941
  * @template TValue The data type of the field to apply the schema to.
2942
+ *
2943
+ * @experimental 21.0.0
2360
2944
  */
2361
2945
  function applyWhen(path, logic, schema) {
2362
2946
  assertPathIsCurrent(path);
@@ -2396,6 +2980,8 @@ function applyWhenValue(path, predicate, schema) {
2396
2980
  * @param action An asynchronous action used to submit the field. The action may return server
2397
2981
  * errors.
2398
2982
  * @template TValue The data type of the field being submitted.
2983
+ *
2984
+ * @experimental 21.0.0
2399
2985
  */
2400
2986
  async function submit(form, action) {
2401
2987
  const node = form();
@@ -2443,6 +3029,8 @@ function setServerErrors(submittedField, errors) {
2443
3029
  * @param fn A **non-reactive** function that sets up reactive logic rules for the form.
2444
3030
  * @returns A schema object that implements the given logic.
2445
3031
  * @template TValue The value type of a `Field` that this schema binds to.
3032
+ *
3033
+ * @experimental 21.0.0
2446
3034
  */
2447
3035
  function schema(fn) {
2448
3036
  return SchemaImpl.create(fn);
@@ -2484,6 +3072,8 @@ function customError(obj) {
2484
3072
  }
2485
3073
  /**
2486
3074
  * A custom error that may contain additional properties
3075
+ *
3076
+ * @experimental 21.0.0
2487
3077
  */
2488
3078
  class CustomValidationError {
2489
3079
  /** Brand the class to avoid Typescript structural matching */
@@ -2503,6 +3093,8 @@ class CustomValidationError {
2503
3093
  /**
2504
3094
  * Internal version of `NgValidationError`, we create this separately so we can change its type on
2505
3095
  * the exported version to a type union of the possible sub-classes.
3096
+ *
3097
+ * @experimental 21.0.0
2506
3098
  */
2507
3099
  class _NgValidationError {
2508
3100
  /** Brand the class to avoid Typescript structural matching */
@@ -2521,12 +3113,16 @@ class _NgValidationError {
2521
3113
  }
2522
3114
  /**
2523
3115
  * An error used to indicate that a required field is empty.
3116
+ *
3117
+ * @experimental 21.0.0
2524
3118
  */
2525
3119
  class RequiredValidationError extends _NgValidationError {
2526
3120
  kind = 'required';
2527
3121
  }
2528
3122
  /**
2529
3123
  * An error used to indicate that a value is lower than the minimum allowed.
3124
+ *
3125
+ * @experimental 21.0.0
2530
3126
  */
2531
3127
  class MinValidationError extends _NgValidationError {
2532
3128
  min;
@@ -2538,6 +3134,8 @@ class MinValidationError extends _NgValidationError {
2538
3134
  }
2539
3135
  /**
2540
3136
  * An error used to indicate that a value is higher than the maximum allowed.
3137
+ *
3138
+ * @experimental 21.0.0
2541
3139
  */
2542
3140
  class MaxValidationError extends _NgValidationError {
2543
3141
  max;
@@ -2549,6 +3147,8 @@ class MaxValidationError extends _NgValidationError {
2549
3147
  }
2550
3148
  /**
2551
3149
  * An error used to indicate that a value is shorter than the minimum allowed length.
3150
+ *
3151
+ * @experimental 21.0.0
2552
3152
  */
2553
3153
  class MinLengthValidationError extends _NgValidationError {
2554
3154
  minLength;
@@ -2560,6 +3160,8 @@ class MinLengthValidationError extends _NgValidationError {
2560
3160
  }
2561
3161
  /**
2562
3162
  * An error used to indicate that a value is longer than the maximum allowed length.
3163
+ *
3164
+ * @experimental 21.0.0
2563
3165
  */
2564
3166
  class MaxLengthValidationError extends _NgValidationError {
2565
3167
  maxLength;
@@ -2571,6 +3173,8 @@ class MaxLengthValidationError extends _NgValidationError {
2571
3173
  }
2572
3174
  /**
2573
3175
  * An error used to indicate that a value does not match the required pattern.
3176
+ *
3177
+ * @experimental 21.0.0
2574
3178
  */
2575
3179
  class PatternValidationError extends _NgValidationError {
2576
3180
  pattern;
@@ -2582,12 +3186,16 @@ class PatternValidationError extends _NgValidationError {
2582
3186
  }
2583
3187
  /**
2584
3188
  * An error used to indicate that a value is not a valid email.
3189
+ *
3190
+ * @experimental 21.0.0
2585
3191
  */
2586
3192
  class EmailValidationError extends _NgValidationError {
2587
3193
  kind = 'email';
2588
3194
  }
2589
3195
  /**
2590
3196
  * An error used to indicate an issue validating against a standard schema.
3197
+ *
3198
+ * @experimental 21.0.0
2591
3199
  */
2592
3200
  class StandardSchemaValidationError extends _NgValidationError {
2593
3201
  issue;
@@ -2618,6 +3226,8 @@ class StandardSchemaValidationError extends _NgValidationError {
2618
3226
  * }
2619
3227
  * }
2620
3228
  * ```
3229
+ *
3230
+ * @experimental 21.0.0
2621
3231
  */
2622
3232
  const NgValidationError = _NgValidationError;
2623
3233
 
@@ -2632,254 +3242,75 @@ function getLengthOrSize(value) {
2632
3242
  *
2633
3243
  * @param opt The option from BaseValidatorConfig.
2634
3244
  * @param ctx The current FieldContext.
2635
- * @returns The value for the option.
2636
- */
2637
- function getOption(opt, ctx) {
2638
- return opt instanceof Function ? opt(ctx) : opt;
2639
- }
2640
- /**
2641
- * Checks if the given value is considered empty. Empty values are: null, undefined, '', false, NaN.
2642
- */
2643
- function isEmpty(value) {
2644
- if (typeof value === 'number') {
2645
- return isNaN(value);
2646
- }
2647
- return value === '' || value === false || value == null;
2648
- }
2649
-
2650
- /**
2651
- * A regular expression that matches valid e-mail addresses.
2652
- *
2653
- * At a high level, this regexp matches e-mail addresses of the format `local-part@tld`, where:
2654
- * - `local-part` consists of one or more of the allowed characters (alphanumeric and some
2655
- * punctuation symbols).
2656
- * - `local-part` cannot begin or end with a period (`.`).
2657
- * - `local-part` cannot be longer than 64 characters.
2658
- * - `tld` consists of one or more `labels` separated by periods (`.`). For example `localhost` or
2659
- * `foo.com`.
2660
- * - A `label` consists of one or more of the allowed characters (alphanumeric, dashes (`-`) and
2661
- * periods (`.`)).
2662
- * - A `label` cannot begin or end with a dash (`-`) or a period (`.`).
2663
- * - A `label` cannot be longer than 63 characters.
2664
- * - The whole address cannot be longer than 254 characters.
2665
- *
2666
- * ## Implementation background
2667
- *
2668
- * This regexp was ported over from AngularJS (see there for git history):
2669
- * https://github.com/angular/angular.js/blob/c133ef836/src/ng/directive/input.js#L27
2670
- * It is based on the
2671
- * [WHATWG version](https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address) with
2672
- * some enhancements to incorporate more RFC rules (such as rules related to domain names and the
2673
- * lengths of different parts of the address). The main differences from the WHATWG version are:
2674
- * - Disallow `local-part` to begin or end with a period (`.`).
2675
- * - Disallow `local-part` length to exceed 64 characters.
2676
- * - Disallow total address length to exceed 254 characters.
2677
- *
2678
- * See [this commit](https://github.com/angular/angular.js/commit/f3f5cf72e) for more details.
2679
- */
2680
- 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])?)*$/;
2681
- /**
2682
- * Binds a validator to the given path that requires the value to match the standard email format.
2683
- * This function can only be called on string paths.
2684
- *
2685
- * @param path Path of the field to validate
2686
- * @param config Optional, allows providing any of the following options:
2687
- * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.email()`
2688
- * or a function that receives the `FieldContext` and returns custom validation error(s).
2689
- * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
2690
- */
2691
- function email(path, config) {
2692
- validate(path, (ctx) => {
2693
- if (isEmpty(ctx.value())) {
2694
- return undefined;
2695
- }
2696
- if (!EMAIL_REGEXP.test(ctx.value())) {
2697
- if (config?.error) {
2698
- return getOption(config.error, ctx);
2699
- }
2700
- else {
2701
- return emailError({ message: getOption(config?.message, ctx) });
2702
- }
2703
- }
2704
- return undefined;
2705
- });
2706
- }
2707
-
2708
- /**
2709
- * Binds a validator to the given path that requires the value to be less than or equal to the
2710
- * given `maxValue`.
2711
- * This function can only be called on number paths.
2712
- * In addition to binding a validator, this function adds `MAX` property to the field.
2713
- *
2714
- * @param path Path of the field to validate
2715
- * @param maxValue The maximum value, or a LogicFn that returns the maximum value.
2716
- * @param config Optional, allows providing any of the following options:
2717
- * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.max(maxValue)`
2718
- * or a function that receives the `FieldContext` and returns custom validation error(s).
2719
- * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
2720
- */
2721
- function max(path, maxValue, config) {
2722
- const MAX_MEMO = property(path, (ctx) => computed(() => (typeof maxValue === 'number' ? maxValue : maxValue(ctx))));
2723
- aggregateProperty(path, MAX, ({ state }) => state.property(MAX_MEMO)());
2724
- validate(path, (ctx) => {
2725
- if (isEmpty(ctx.value())) {
2726
- return undefined;
2727
- }
2728
- const max = ctx.state.property(MAX_MEMO)();
2729
- if (max === undefined || Number.isNaN(max)) {
2730
- return undefined;
2731
- }
2732
- if (ctx.value() > max) {
2733
- if (config?.error) {
2734
- return getOption(config.error, ctx);
2735
- }
2736
- else {
2737
- return maxError(max, { message: getOption(config?.message, ctx) });
2738
- }
2739
- }
2740
- return undefined;
2741
- });
2742
- }
2743
-
2744
- /**
2745
- * Binds a validator to the given path that requires the length of the value to be less than or
2746
- * equal to the given `maxLength`.
2747
- * This function can only be called on string or array paths.
2748
- * In addition to binding a validator, this function adds `MAX_LENGTH` property to the field.
2749
- *
2750
- * @param path Path of the field to validate
2751
- * @param maxLength The maximum length, or a LogicFn that returns the maximum length.
2752
- * @param config Optional, allows providing any of the following options:
2753
- * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.maxLength(maxLength)`
2754
- * or a function that receives the `FieldContext` and returns custom validation error(s).
2755
- * @template TValue The type of value stored in the field the logic is bound to.
2756
- * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
2757
- */
2758
- function maxLength(path, maxLength, config) {
2759
- const MAX_LENGTH_MEMO = property(path, (ctx) => computed(() => (typeof maxLength === 'number' ? maxLength : maxLength(ctx))));
2760
- aggregateProperty(path, MAX_LENGTH, ({ state }) => state.property(MAX_LENGTH_MEMO)());
2761
- validate(path, (ctx) => {
2762
- if (isEmpty(ctx.value())) {
2763
- return undefined;
2764
- }
2765
- const maxLength = ctx.state.property(MAX_LENGTH_MEMO)();
2766
- if (maxLength === undefined) {
2767
- return undefined;
2768
- }
2769
- if (getLengthOrSize(ctx.value()) > maxLength) {
2770
- if (config?.error) {
2771
- return getOption(config.error, ctx);
2772
- }
2773
- else {
2774
- return maxLengthError(maxLength, { message: getOption(config?.message, ctx) });
2775
- }
2776
- }
2777
- return undefined;
2778
- });
3245
+ * @returns The value for the option.
3246
+ */
3247
+ function getOption(opt, ctx) {
3248
+ return opt instanceof Function ? opt(ctx) : opt;
2779
3249
  }
2780
-
2781
3250
  /**
2782
- * Binds a validator to the given path that requires the value to be greater than or equal to
2783
- * the given `minValue`.
2784
- * This function can only be called on number paths.
2785
- * In addition to binding a validator, this function adds `MIN` property to the field.
2786
- *
2787
- * @param path Path of the field to validate
2788
- * @param minValue The minimum value, or a LogicFn that returns the minimum value.
2789
- * @param config Optional, allows providing any of the following options:
2790
- * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.min(minValue)`
2791
- * or a function that receives the `FieldContext` and returns custom validation error(s).
2792
- * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
3251
+ * Checks if the given value is considered empty. Empty values are: null, undefined, '', false, NaN.
2793
3252
  */
2794
- function min(path, minValue, config) {
2795
- const MIN_MEMO = property(path, (ctx) => computed(() => (typeof minValue === 'number' ? minValue : minValue(ctx))));
2796
- aggregateProperty(path, MIN, ({ state }) => state.property(MIN_MEMO)());
2797
- validate(path, (ctx) => {
2798
- if (isEmpty(ctx.value())) {
2799
- return undefined;
2800
- }
2801
- const min = ctx.state.property(MIN_MEMO)();
2802
- if (min === undefined || Number.isNaN(min)) {
2803
- return undefined;
2804
- }
2805
- if (ctx.value() < min) {
2806
- if (config?.error) {
2807
- return getOption(config.error, ctx);
2808
- }
2809
- else {
2810
- return minError(min, { message: getOption(config?.message, ctx) });
2811
- }
2812
- }
2813
- return undefined;
2814
- });
3253
+ function isEmpty(value) {
3254
+ if (typeof value === 'number') {
3255
+ return isNaN(value);
3256
+ }
3257
+ return value === '' || value === false || value == null;
2815
3258
  }
2816
3259
 
2817
3260
  /**
2818
- * Binds a validator to the given path that requires the length of the value to be greater than or
2819
- * equal to the given `minLength`.
2820
- * This function can only be called on string or array paths.
2821
- * In addition to binding a validator, this function adds `MIN_LENGTH` property to the field.
3261
+ * A regular expression that matches valid e-mail addresses.
2822
3262
  *
2823
- * @param path Path of the field to validate
2824
- * @param minLength The minimum length, or a LogicFn that returns the minimum length.
2825
- * @param config Optional, allows providing any of the following options:
2826
- * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.minLength(minLength)`
2827
- * or a function that receives the `FieldContext` and returns custom validation error(s).
2828
- * @template TValue The type of value stored in the field the logic is bound to.
2829
- * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
3263
+ * At a high level, this regexp matches e-mail addresses of the format `local-part@tld`, where:
3264
+ * - `local-part` consists of one or more of the allowed characters (alphanumeric and some
3265
+ * punctuation symbols).
3266
+ * - `local-part` cannot begin or end with a period (`.`).
3267
+ * - `local-part` cannot be longer than 64 characters.
3268
+ * - `tld` consists of one or more `labels` separated by periods (`.`). For example `localhost` or
3269
+ * `foo.com`.
3270
+ * - A `label` consists of one or more of the allowed characters (alphanumeric, dashes (`-`) and
3271
+ * periods (`.`)).
3272
+ * - A `label` cannot begin or end with a dash (`-`) or a period (`.`).
3273
+ * - A `label` cannot be longer than 63 characters.
3274
+ * - The whole address cannot be longer than 254 characters.
3275
+ *
3276
+ * ## Implementation background
3277
+ *
3278
+ * This regexp was ported over from AngularJS (see there for git history):
3279
+ * https://github.com/angular/angular.js/blob/c133ef836/src/ng/directive/input.js#L27
3280
+ * It is based on the
3281
+ * [WHATWG version](https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address) with
3282
+ * some enhancements to incorporate more RFC rules (such as rules related to domain names and the
3283
+ * lengths of different parts of the address). The main differences from the WHATWG version are:
3284
+ * - Disallow `local-part` to begin or end with a period (`.`).
3285
+ * - Disallow `local-part` length to exceed 64 characters.
3286
+ * - Disallow total address length to exceed 254 characters.
3287
+ *
3288
+ * See [this commit](https://github.com/angular/angular.js/commit/f3f5cf72e) for more details.
2830
3289
  */
2831
- function minLength(path, minLength, config) {
2832
- const MIN_LENGTH_MEMO = property(path, (ctx) => computed(() => (typeof minLength === 'number' ? minLength : minLength(ctx))));
2833
- aggregateProperty(path, MIN_LENGTH, ({ state }) => state.property(MIN_LENGTH_MEMO)());
2834
- validate(path, (ctx) => {
2835
- if (isEmpty(ctx.value())) {
2836
- return undefined;
2837
- }
2838
- const minLength = ctx.state.property(MIN_LENGTH_MEMO)();
2839
- if (minLength === undefined) {
2840
- return undefined;
2841
- }
2842
- if (getLengthOrSize(ctx.value()) < minLength) {
2843
- if (config?.error) {
2844
- return getOption(config.error, ctx);
2845
- }
2846
- else {
2847
- return minLengthError(minLength, { message: getOption(config?.message, ctx) });
2848
- }
2849
- }
2850
- return undefined;
2851
- });
2852
- }
2853
-
3290
+ 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])?)*$/;
2854
3291
  /**
2855
- * Binds a validator to the given path that requires the value to match a specific regex pattern.
3292
+ * Binds a validator to the given path that requires the value to match the standard email format.
2856
3293
  * This function can only be called on string paths.
2857
- * In addition to binding a validator, this function adds `PATTERN` property to the field.
2858
3294
  *
2859
3295
  * @param path Path of the field to validate
2860
- * @param pattern The RegExp pattern to match, or a LogicFn that returns the RegExp pattern.
2861
3296
  * @param config Optional, allows providing any of the following options:
2862
- * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.pattern(pattern)`
3297
+ * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.email()`
2863
3298
  * or a function that receives the `FieldContext` and returns custom validation error(s).
2864
3299
  * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
3300
+ *
3301
+ * @experimental 21.0.0
2865
3302
  */
2866
- function pattern(path, pattern, config) {
2867
- const PATTERN_MEMO = property(path, (ctx) => computed(() => (pattern instanceof RegExp ? pattern : pattern(ctx))));
2868
- aggregateProperty(path, PATTERN, ({ state }) => state.property(PATTERN_MEMO)());
3303
+ function email(path, config) {
2869
3304
  validate(path, (ctx) => {
2870
3305
  if (isEmpty(ctx.value())) {
2871
3306
  return undefined;
2872
3307
  }
2873
- const pattern = ctx.state.property(PATTERN_MEMO)();
2874
- if (pattern === undefined) {
2875
- return undefined;
2876
- }
2877
- if (!pattern.test(ctx.value())) {
3308
+ if (!EMAIL_REGEXP.test(ctx.value())) {
2878
3309
  if (config?.error) {
2879
3310
  return getOption(config.error, ctx);
2880
3311
  }
2881
3312
  else {
2882
- return patternError(pattern, { message: getOption(config?.message, ctx) });
3313
+ return emailError({ message: getOption(config?.message, ctx) });
2883
3314
  }
2884
3315
  }
2885
3316
  return undefined;
@@ -2887,490 +3318,290 @@ function pattern(path, pattern, config) {
2887
3318
  }
2888
3319
 
2889
3320
  /**
2890
- * Binds a validator to the given path that requires the value to be non-empty.
2891
- * This function can only be called on any type of path.
2892
- * In addition to binding a validator, this function adds `REQUIRED` property to the field.
3321
+ * Binds a validator to the given path that requires the value to be less than or equal to the
3322
+ * given `maxValue`.
3323
+ * This function can only be called on number paths.
3324
+ * In addition to binding a validator, this function adds `MAX` property to the field.
2893
3325
  *
2894
3326
  * @param path Path of the field to validate
3327
+ * @param maxValue The maximum value, or a LogicFn that returns the maximum value.
2895
3328
  * @param config Optional, allows providing any of the following options:
2896
- * - `message`: A user-facing message for the error.
2897
- * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.required()`
3329
+ * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.max(maxValue)`
2898
3330
  * or a function that receives the `FieldContext` and returns custom validation error(s).
2899
- * - `when`: A function that receives the `FieldContext` and returns true if the field is required
2900
- * @template TValue The type of value stored in the field the logic is bound to.
2901
3331
  * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
3332
+ *
3333
+ * @experimental 21.0.0
2902
3334
  */
2903
- function required(path, config) {
2904
- const REQUIRED_MEMO = property(path, (ctx) => computed(() => (config?.when ? config.when(ctx) : true)));
2905
- aggregateProperty(path, REQUIRED, ({ state }) => state.property(REQUIRED_MEMO)());
3335
+ function max(path, maxValue, config) {
3336
+ const MAX_MEMO = property(path, (ctx) => computed(() => (typeof maxValue === 'number' ? maxValue : maxValue(ctx))));
3337
+ aggregateProperty(path, MAX, ({ state }) => state.property(MAX_MEMO)());
2906
3338
  validate(path, (ctx) => {
2907
- if (ctx.state.property(REQUIRED_MEMO)() && isEmpty(ctx.value())) {
2908
- if (config?.error) {
2909
- return getOption(config.error, ctx);
2910
- }
2911
- else {
2912
- return requiredError({ message: getOption(config?.message, ctx) });
2913
- }
2914
- }
2915
- return undefined;
2916
- });
2917
- }
2918
-
2919
- // TODO: These utilities to be replaced with proper integration into framework.
2920
- function privateGetComponentInstance(injector) {
2921
- assertIsNodeInjector(injector);
2922
- if (injector._tNode.directiveStart === 0 || injector._tNode.componentOffset === -1) {
2923
- return undefined;
2924
- }
2925
- return injector._lView[injector._tNode.directiveStart + injector._tNode.componentOffset];
2926
- }
2927
- function privateSetComponentInput(inputSignal, value) {
2928
- inputSignal[_SIGNAL].applyValueToInputSignal(inputSignal[_SIGNAL], value);
2929
- }
2930
- function privateIsSignalInput(value) {
2931
- return isInputSignal(value);
2932
- }
2933
- function privateIsModelInput(value) {
2934
- return isInputSignal(value) && isObject(value) && 'subscribe' in value;
2935
- }
2936
- function privateRunEffect(ref) {
2937
- ref[_SIGNAL].run();
2938
- }
2939
- function assertIsNodeInjector(injector) {
2940
- if (!('_tNode' in injector)) {
2941
- throw new Error('Expected a Node Injector');
2942
- }
2943
- }
2944
- function isInputSignal(value) {
2945
- if (!isObject(value) || !(_SIGNAL in value)) {
2946
- return false;
2947
- }
2948
- const node = value[_SIGNAL];
2949
- return isObject(node) && 'applyValueToInputSignal' in node;
2950
- }
2951
-
2952
- /**
2953
- * A fake version of `NgControl` provided by the `Control` directive. This allows interoperability
2954
- * with a wider range of components designed to work with reactive forms, in particular ones that
2955
- * inject the `NgControl`. The interop control does not implement *all* properties and methods of
2956
- * the real `NgControl`, but does implement some of the most commonly used ones that have a clear
2957
- * equivalent in signal forms.
2958
- */
2959
- class InteropNgControl {
2960
- field;
2961
- constructor(field) {
2962
- this.field = field;
2963
- }
2964
- control = this;
2965
- get value() {
2966
- return this.field().value();
2967
- }
2968
- get valid() {
2969
- return this.field().valid();
2970
- }
2971
- get invalid() {
2972
- return this.field().invalid();
2973
- }
2974
- get pending() {
2975
- return this.field().pending();
2976
- }
2977
- get disabled() {
2978
- return this.field().disabled();
2979
- }
2980
- get enabled() {
2981
- return !this.field().disabled();
2982
- }
2983
- get errors() {
2984
- const errors = this.field().errors();
2985
- if (errors.length === 0) {
2986
- return null;
2987
- }
2988
- const errObj = {};
2989
- for (const error of errors) {
2990
- errObj[error.kind] = error;
2991
- }
2992
- return errObj;
2993
- }
2994
- get pristine() {
2995
- return !this.field().dirty();
2996
- }
2997
- get dirty() {
2998
- return this.field().dirty();
2999
- }
3000
- get touched() {
3001
- return this.field().touched();
3002
- }
3003
- get untouched() {
3004
- return !this.field().touched();
3005
- }
3006
- get status() {
3007
- if (this.field().disabled()) {
3008
- return 'DISABLED';
3009
- }
3010
- if (this.field().valid()) {
3011
- return 'VALID';
3012
- }
3013
- if (this.field().invalid()) {
3014
- return 'INVALID';
3339
+ if (isEmpty(ctx.value())) {
3340
+ return undefined;
3015
3341
  }
3016
- if (this.field().pending()) {
3017
- return 'PENDING';
3342
+ const max = ctx.state.property(MAX_MEMO)();
3343
+ if (max === undefined || Number.isNaN(max)) {
3344
+ return undefined;
3018
3345
  }
3019
- throw Error('AssertionError: unknown form control status');
3020
- }
3021
- valueAccessor = null;
3022
- hasValidator(validator) {
3023
- // This addresses a common case where users look for the presence of `Validators.required` to
3024
- // determine whether or not to show a required "*" indicator in the UI.
3025
- if (validator === Validators.required) {
3026
- return this.field().property(REQUIRED)();
3346
+ if (ctx.value() > max) {
3347
+ if (config?.error) {
3348
+ return getOption(config.error, ctx);
3349
+ }
3350
+ else {
3351
+ return maxError(max, { message: getOption(config?.message, ctx) });
3352
+ }
3027
3353
  }
3028
- return false;
3029
- }
3030
- updateValueAndValidity() {
3031
- // No-op since value and validity are always up to date in signal forms.
3032
- // We offer this method so that reactive forms code attempting to call it doesn't error.
3033
- }
3354
+ return undefined;
3355
+ });
3034
3356
  }
3035
3357
 
3036
3358
  /**
3037
- * Binds a form `Field` to a UI control that edits it. A UI control can be one of several things:
3038
- * 1. A native HTML input or textarea
3039
- * 2. A signal forms custom control that implements `FormValueControl` or `FormCheckboxControl`
3040
- * 3. A component that provides a ControlValueAccessor. This should only be used to backwards
3041
- * compatibility with reactive forms. Prefer options (1) and (2).
3359
+ * Binds a validator to the given path that requires the length of the value to be less than or
3360
+ * equal to the given `maxLength`.
3361
+ * This function can only be called on string or array paths.
3362
+ * In addition to binding a validator, this function adds `MAX_LENGTH` property to the field.
3042
3363
  *
3043
- * This directive has several responsibilities:
3044
- * 1. Two-way binds the field's value with the UI control's value
3045
- * 2. Binds additional forms related state on the field to the UI control (disabled, required, etc.)
3046
- * 3. Relays relevant events on the control to the field (e.g. marks field touched on blur)
3047
- * 4. Provides a fake `NgControl` that implements a subset of the features available on the reactive
3048
- * forms `NgControl`. This is provided to improve interoperability with controls designed to work
3049
- * with reactive forms. It should not be used by controls written for signal forms.
3364
+ * @param path Path of the field to validate
3365
+ * @param maxLength The maximum length, or a LogicFn that returns the maximum length.
3366
+ * @param config Optional, allows providing any of the following options:
3367
+ * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.maxLength(maxLength)`
3368
+ * or a function that receives the `FieldContext` and returns custom validation error(s).
3369
+ * @template TValue The type of value stored in the field the logic is bound to.
3370
+ * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
3371
+ *
3372
+ * @experimental 21.0.0
3050
3373
  */
3051
- class Control {
3052
- /** The injector for this component. */
3053
- injector = inject(Injector);
3054
- renderer = inject(Renderer2);
3055
- /** Whether state synchronization with the field has been setup yet. */
3056
- initialized = false;
3057
- /** The field that is bound to this control. */
3058
- field = signal(undefined, ...(ngDevMode ? [{ debugName: "field" }] : []));
3059
- // If `[control]` is applied to a custom UI control, it wants to synchronize state in the field w/
3060
- // the inputs of that custom control. This is difficult to do in user-land. We use `effect`, but
3061
- // effects don't run before the lifecycle hooks of the component. This is usually okay, but has
3062
- // one significant issue: the UI control's required inputs won't be set in time for those
3063
- // lifecycle hooks to run.
3064
- //
3065
- // Eventually we can build custom functionality for the `Control` directive into the framework,
3066
- // but for now we work around this limitation with a hack. We use an `@Input` instead of a
3067
- // signal-based `input()` for the `[control]` to hook the exact moment inputs are being set,
3068
- // before the important lifecycle hooks of the UI control. We can then initialize all our effects
3069
- // and force them to run immediately, ensuring all required inputs have values.
3070
- set _field(value) {
3071
- this.field.set(value);
3072
- if (!this.initialized) {
3073
- this.initialize();
3074
- }
3075
- }
3076
- /** The field state of the bound field. */
3077
- state = computed(() => this.field()(), ...(ngDevMode ? [{ debugName: "state" }] : []));
3078
- /** The HTMLElement this directive is attached to. */
3079
- el = inject(ElementRef);
3080
- /** The NG_VALUE_ACCESSOR array for the host component. */
3081
- cvaArray = inject(NG_VALUE_ACCESSOR, { optional: true });
3082
- /** The Cached value for the lazily created interop NgControl. */
3083
- _ngControl;
3084
- /** A fake NgControl provided for better interop with reactive forms. */
3085
- get ngControl() {
3086
- return (this._ngControl ??= new InteropNgControl(() => this.state()));
3087
- }
3088
- /** The ControlValueAccessor for the host component. */
3089
- get cva() {
3090
- return this.cvaArray?.[0] ?? this._ngControl?.valueAccessor ?? undefined;
3091
- }
3092
- /** Initializes state synchronization between the field and the host UI control. */
3093
- initialize() {
3094
- this.initialized = true;
3095
- const injector = this.injector;
3096
- const cmp = privateGetComponentInstance(injector);
3097
- // If component has a `control` input, we assume that it will handle binding the field to the
3098
- // appropriate native/custom control in its template, so we do not attempt to bind any inputs on
3099
- // this component.
3100
- if (cmp && isShadowedControlComponent(cmp)) {
3101
- return;
3102
- }
3103
- if (cmp && isFormUiControl(cmp)) {
3104
- // If we're binding to a component that follows the standard form ui control contract,
3105
- // set up state synchronization based on the contract.
3106
- this.setupCustomUiControl(cmp);
3107
- }
3108
- else if (this.cva !== undefined) {
3109
- // If we're binding to a component that doesn't follow the standard contract, but provides a
3110
- // control value accessor, set up state synchronization based on th CVA.
3111
- this.setupControlValueAccessor(this.cva);
3112
- }
3113
- else if (this.el.nativeElement instanceof HTMLInputElement ||
3114
- this.el.nativeElement instanceof HTMLTextAreaElement ||
3115
- this.el.nativeElement instanceof HTMLSelectElement) {
3116
- // If we're binding to a native html input, set up state synchronization with its native
3117
- // properties / attributes.
3118
- this.setupNativeInput(this.el.nativeElement);
3374
+ function maxLength(path, maxLength, config) {
3375
+ const MAX_LENGTH_MEMO = property(path, (ctx) => computed(() => (typeof maxLength === 'number' ? maxLength : maxLength(ctx))));
3376
+ aggregateProperty(path, MAX_LENGTH, ({ state }) => state.property(MAX_LENGTH_MEMO)());
3377
+ validate(path, (ctx) => {
3378
+ if (isEmpty(ctx.value())) {
3379
+ return undefined;
3119
3380
  }
3120
- else {
3121
- throw new Error(`Unhandled control?`);
3381
+ const maxLength = ctx.state.property(MAX_LENGTH_MEMO)();
3382
+ if (maxLength === undefined) {
3383
+ return undefined;
3122
3384
  }
3123
- // Register this control on the field it is currently bound to. We do this at the end of
3124
- // initialization so that it only runs if we are actually syncing with this control
3125
- // (as opposed to just passing the field through to its `control` input).
3126
- effect((onCleanup) => {
3127
- const fieldNode = this.state();
3128
- fieldNode.nodeState.controls.update((controls) => [...controls, this]);
3129
- onCleanup(() => {
3130
- fieldNode.nodeState.controls.update((controls) => controls.filter((c) => c !== this));
3131
- });
3132
- }, { injector: this.injector });
3133
- }
3134
- /**
3135
- * Set up state synchronization between the field and a native <input>, <textarea>, or <select>.
3136
- */
3137
- setupNativeInput(input) {
3138
- const inputType = input instanceof HTMLTextAreaElement
3139
- ? 'text'
3140
- : input instanceof HTMLSelectElement
3141
- ? 'select'
3142
- : input.type;
3143
- input.addEventListener('input', () => {
3144
- switch (inputType) {
3145
- case 'checkbox':
3146
- this.state().value.set(input.checked);
3147
- break;
3148
- case 'radio':
3149
- // The `input` event only fires when a radio button becomes selected, so write its `value`
3150
- // into the state.
3151
- this.state().value.set(input.value);
3152
- break;
3153
- default:
3154
- this.state().value.set(input.value);
3155
- break;
3385
+ if (getLengthOrSize(ctx.value()) > maxLength) {
3386
+ if (config?.error) {
3387
+ return getOption(config.error, ctx);
3388
+ }
3389
+ else {
3390
+ return maxLengthError(maxLength, { message: getOption(config?.message, ctx) });
3156
3391
  }
3157
- this.state().markAsDirty();
3158
- });
3159
- input.addEventListener('blur', () => this.state().markAsTouched());
3160
- this.maybeSynchronize(() => this.state().readonly(), this.withBooleanAttribute(input, 'readonly'));
3161
- // TODO: consider making a global configuration option for using aria-disabled instead.
3162
- this.maybeSynchronize(() => this.state().disabled(), this.withBooleanAttribute(input, 'disabled'));
3163
- this.maybeSynchronize(() => this.state().name(), this.withAttribute(input, 'name'));
3164
- this.maybeSynchronize(this.propertySource(REQUIRED), this.withBooleanAttribute(input, 'required'));
3165
- this.maybeSynchronize(this.propertySource(MIN), this.withAttribute(input, 'min'));
3166
- this.maybeSynchronize(this.propertySource(MIN_LENGTH), this.withAttribute(input, 'minLength'));
3167
- this.maybeSynchronize(this.propertySource(MAX), this.withAttribute(input, 'max'));
3168
- this.maybeSynchronize(this.propertySource(MAX_LENGTH), this.withAttribute(input, 'maxLength'));
3169
- switch (inputType) {
3170
- case 'checkbox':
3171
- this.maybeSynchronize(() => this.state().value(), (value) => (input.checked = value));
3172
- break;
3173
- case 'radio':
3174
- this.maybeSynchronize(() => this.state().value(), (value) => {
3175
- // Although HTML behavior is to clear the input already, we do this just in case.
3176
- // It seems like it might be necessary in certain environments (e.g. Domino).
3177
- input.checked = input.value === value;
3178
- });
3179
- break;
3180
- case 'select':
3181
- this.maybeSynchronize(() => this.state().value(), (value) => {
3182
- // A select will not take a value unil the value's option has rendered.
3183
- afterNextRender(() => (input.value = value), { injector: this.injector });
3184
- });
3185
- break;
3186
- default:
3187
- this.maybeSynchronize(() => this.state().value(), (value) => {
3188
- input.value = value;
3189
- });
3190
- break;
3191
- }
3192
- }
3193
- /** Set up state synchronization between the field and a ControlValueAccessor. */
3194
- setupControlValueAccessor(cva) {
3195
- cva.registerOnChange((value) => this.state().value.set(value));
3196
- cva.registerOnTouched(() => this.state().markAsTouched());
3197
- this.maybeSynchronize(() => this.state().value(), (value) => cva.writeValue(value));
3198
- if (cva.setDisabledState) {
3199
- this.maybeSynchronize(() => this.state().disabled(), (value) => cva.setDisabledState(value));
3200
- }
3201
- cva.writeValue(this.state().value());
3202
- cva.setDisabledState?.(this.state().disabled());
3203
- }
3204
- /** Set up state synchronization between the field and a FormUiControl. */
3205
- setupCustomUiControl(cmp) {
3206
- // Handle the property side of the model binding. How we do this depends on the shape of the
3207
- // component. There are 2 options:
3208
- // * it provides a `value` model (most controls that edit a single value)
3209
- // * it provides a `checked` model with no `value` signal (custom checkbox)
3210
- let cleanupValue;
3211
- if (isFormValueControl(cmp)) {
3212
- // <custom-input [(value)]="state().value">
3213
- this.maybeSynchronize(() => this.state().value(), withInput(cmp.value));
3214
- cleanupValue = cmp.value.subscribe((newValue) => this.state().value.set(newValue));
3215
- }
3216
- else if (isFormCheckboxControl(cmp)) {
3217
- // <custom-checkbox [(checked)]="state().value" />
3218
- this.maybeSynchronize(() => this.state().value(), withInput(cmp.checked));
3219
- cleanupValue = cmp.checked.subscribe((newValue) => this.state().value.set(newValue));
3220
3392
  }
3221
- else {
3222
- throw new Error(`Unknown custom control subtype`);
3393
+ return undefined;
3394
+ });
3395
+ }
3396
+
3397
+ /**
3398
+ * Binds a validator to the given path that requires the value to be greater than or equal to
3399
+ * the given `minValue`.
3400
+ * This function can only be called on number paths.
3401
+ * In addition to binding a validator, this function adds `MIN` property to the field.
3402
+ *
3403
+ * @param path Path of the field to validate
3404
+ * @param minValue The minimum value, or a LogicFn that returns the minimum value.
3405
+ * @param config Optional, allows providing any of the following options:
3406
+ * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.min(minValue)`
3407
+ * or a function that receives the `FieldContext` and returns custom validation error(s).
3408
+ * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
3409
+ *
3410
+ * @experimental 21.0.0
3411
+ */
3412
+ function min(path, minValue, config) {
3413
+ const MIN_MEMO = property(path, (ctx) => computed(() => (typeof minValue === 'number' ? minValue : minValue(ctx))));
3414
+ aggregateProperty(path, MIN, ({ state }) => state.property(MIN_MEMO)());
3415
+ validate(path, (ctx) => {
3416
+ if (isEmpty(ctx.value())) {
3417
+ return undefined;
3223
3418
  }
3224
- this.maybeSynchronize(() => this.state().name(), withInput(cmp.name));
3225
- this.maybeSynchronize(() => this.state().disabled(), withInput(cmp.disabled));
3226
- this.maybeSynchronize(() => this.state().disabledReasons(), withInput(cmp.disabledReasons));
3227
- this.maybeSynchronize(() => this.state().readonly(), withInput(cmp.readonly));
3228
- this.maybeSynchronize(() => this.state().hidden(), withInput(cmp.hidden));
3229
- this.maybeSynchronize(() => this.state().errors(), withInput(cmp.errors));
3230
- if (privateIsModelInput(cmp.touched) || privateIsSignalInput(cmp.touched)) {
3231
- this.maybeSynchronize(() => this.state().touched(), withInput(cmp.touched));
3419
+ const min = ctx.state.property(MIN_MEMO)();
3420
+ if (min === undefined || Number.isNaN(min)) {
3421
+ return undefined;
3232
3422
  }
3233
- this.maybeSynchronize(() => this.state().dirty(), withInput(cmp.dirty));
3234
- this.maybeSynchronize(() => this.state().invalid(), withInput(cmp.invalid));
3235
- this.maybeSynchronize(() => this.state().pending(), withInput(cmp.pending));
3236
- this.maybeSynchronize(this.propertySource(REQUIRED), withInput(cmp.required));
3237
- this.maybeSynchronize(this.propertySource(MIN), withInput(cmp.min));
3238
- this.maybeSynchronize(this.propertySource(MIN_LENGTH), withInput(cmp.minLength));
3239
- this.maybeSynchronize(this.propertySource(MAX), withInput(cmp.max));
3240
- this.maybeSynchronize(this.propertySource(MAX_LENGTH), withInput(cmp.maxLength));
3241
- this.maybeSynchronize(this.propertySource(PATTERN), withInput(cmp.pattern));
3242
- let cleanupTouch;
3243
- let cleanupDefaultTouch;
3244
- if (privateIsModelInput(cmp.touched) || isOutputRef(cmp.touched)) {
3245
- cleanupTouch = cmp.touched.subscribe(() => this.state().markAsTouched());
3423
+ if (ctx.value() < min) {
3424
+ if (config?.error) {
3425
+ return getOption(config.error, ctx);
3426
+ }
3427
+ else {
3428
+ return minError(min, { message: getOption(config?.message, ctx) });
3429
+ }
3246
3430
  }
3247
- else {
3248
- // If the component did not give us a touch event stream, use the standard touch logic,
3249
- // marking it touched when the focus moves from inside the host element to outside.
3250
- const listener = (event) => {
3251
- const newActiveEl = event.relatedTarget;
3252
- if (!this.el.nativeElement.contains(newActiveEl)) {
3253
- this.state().markAsTouched();
3254
- }
3255
- };
3256
- this.el.nativeElement.addEventListener('focusout', listener);
3257
- cleanupDefaultTouch = () => this.el.nativeElement.removeEventListener('focusout', listener);
3431
+ return undefined;
3432
+ });
3433
+ }
3434
+
3435
+ /**
3436
+ * Binds a validator to the given path that requires the length of the value to be greater than or
3437
+ * equal to the given `minLength`.
3438
+ * This function can only be called on string or array paths.
3439
+ * In addition to binding a validator, this function adds `MIN_LENGTH` property to the field.
3440
+ *
3441
+ * @param path Path of the field to validate
3442
+ * @param minLength The minimum length, or a LogicFn that returns the minimum length.
3443
+ * @param config Optional, allows providing any of the following options:
3444
+ * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.minLength(minLength)`
3445
+ * or a function that receives the `FieldContext` and returns custom validation error(s).
3446
+ * @template TValue The type of value stored in the field the logic is bound to.
3447
+ * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
3448
+ *
3449
+ * @experimental 21.0.0
3450
+ */
3451
+ function minLength(path, minLength, config) {
3452
+ const MIN_LENGTH_MEMO = property(path, (ctx) => computed(() => (typeof minLength === 'number' ? minLength : minLength(ctx))));
3453
+ aggregateProperty(path, MIN_LENGTH, ({ state }) => state.property(MIN_LENGTH_MEMO)());
3454
+ validate(path, (ctx) => {
3455
+ if (isEmpty(ctx.value())) {
3456
+ return undefined;
3258
3457
  }
3259
- // Cleanup for output binding subscriptions:
3260
- this.injector.get(DestroyRef).onDestroy(() => {
3261
- cleanupValue?.unsubscribe();
3262
- cleanupTouch?.unsubscribe();
3263
- cleanupDefaultTouch?.();
3264
- });
3265
- }
3266
- /** Synchronize a value from a reactive source to a given sink. */
3267
- maybeSynchronize(source, sink) {
3268
- if (!sink) {
3458
+ const minLength = ctx.state.property(MIN_LENGTH_MEMO)();
3459
+ if (minLength === undefined) {
3269
3460
  return undefined;
3270
3461
  }
3271
- const ref = effect(() => {
3272
- const value = source();
3273
- untracked(() => sink(value));
3274
- }, ...(ngDevMode ? [{ debugName: "ref", injector: this.injector }] : [{ injector: this.injector }]));
3275
- // Run the effect immediately to ensure sinks which are required inputs are set before they can
3276
- // be observed. See the note on `_field` for more details.
3277
- privateRunEffect(ref);
3278
- }
3279
- /** Creates a reactive value source by reading the given AggregateProperty from the field. */
3280
- propertySource(key) {
3281
- const metaSource = computed(() => this.state().hasProperty(key) ? this.state().property(key) : key.getInitial, ...(ngDevMode ? [{ debugName: "metaSource" }] : []));
3282
- return () => metaSource()?.();
3283
- }
3284
- /** Creates a (non-boolean) value sync that writes the given attribute of the given element. */
3285
- withAttribute(element, attribute) {
3286
- return (value) => {
3287
- if (value !== undefined) {
3288
- this.renderer.setAttribute(element, attribute, value.toString());
3462
+ if (getLengthOrSize(ctx.value()) < minLength) {
3463
+ if (config?.error) {
3464
+ return getOption(config.error, ctx);
3289
3465
  }
3290
3466
  else {
3291
- this.renderer.removeAttribute(element, attribute);
3467
+ return minLengthError(minLength, { message: getOption(config?.message, ctx) });
3292
3468
  }
3293
- };
3294
- }
3295
- /** Creates a boolean value sync that writes the given attribute of the given element. */
3296
- withBooleanAttribute(element, attribute) {
3297
- return (value) => {
3298
- if (value) {
3299
- this.renderer.setAttribute(element, attribute, '');
3469
+ }
3470
+ return undefined;
3471
+ });
3472
+ }
3473
+
3474
+ /**
3475
+ * Binds a validator to the given path that requires the value to match a specific regex pattern.
3476
+ * This function can only be called on string paths.
3477
+ * In addition to binding a validator, this function adds `PATTERN` property to the field.
3478
+ *
3479
+ * @param path Path of the field to validate
3480
+ * @param pattern The RegExp pattern to match, or a LogicFn that returns the RegExp pattern.
3481
+ * @param config Optional, allows providing any of the following options:
3482
+ * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.pattern(pattern)`
3483
+ * or a function that receives the `FieldContext` and returns custom validation error(s).
3484
+ * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
3485
+ *
3486
+ * @experimental 21.0.0
3487
+ */
3488
+ function pattern(path, pattern, config) {
3489
+ const PATTERN_MEMO = property(path, (ctx) => computed(() => (pattern instanceof RegExp ? pattern : pattern(ctx))));
3490
+ aggregateProperty(path, PATTERN, ({ state }) => state.property(PATTERN_MEMO)());
3491
+ validate(path, (ctx) => {
3492
+ if (isEmpty(ctx.value())) {
3493
+ return undefined;
3494
+ }
3495
+ const pattern = ctx.state.property(PATTERN_MEMO)();
3496
+ if (pattern === undefined) {
3497
+ return undefined;
3498
+ }
3499
+ if (!pattern.test(ctx.value())) {
3500
+ if (config?.error) {
3501
+ return getOption(config.error, ctx);
3300
3502
  }
3301
3503
  else {
3302
- this.renderer.removeAttribute(element, attribute);
3504
+ return patternError(pattern, { message: getOption(config?.message, ctx) });
3303
3505
  }
3304
- };
3305
- }
3306
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: Control, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3307
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.2.0-next.2", type: Control, isStandalone: true, selector: "[control]", inputs: { _field: ["control", "_field"] }, providers: [
3308
- {
3309
- provide: NgControl,
3310
- useFactory: () => inject(Control).ngControl,
3311
- },
3312
- ], ngImport: i0 });
3313
- }
3314
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: Control, decorators: [{
3315
- type: Directive,
3316
- args: [{
3317
- selector: '[control]',
3318
- providers: [
3319
- {
3320
- provide: NgControl,
3321
- useFactory: () => inject(Control).ngControl,
3322
- },
3323
- ],
3324
- }]
3325
- }], propDecorators: { _field: [{
3326
- type: Input,
3327
- args: [{ required: true, alias: 'control' }]
3328
- }] } });
3329
- /** Creates a value sync from an input signal. */
3330
- function withInput(input) {
3331
- return input ? (value) => privateSetComponentInput(input, value) : undefined;
3506
+ }
3507
+ return undefined;
3508
+ });
3332
3509
  }
3510
+
3333
3511
  /**
3334
- * Checks whether the given component matches the contract for either FormValueControl or
3335
- * FormCheckboxControl.
3512
+ * Binds a validator to the given path that requires the value to be non-empty.
3513
+ * This function can only be called on any type of path.
3514
+ * In addition to binding a validator, this function adds `REQUIRED` property to the field.
3515
+ *
3516
+ * @param path Path of the field to validate
3517
+ * @param config Optional, allows providing any of the following options:
3518
+ * - `message`: A user-facing message for the error.
3519
+ * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.required()`
3520
+ * or a function that receives the `FieldContext` and returns custom validation error(s).
3521
+ * - `when`: A function that receives the `FieldContext` and returns true if the field is required
3522
+ * @template TValue The type of value stored in the field the logic is bound to.
3523
+ * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
3524
+ *
3525
+ * @experimental 21.0.0
3336
3526
  */
3337
- function isFormUiControl(cmp) {
3338
- const castCmp = cmp;
3339
- return ((isFormValueControl(castCmp) || isFormCheckboxControl(castCmp)) &&
3340
- (castCmp.readonly === undefined || privateIsSignalInput(castCmp.readonly)) &&
3341
- (castCmp.disabled === undefined || privateIsSignalInput(castCmp.disabled)) &&
3342
- (castCmp.disabledReasons === undefined || privateIsSignalInput(castCmp.disabledReasons)) &&
3343
- (castCmp.errors === undefined || privateIsSignalInput(castCmp.errors)) &&
3344
- (castCmp.invalid === undefined || privateIsSignalInput(castCmp.invalid)) &&
3345
- (castCmp.pending === undefined || privateIsSignalInput(castCmp.pending)) &&
3346
- (castCmp.touched === undefined ||
3347
- privateIsModelInput(castCmp.touched) ||
3348
- privateIsSignalInput(castCmp.touched) ||
3349
- isOutputRef(castCmp.touched)) &&
3350
- (castCmp.dirty === undefined || privateIsSignalInput(castCmp.dirty)) &&
3351
- (castCmp.min === undefined || privateIsSignalInput(castCmp.min)) &&
3352
- (castCmp.minLength === undefined || privateIsSignalInput(castCmp.minLength)) &&
3353
- (castCmp.max === undefined || privateIsSignalInput(castCmp.max)) &&
3354
- (castCmp.maxLength === undefined || privateIsSignalInput(castCmp.maxLength)));
3355
- }
3356
- /** Checks whether the given FormUiControl is a FormValueControl. */
3357
- function isFormValueControl(cmp) {
3358
- return privateIsModelInput(cmp.value);
3359
- }
3360
- /** Checks whether the given FormUiControl is a FormCheckboxControl. */
3361
- function isFormCheckboxControl(cmp) {
3362
- return (privateIsModelInput(cmp.checked) &&
3363
- cmp.value === undefined);
3527
+ function required(path, config) {
3528
+ const REQUIRED_MEMO = property(path, (ctx) => computed(() => (config?.when ? config.when(ctx) : true)));
3529
+ aggregateProperty(path, REQUIRED, ({ state }) => state.property(REQUIRED_MEMO)());
3530
+ validate(path, (ctx) => {
3531
+ if (ctx.state.property(REQUIRED_MEMO)() && isEmpty(ctx.value())) {
3532
+ if (config?.error) {
3533
+ return getOption(config.error, ctx);
3534
+ }
3535
+ else {
3536
+ return requiredError({ message: getOption(config?.message, ctx) });
3537
+ }
3538
+ }
3539
+ return undefined;
3540
+ });
3364
3541
  }
3365
- /** Checks whether the given component has an input called `control`. */
3366
- function isShadowedControlComponent(cmp) {
3367
- const mirror = reflectComponentType(cmp.constructor);
3368
- return mirror?.inputs.some((input) => input.templateName === 'control') ?? false;
3542
+
3543
+ /**
3544
+ * Validates a field using a `StandardSchemaV1` compatible validator (e.g. a Zod validator).
3545
+ *
3546
+ * See https://github.com/standard-schema/standard-schema for more about standard schema.
3547
+ *
3548
+ * @param path The `FieldPath` to the field to validate.
3549
+ * @param schema The standard schema compatible validator to use for validation.
3550
+ * @template TSchema The type validated by the schema. This may be either the full `TValue` type,
3551
+ * or a partial of it.
3552
+ * @template TValue The type of value stored in the field being validated.
3553
+ *
3554
+ * @experimental 21.0.0
3555
+ */
3556
+ function validateStandardSchema(path, schema) {
3557
+ // We create both a sync and async validator because the standard schema validator can return
3558
+ // either a sync result or a Promise, and we need to handle both cases. The sync validator
3559
+ // handles the sync result, and the async validator handles the Promise.
3560
+ // We memoize the result of the validation function here, so that it is only run once for both
3561
+ // validators, it can then be passed through both sync & async validation.
3562
+ const VALIDATOR_MEMO = property(path, ({ value }) => {
3563
+ return computed(() => schema['~standard'].validate(value()));
3564
+ });
3565
+ validateTree(path, ({ state, fieldOf }) => {
3566
+ // Skip sync validation if the result is a Promise.
3567
+ const result = state.property(VALIDATOR_MEMO)();
3568
+ if (_isPromise(result)) {
3569
+ return [];
3570
+ }
3571
+ return result.issues?.map((issue) => standardIssueToFormTreeError(fieldOf(path), issue)) ?? [];
3572
+ });
3573
+ validateAsync(path, {
3574
+ params: ({ state }) => {
3575
+ // Skip async validation if the result is *not* a Promise.
3576
+ const result = state.property(VALIDATOR_MEMO)();
3577
+ return _isPromise(result) ? result : undefined;
3578
+ },
3579
+ factory: (params) => {
3580
+ return resource({
3581
+ params,
3582
+ loader: async ({ params }) => (await params)?.issues ?? [],
3583
+ });
3584
+ },
3585
+ errors: (issues, { fieldOf }) => {
3586
+ return issues.map((issue) => standardIssueToFormTreeError(fieldOf(path), issue));
3587
+ },
3588
+ });
3369
3589
  }
3370
- /** Checks whether the given object is an output ref. */
3371
- function isOutputRef(value) {
3372
- return value instanceof OutputEmitterRef || value instanceof EventEmitter;
3590
+ /**
3591
+ * Converts a `StandardSchemaV1.Issue` to a `FormTreeError`.
3592
+ *
3593
+ * @param field The root field to which the issue's path is relative.
3594
+ * @param issue The `StandardSchemaV1.Issue` to convert.
3595
+ * @returns A `ValidationError` representing the issue.
3596
+ */
3597
+ function standardIssueToFormTreeError(field, issue) {
3598
+ let target = field;
3599
+ for (const pathPart of issue.path ?? []) {
3600
+ const pathKey = typeof pathPart === 'object' ? pathPart.key : pathPart;
3601
+ target = target[pathKey];
3602
+ }
3603
+ return addDefaultField(standardSchemaError(issue), target);
3373
3604
  }
3374
3605
 
3375
- export { AggregateProperty, Control, CustomValidationError, EmailValidationError, InteropNgControl, MAX, MAX_LENGTH, MIN, MIN_LENGTH, MaxLengthValidationError, MaxValidationError, MinLengthValidationError, MinValidationError, NgValidationError, PATTERN, PatternValidationError, Property, REQUIRED, RequiredValidationError, StandardSchemaValidationError, aggregateProperty, andProperty, apply, applyEach, applyWhen, applyWhenValue, createProperty, customError, disabled, email, emailError, form, hidden, listProperty, max, maxError, maxLength, maxLengthError, maxProperty, min, minError, minLength, minLengthError, minProperty, orProperty, pattern, patternError, property, readonly, reducedProperty, required, requiredError, schema, standardSchemaError, submit, validate, validateAsync, validateHttp, validateTree };
3606
+ export { AggregateProperty, Control, CustomValidationError, EmailValidationError, MAX, MAX_LENGTH, MIN, MIN_LENGTH, MaxLengthValidationError, MaxValidationError, MinLengthValidationError, MinValidationError, NgValidationError, PATTERN, PatternValidationError, Property, REQUIRED, RequiredValidationError, StandardSchemaValidationError, aggregateProperty, andProperty, apply, applyEach, applyWhen, applyWhenValue, createProperty, customError, disabled, email, emailError, form, hidden, listProperty, max, maxError, maxLength, maxLengthError, maxProperty, min, minError, minLength, minLengthError, minProperty, orProperty, pattern, patternError, property, readonly, reducedProperty, required, requiredError, schema, standardSchemaError, submit, validate, validateAsync, validateHttp, validateStandardSchema, validateTree };
3376
3607
  //# sourceMappingURL=signals.mjs.map