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