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

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.3
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.3", ngImport: i0, type: Control, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1861
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.0-next.3", 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.3", 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);
@@ -2321,6 +2889,8 @@ function form(...args) {
2321
2889
  * @param schema A schema for an element of the array, or function that binds logic to an
2322
2890
  * element of the array.
2323
2891
  * @template TValue The data type of the item field to apply the schema to.
2892
+ *
2893
+ * @experimental 21.0.0
2324
2894
  */
2325
2895
  function applyEach(path, schema) {
2326
2896
  assertPathIsCurrent(path);
@@ -2344,6 +2914,8 @@ function applyEach(path, schema) {
2344
2914
  * @param path The target path to apply the schema to.
2345
2915
  * @param schema The schema to apply to the property
2346
2916
  * @template TValue The data type of the field to apply the schema to.
2917
+ *
2918
+ * @experimental 21.0.0
2347
2919
  */
2348
2920
  function apply(path, schema) {
2349
2921
  assertPathIsCurrent(path);
@@ -2357,6 +2929,8 @@ function apply(path, schema) {
2357
2929
  * @param logic A `LogicFn<T, boolean>` that returns `true` when the schema should be applied.
2358
2930
  * @param schema The schema to apply to the field when the `logic` function returns `true`.
2359
2931
  * @template TValue The data type of the field to apply the schema to.
2932
+ *
2933
+ * @experimental 21.0.0
2360
2934
  */
2361
2935
  function applyWhen(path, logic, schema) {
2362
2936
  assertPathIsCurrent(path);
@@ -2396,6 +2970,8 @@ function applyWhenValue(path, predicate, schema) {
2396
2970
  * @param action An asynchronous action used to submit the field. The action may return server
2397
2971
  * errors.
2398
2972
  * @template TValue The data type of the field being submitted.
2973
+ *
2974
+ * @experimental 21.0.0
2399
2975
  */
2400
2976
  async function submit(form, action) {
2401
2977
  const node = form();
@@ -2443,6 +3019,8 @@ function setServerErrors(submittedField, errors) {
2443
3019
  * @param fn A **non-reactive** function that sets up reactive logic rules for the form.
2444
3020
  * @returns A schema object that implements the given logic.
2445
3021
  * @template TValue The value type of a `Field` that this schema binds to.
3022
+ *
3023
+ * @experimental 21.0.0
2446
3024
  */
2447
3025
  function schema(fn) {
2448
3026
  return SchemaImpl.create(fn);
@@ -2484,6 +3062,8 @@ function customError(obj) {
2484
3062
  }
2485
3063
  /**
2486
3064
  * A custom error that may contain additional properties
3065
+ *
3066
+ * @experimental 21.0.0
2487
3067
  */
2488
3068
  class CustomValidationError {
2489
3069
  /** Brand the class to avoid Typescript structural matching */
@@ -2503,6 +3083,8 @@ class CustomValidationError {
2503
3083
  /**
2504
3084
  * Internal version of `NgValidationError`, we create this separately so we can change its type on
2505
3085
  * the exported version to a type union of the possible sub-classes.
3086
+ *
3087
+ * @experimental 21.0.0
2506
3088
  */
2507
3089
  class _NgValidationError {
2508
3090
  /** Brand the class to avoid Typescript structural matching */
@@ -2521,12 +3103,16 @@ class _NgValidationError {
2521
3103
  }
2522
3104
  /**
2523
3105
  * An error used to indicate that a required field is empty.
3106
+ *
3107
+ * @experimental 21.0.0
2524
3108
  */
2525
3109
  class RequiredValidationError extends _NgValidationError {
2526
3110
  kind = 'required';
2527
3111
  }
2528
3112
  /**
2529
3113
  * An error used to indicate that a value is lower than the minimum allowed.
3114
+ *
3115
+ * @experimental 21.0.0
2530
3116
  */
2531
3117
  class MinValidationError extends _NgValidationError {
2532
3118
  min;
@@ -2538,6 +3124,8 @@ class MinValidationError extends _NgValidationError {
2538
3124
  }
2539
3125
  /**
2540
3126
  * An error used to indicate that a value is higher than the maximum allowed.
3127
+ *
3128
+ * @experimental 21.0.0
2541
3129
  */
2542
3130
  class MaxValidationError extends _NgValidationError {
2543
3131
  max;
@@ -2549,6 +3137,8 @@ class MaxValidationError extends _NgValidationError {
2549
3137
  }
2550
3138
  /**
2551
3139
  * An error used to indicate that a value is shorter than the minimum allowed length.
3140
+ *
3141
+ * @experimental 21.0.0
2552
3142
  */
2553
3143
  class MinLengthValidationError extends _NgValidationError {
2554
3144
  minLength;
@@ -2560,6 +3150,8 @@ class MinLengthValidationError extends _NgValidationError {
2560
3150
  }
2561
3151
  /**
2562
3152
  * An error used to indicate that a value is longer than the maximum allowed length.
3153
+ *
3154
+ * @experimental 21.0.0
2563
3155
  */
2564
3156
  class MaxLengthValidationError extends _NgValidationError {
2565
3157
  maxLength;
@@ -2571,6 +3163,8 @@ class MaxLengthValidationError extends _NgValidationError {
2571
3163
  }
2572
3164
  /**
2573
3165
  * An error used to indicate that a value does not match the required pattern.
3166
+ *
3167
+ * @experimental 21.0.0
2574
3168
  */
2575
3169
  class PatternValidationError extends _NgValidationError {
2576
3170
  pattern;
@@ -2582,12 +3176,16 @@ class PatternValidationError extends _NgValidationError {
2582
3176
  }
2583
3177
  /**
2584
3178
  * An error used to indicate that a value is not a valid email.
3179
+ *
3180
+ * @experimental 21.0.0
2585
3181
  */
2586
3182
  class EmailValidationError extends _NgValidationError {
2587
3183
  kind = 'email';
2588
3184
  }
2589
3185
  /**
2590
3186
  * An error used to indicate an issue validating against a standard schema.
3187
+ *
3188
+ * @experimental 21.0.0
2591
3189
  */
2592
3190
  class StandardSchemaValidationError extends _NgValidationError {
2593
3191
  issue;
@@ -2618,6 +3216,8 @@ class StandardSchemaValidationError extends _NgValidationError {
2618
3216
  * }
2619
3217
  * }
2620
3218
  * ```
3219
+ *
3220
+ * @experimental 21.0.0
2621
3221
  */
2622
3222
  const NgValidationError = _NgValidationError;
2623
3223
 
@@ -2632,254 +3232,75 @@ function getLengthOrSize(value) {
2632
3232
  *
2633
3233
  * @param opt The option from BaseValidatorConfig.
2634
3234
  * @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
- });
3235
+ * @returns The value for the option.
3236
+ */
3237
+ function getOption(opt, ctx) {
3238
+ return opt instanceof Function ? opt(ctx) : opt;
2779
3239
  }
2780
-
2781
3240
  /**
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)
3241
+ * Checks if the given value is considered empty. Empty values are: null, undefined, '', false, NaN.
2793
3242
  */
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
- });
3243
+ function isEmpty(value) {
3244
+ if (typeof value === 'number') {
3245
+ return isNaN(value);
3246
+ }
3247
+ return value === '' || value === false || value == null;
2815
3248
  }
2816
3249
 
2817
3250
  /**
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.
3251
+ * A regular expression that matches valid e-mail addresses.
2822
3252
  *
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)
3253
+ * At a high level, this regexp matches e-mail addresses of the format `local-part@tld`, where:
3254
+ * - `local-part` consists of one or more of the allowed characters (alphanumeric and some
3255
+ * punctuation symbols).
3256
+ * - `local-part` cannot begin or end with a period (`.`).
3257
+ * - `local-part` cannot be longer than 64 characters.
3258
+ * - `tld` consists of one or more `labels` separated by periods (`.`). For example `localhost` or
3259
+ * `foo.com`.
3260
+ * - A `label` consists of one or more of the allowed characters (alphanumeric, dashes (`-`) and
3261
+ * periods (`.`)).
3262
+ * - A `label` cannot begin or end with a dash (`-`) or a period (`.`).
3263
+ * - A `label` cannot be longer than 63 characters.
3264
+ * - The whole address cannot be longer than 254 characters.
3265
+ *
3266
+ * ## Implementation background
3267
+ *
3268
+ * This regexp was ported over from AngularJS (see there for git history):
3269
+ * https://github.com/angular/angular.js/blob/c133ef836/src/ng/directive/input.js#L27
3270
+ * It is based on the
3271
+ * [WHATWG version](https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address) with
3272
+ * some enhancements to incorporate more RFC rules (such as rules related to domain names and the
3273
+ * lengths of different parts of the address). The main differences from the WHATWG version are:
3274
+ * - Disallow `local-part` to begin or end with a period (`.`).
3275
+ * - Disallow `local-part` length to exceed 64 characters.
3276
+ * - Disallow total address length to exceed 254 characters.
3277
+ *
3278
+ * See [this commit](https://github.com/angular/angular.js/commit/f3f5cf72e) for more details.
2830
3279
  */
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
-
3280
+ 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
3281
  /**
2855
- * Binds a validator to the given path that requires the value to match a specific regex pattern.
3282
+ * Binds a validator to the given path that requires the value to match the standard email format.
2856
3283
  * 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
3284
  *
2859
3285
  * @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
3286
  * @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)`
3287
+ * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.email()`
2863
3288
  * or a function that receives the `FieldContext` and returns custom validation error(s).
2864
3289
  * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
3290
+ *
3291
+ * @experimental 21.0.0
2865
3292
  */
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)());
3293
+ function email(path, config) {
2869
3294
  validate(path, (ctx) => {
2870
3295
  if (isEmpty(ctx.value())) {
2871
3296
  return undefined;
2872
3297
  }
2873
- const pattern = ctx.state.property(PATTERN_MEMO)();
2874
- if (pattern === undefined) {
2875
- return undefined;
2876
- }
2877
- if (!pattern.test(ctx.value())) {
3298
+ if (!EMAIL_REGEXP.test(ctx.value())) {
2878
3299
  if (config?.error) {
2879
3300
  return getOption(config.error, ctx);
2880
3301
  }
2881
3302
  else {
2882
- return patternError(pattern, { message: getOption(config?.message, ctx) });
3303
+ return emailError({ message: getOption(config?.message, ctx) });
2883
3304
  }
2884
3305
  }
2885
3306
  return undefined;
@@ -2887,490 +3308,290 @@ function pattern(path, pattern, config) {
2887
3308
  }
2888
3309
 
2889
3310
  /**
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.
3311
+ * Binds a validator to the given path that requires the value to be less than or equal to the
3312
+ * given `maxValue`.
3313
+ * This function can only be called on number paths.
3314
+ * In addition to binding a validator, this function adds `MAX` property to the field.
2893
3315
  *
2894
3316
  * @param path Path of the field to validate
3317
+ * @param maxValue The maximum value, or a LogicFn that returns the maximum value.
2895
3318
  * @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()`
3319
+ * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.max(maxValue)`
2898
3320
  * 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
3321
  * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
3322
+ *
3323
+ * @experimental 21.0.0
2902
3324
  */
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)());
3325
+ function max(path, maxValue, config) {
3326
+ const MAX_MEMO = property(path, (ctx) => computed(() => (typeof maxValue === 'number' ? maxValue : maxValue(ctx))));
3327
+ aggregateProperty(path, MAX, ({ state }) => state.property(MAX_MEMO)());
2906
3328
  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';
3329
+ if (isEmpty(ctx.value())) {
3330
+ return undefined;
3015
3331
  }
3016
- if (this.field().pending()) {
3017
- return 'PENDING';
3332
+ const max = ctx.state.property(MAX_MEMO)();
3333
+ if (max === undefined || Number.isNaN(max)) {
3334
+ return undefined;
3018
3335
  }
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)();
3336
+ if (ctx.value() > max) {
3337
+ if (config?.error) {
3338
+ return getOption(config.error, ctx);
3339
+ }
3340
+ else {
3341
+ return maxError(max, { message: getOption(config?.message, ctx) });
3342
+ }
3027
3343
  }
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
- }
3344
+ return undefined;
3345
+ });
3034
3346
  }
3035
3347
 
3036
3348
  /**
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).
3349
+ * Binds a validator to the given path that requires the length of the value to be less than or
3350
+ * equal to the given `maxLength`.
3351
+ * This function can only be called on string or array paths.
3352
+ * In addition to binding a validator, this function adds `MAX_LENGTH` property to the field.
3042
3353
  *
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.
3354
+ * @param path Path of the field to validate
3355
+ * @param maxLength The maximum length, or a LogicFn that returns the maximum length.
3356
+ * @param config Optional, allows providing any of the following options:
3357
+ * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.maxLength(maxLength)`
3358
+ * or a function that receives the `FieldContext` and returns custom validation error(s).
3359
+ * @template TValue The type of value stored in the field the logic is bound to.
3360
+ * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
3361
+ *
3362
+ * @experimental 21.0.0
3050
3363
  */
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);
3364
+ function maxLength(path, maxLength, config) {
3365
+ const MAX_LENGTH_MEMO = property(path, (ctx) => computed(() => (typeof maxLength === 'number' ? maxLength : maxLength(ctx))));
3366
+ aggregateProperty(path, MAX_LENGTH, ({ state }) => state.property(MAX_LENGTH_MEMO)());
3367
+ validate(path, (ctx) => {
3368
+ if (isEmpty(ctx.value())) {
3369
+ return undefined;
3119
3370
  }
3120
- else {
3121
- throw new Error(`Unhandled control?`);
3371
+ const maxLength = ctx.state.property(MAX_LENGTH_MEMO)();
3372
+ if (maxLength === undefined) {
3373
+ return undefined;
3122
3374
  }
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;
3375
+ if (getLengthOrSize(ctx.value()) > maxLength) {
3376
+ if (config?.error) {
3377
+ return getOption(config.error, ctx);
3378
+ }
3379
+ else {
3380
+ return maxLengthError(maxLength, { message: getOption(config?.message, ctx) });
3156
3381
  }
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
3382
  }
3221
- else {
3222
- throw new Error(`Unknown custom control subtype`);
3383
+ return undefined;
3384
+ });
3385
+ }
3386
+
3387
+ /**
3388
+ * Binds a validator to the given path that requires the value to be greater than or equal to
3389
+ * the given `minValue`.
3390
+ * This function can only be called on number paths.
3391
+ * In addition to binding a validator, this function adds `MIN` property to the field.
3392
+ *
3393
+ * @param path Path of the field to validate
3394
+ * @param minValue The minimum value, or a LogicFn that returns the minimum value.
3395
+ * @param config Optional, allows providing any of the following options:
3396
+ * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.min(minValue)`
3397
+ * or a function that receives the `FieldContext` and returns custom validation error(s).
3398
+ * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
3399
+ *
3400
+ * @experimental 21.0.0
3401
+ */
3402
+ function min(path, minValue, config) {
3403
+ const MIN_MEMO = property(path, (ctx) => computed(() => (typeof minValue === 'number' ? minValue : minValue(ctx))));
3404
+ aggregateProperty(path, MIN, ({ state }) => state.property(MIN_MEMO)());
3405
+ validate(path, (ctx) => {
3406
+ if (isEmpty(ctx.value())) {
3407
+ return undefined;
3223
3408
  }
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));
3409
+ const min = ctx.state.property(MIN_MEMO)();
3410
+ if (min === undefined || Number.isNaN(min)) {
3411
+ return undefined;
3232
3412
  }
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());
3413
+ if (ctx.value() < min) {
3414
+ if (config?.error) {
3415
+ return getOption(config.error, ctx);
3416
+ }
3417
+ else {
3418
+ return minError(min, { message: getOption(config?.message, ctx) });
3419
+ }
3246
3420
  }
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);
3421
+ return undefined;
3422
+ });
3423
+ }
3424
+
3425
+ /**
3426
+ * Binds a validator to the given path that requires the length of the value to be greater than or
3427
+ * equal to the given `minLength`.
3428
+ * This function can only be called on string or array paths.
3429
+ * In addition to binding a validator, this function adds `MIN_LENGTH` property to the field.
3430
+ *
3431
+ * @param path Path of the field to validate
3432
+ * @param minLength The minimum length, or a LogicFn that returns the minimum length.
3433
+ * @param config Optional, allows providing any of the following options:
3434
+ * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.minLength(minLength)`
3435
+ * or a function that receives the `FieldContext` and returns custom validation error(s).
3436
+ * @template TValue The type of value stored in the field the logic is bound to.
3437
+ * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
3438
+ *
3439
+ * @experimental 21.0.0
3440
+ */
3441
+ function minLength(path, minLength, config) {
3442
+ const MIN_LENGTH_MEMO = property(path, (ctx) => computed(() => (typeof minLength === 'number' ? minLength : minLength(ctx))));
3443
+ aggregateProperty(path, MIN_LENGTH, ({ state }) => state.property(MIN_LENGTH_MEMO)());
3444
+ validate(path, (ctx) => {
3445
+ if (isEmpty(ctx.value())) {
3446
+ return undefined;
3258
3447
  }
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) {
3448
+ const minLength = ctx.state.property(MIN_LENGTH_MEMO)();
3449
+ if (minLength === undefined) {
3269
3450
  return undefined;
3270
3451
  }
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());
3452
+ if (getLengthOrSize(ctx.value()) < minLength) {
3453
+ if (config?.error) {
3454
+ return getOption(config.error, ctx);
3289
3455
  }
3290
3456
  else {
3291
- this.renderer.removeAttribute(element, attribute);
3457
+ return minLengthError(minLength, { message: getOption(config?.message, ctx) });
3292
3458
  }
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, '');
3459
+ }
3460
+ return undefined;
3461
+ });
3462
+ }
3463
+
3464
+ /**
3465
+ * Binds a validator to the given path that requires the value to match a specific regex pattern.
3466
+ * This function can only be called on string paths.
3467
+ * In addition to binding a validator, this function adds `PATTERN` property to the field.
3468
+ *
3469
+ * @param path Path of the field to validate
3470
+ * @param pattern The RegExp pattern to match, or a LogicFn that returns the RegExp pattern.
3471
+ * @param config Optional, allows providing any of the following options:
3472
+ * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.pattern(pattern)`
3473
+ * or a function that receives the `FieldContext` and returns custom validation error(s).
3474
+ * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
3475
+ *
3476
+ * @experimental 21.0.0
3477
+ */
3478
+ function pattern(path, pattern, config) {
3479
+ const PATTERN_MEMO = property(path, (ctx) => computed(() => (pattern instanceof RegExp ? pattern : pattern(ctx))));
3480
+ aggregateProperty(path, PATTERN, ({ state }) => state.property(PATTERN_MEMO)());
3481
+ validate(path, (ctx) => {
3482
+ if (isEmpty(ctx.value())) {
3483
+ return undefined;
3484
+ }
3485
+ const pattern = ctx.state.property(PATTERN_MEMO)();
3486
+ if (pattern === undefined) {
3487
+ return undefined;
3488
+ }
3489
+ if (!pattern.test(ctx.value())) {
3490
+ if (config?.error) {
3491
+ return getOption(config.error, ctx);
3300
3492
  }
3301
3493
  else {
3302
- this.renderer.removeAttribute(element, attribute);
3494
+ return patternError(pattern, { message: getOption(config?.message, ctx) });
3303
3495
  }
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;
3496
+ }
3497
+ return undefined;
3498
+ });
3332
3499
  }
3500
+
3333
3501
  /**
3334
- * Checks whether the given component matches the contract for either FormValueControl or
3335
- * FormCheckboxControl.
3502
+ * Binds a validator to the given path that requires the value to be non-empty.
3503
+ * This function can only be called on any type of path.
3504
+ * In addition to binding a validator, this function adds `REQUIRED` property to the field.
3505
+ *
3506
+ * @param path Path of the field to validate
3507
+ * @param config Optional, allows providing any of the following options:
3508
+ * - `message`: A user-facing message for the error.
3509
+ * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.required()`
3510
+ * or a function that receives the `FieldContext` and returns custom validation error(s).
3511
+ * - `when`: A function that receives the `FieldContext` and returns true if the field is required
3512
+ * @template TValue The type of value stored in the field the logic is bound to.
3513
+ * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
3514
+ *
3515
+ * @experimental 21.0.0
3336
3516
  */
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);
3517
+ function required(path, config) {
3518
+ const REQUIRED_MEMO = property(path, (ctx) => computed(() => (config?.when ? config.when(ctx) : true)));
3519
+ aggregateProperty(path, REQUIRED, ({ state }) => state.property(REQUIRED_MEMO)());
3520
+ validate(path, (ctx) => {
3521
+ if (ctx.state.property(REQUIRED_MEMO)() && isEmpty(ctx.value())) {
3522
+ if (config?.error) {
3523
+ return getOption(config.error, ctx);
3524
+ }
3525
+ else {
3526
+ return requiredError({ message: getOption(config?.message, ctx) });
3527
+ }
3528
+ }
3529
+ return undefined;
3530
+ });
3364
3531
  }
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;
3532
+
3533
+ /**
3534
+ * Validates a field using a `StandardSchemaV1` compatible validator (e.g. a Zod validator).
3535
+ *
3536
+ * See https://github.com/standard-schema/standard-schema for more about standard schema.
3537
+ *
3538
+ * @param path The `FieldPath` to the field to validate.
3539
+ * @param schema The standard schema compatible validator to use for validation.
3540
+ * @template TSchema The type validated by the schema. This may be either the full `TValue` type,
3541
+ * or a partial of it.
3542
+ * @template TValue The type of value stored in the field being validated.
3543
+ *
3544
+ * @experimental 21.0.0
3545
+ */
3546
+ function validateStandardSchema(path, schema) {
3547
+ // We create both a sync and async validator because the standard schema validator can return
3548
+ // either a sync result or a Promise, and we need to handle both cases. The sync validator
3549
+ // handles the sync result, and the async validator handles the Promise.
3550
+ // We memoize the result of the validation function here, so that it is only run once for both
3551
+ // validators, it can then be passed through both sync & async validation.
3552
+ const VALIDATOR_MEMO = property(path, ({ value }) => {
3553
+ return computed(() => schema['~standard'].validate(value()));
3554
+ });
3555
+ validateTree(path, ({ state, fieldOf }) => {
3556
+ // Skip sync validation if the result is a Promise.
3557
+ const result = state.property(VALIDATOR_MEMO)();
3558
+ if (_isPromise(result)) {
3559
+ return [];
3560
+ }
3561
+ return result.issues?.map((issue) => standardIssueToFormTreeError(fieldOf(path), issue)) ?? [];
3562
+ });
3563
+ validateAsync(path, {
3564
+ params: ({ state }) => {
3565
+ // Skip async validation if the result is *not* a Promise.
3566
+ const result = state.property(VALIDATOR_MEMO)();
3567
+ return _isPromise(result) ? result : undefined;
3568
+ },
3569
+ factory: (params) => {
3570
+ return resource({
3571
+ params,
3572
+ loader: async ({ params }) => (await params)?.issues ?? [],
3573
+ });
3574
+ },
3575
+ errors: (issues, { fieldOf }) => {
3576
+ return issues.map((issue) => standardIssueToFormTreeError(fieldOf(path), issue));
3577
+ },
3578
+ });
3369
3579
  }
3370
- /** Checks whether the given object is an output ref. */
3371
- function isOutputRef(value) {
3372
- return value instanceof OutputEmitterRef || value instanceof EventEmitter;
3580
+ /**
3581
+ * Converts a `StandardSchemaV1.Issue` to a `FormTreeError`.
3582
+ *
3583
+ * @param field The root field to which the issue's path is relative.
3584
+ * @param issue The `StandardSchemaV1.Issue` to convert.
3585
+ * @returns A `ValidationError` representing the issue.
3586
+ */
3587
+ function standardIssueToFormTreeError(field, issue) {
3588
+ let target = field;
3589
+ for (const pathPart of issue.path ?? []) {
3590
+ const pathKey = typeof pathPart === 'object' ? pathPart.key : pathPart;
3591
+ target = target[pathKey];
3592
+ }
3593
+ return addDefaultField(standardSchemaError(issue), target);
3373
3594
  }
3374
3595
 
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 };
3596
+ 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
3597
  //# sourceMappingURL=signals.mjs.map