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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3597 @@
1
+ /**
2
+ * @license Angular v21.0.0-next.3
3
+ * (c) 2010-2025 Google LLC. https://angular.io/
4
+ * License: MIT
5
+ */
6
+
7
+ import { httpResource } from '@angular/common/http';
8
+ import * as i0 from '@angular/core';
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';
10
+ import { Validators, NG_VALUE_ACCESSOR, NgControl } from '@angular/forms';
11
+ import { SIGNAL } from '@angular/core/primitives/signals';
12
+
13
+ /**
14
+ * A version of `Array.isArray` that handles narrowing of readonly arrays properly.
15
+ */
16
+ function isArray(value) {
17
+ return Array.isArray(value);
18
+ }
19
+ /**
20
+ * Checks if a value is an object.
21
+ */
22
+ function isObject(value) {
23
+ return (typeof value === 'object' || typeof value === 'function') && value != null;
24
+ }
25
+
26
+ /**
27
+ * Perform a reduction over a field's children (if any) and return the result.
28
+ *
29
+ * Optionally, the reduction is short circuited based on the provided `shortCircuit` function.
30
+ */
31
+ function reduceChildren(node, initialValue, fn, shortCircuit) {
32
+ const childrenMap = node.structure.childrenMap();
33
+ if (!childrenMap) {
34
+ return initialValue;
35
+ }
36
+ let value = initialValue;
37
+ for (const child of childrenMap.values()) {
38
+ if (shortCircuit?.(value)) {
39
+ break;
40
+ }
41
+ value = fn(child, value);
42
+ }
43
+ return value;
44
+ }
45
+ /** A shortCircuit function for reduceChildren that short-circuits if the value is false. */
46
+ function shortCircuitFalse(value) {
47
+ return !value;
48
+ }
49
+ /** A shortCircuit function for reduceChildren that short-circuits if the value is true. */
50
+ function shortCircuitTrue(value) {
51
+ return value;
52
+ }
53
+
54
+ /**
55
+ * Helper function taking validation state, and returning own state of the node.
56
+ * @param state
57
+ */
58
+ function calculateValidationSelfStatus(state) {
59
+ if (state.errors().length > 0) {
60
+ return 'invalid';
61
+ }
62
+ if (state.pending()) {
63
+ return 'unknown';
64
+ }
65
+ return 'valid';
66
+ }
67
+ /**
68
+ * The validation state associated with a `FieldNode`.
69
+ *
70
+ * This class collects together various types of errors to represent the full validation state of
71
+ * the field. There are 4 types of errors that need to be combined to determine validity:
72
+ * 1. The synchronous errors produced by the schema logic.
73
+ * 2. The synchronous tree errors produced by the schema logic. Tree errors may apply to a different
74
+ * field than the one that the logic that produced them is bound to. They support targeting the
75
+ * error at an arbitrary descendant field.
76
+ * 3. The asynchronous tree errors produced by the schema logic. These work like synchronous tree
77
+ * errors, except the error list may also contain a special sentinel value indicating that a
78
+ * validator is still running.
79
+ * 4. Server errors are not produced by the schema logic, but instead get imperatively added when a
80
+ * form submit fails with errors.
81
+ */
82
+ class FieldValidationState {
83
+ node;
84
+ constructor(node) {
85
+ this.node = node;
86
+ }
87
+ /**
88
+ * The full set of synchronous tree errors visible to this field. This includes ones that are
89
+ * targeted at a descendant field rather than at this field.
90
+ */
91
+ rawSyncTreeErrors = computed(() => {
92
+ if (this.shouldSkipValidation()) {
93
+ return [];
94
+ }
95
+ return [
96
+ ...this.node.logicNode.logic.syncTreeErrors.compute(this.node.context),
97
+ ...(this.node.structure.parent?.validationState.rawSyncTreeErrors() ?? []),
98
+ ];
99
+ }, ...(ngDevMode ? [{ debugName: "rawSyncTreeErrors" }] : []));
100
+ /**
101
+ * The full set of synchronous errors for this field, including synchronous tree errors and server
102
+ * errors. Server errors are considered "synchronous" because they are imperatively added. From
103
+ * the perspective of the field state they are either there or not, they are never in a pending
104
+ * state.
105
+ */
106
+ syncErrors = computed(() => {
107
+ // Short-circuit running validators if validation doesn't apply to this field.
108
+ if (this.shouldSkipValidation()) {
109
+ return [];
110
+ }
111
+ return [
112
+ ...this.node.logicNode.logic.syncErrors.compute(this.node.context),
113
+ ...this.syncTreeErrors(),
114
+ ...normalizeErrors(this.node.submitState.serverErrors()),
115
+ ];
116
+ }, ...(ngDevMode ? [{ debugName: "syncErrors" }] : []));
117
+ /**
118
+ * Whether the field is considered valid according solely to its synchronous validators.
119
+ * Errors resulting from a previous submit attempt are also considered for this state.
120
+ */
121
+ syncValid = computed(() => {
122
+ // Short-circuit checking children if validation doesn't apply to this field.
123
+ if (this.shouldSkipValidation()) {
124
+ return true;
125
+ }
126
+ return reduceChildren(this.node, this.syncErrors().length === 0, (child, value) => value && child.validationState.syncValid(), shortCircuitFalse);
127
+ }, ...(ngDevMode ? [{ debugName: "syncValid" }] : []));
128
+ /**
129
+ * The synchronous tree errors visible to this field that are specifically targeted at this field
130
+ * rather than a descendant.
131
+ */
132
+ syncTreeErrors = computed(() => this.rawSyncTreeErrors().filter((err) => err.field === this.node.fieldProxy), ...(ngDevMode ? [{ debugName: "syncTreeErrors" }] : []));
133
+ /**
134
+ * The full set of asynchronous tree errors visible to this field. This includes ones that are
135
+ * targeted at a descendant field rather than at this field, as well as sentinel 'pending' values
136
+ * indicating that the validator is still running and an error could still occur.
137
+ */
138
+ rawAsyncErrors = computed(() => {
139
+ // Short-circuit running validators if validation doesn't apply to this field.
140
+ if (this.shouldSkipValidation()) {
141
+ return [];
142
+ }
143
+ return [
144
+ // TODO: add field in `validateAsync` and remove this map
145
+ ...this.node.logicNode.logic.asyncErrors.compute(this.node.context),
146
+ // TODO: does it make sense to filter this to errors in this subtree?
147
+ ...(this.node.structure.parent?.validationState.rawAsyncErrors() ?? []),
148
+ ];
149
+ }, ...(ngDevMode ? [{ debugName: "rawAsyncErrors" }] : []));
150
+ /**
151
+ * The asynchronous tree errors visible to this field that are specifically targeted at this field
152
+ * rather than a descendant. This also includes all 'pending' sentinel values, since those could
153
+ * theoretically result in errors for this field.
154
+ */
155
+ asyncErrors = computed(() => {
156
+ if (this.shouldSkipValidation()) {
157
+ return [];
158
+ }
159
+ return this.rawAsyncErrors().filter((err) => err === 'pending' || err.field === this.node.fieldProxy);
160
+ }, ...(ngDevMode ? [{ debugName: "asyncErrors" }] : []));
161
+ /**
162
+ * The combined set of all errors that currently apply to this field.
163
+ */
164
+ errors = computed(() => [
165
+ ...this.syncErrors(),
166
+ ...this.asyncErrors().filter((err) => err !== 'pending'),
167
+ ], ...(ngDevMode ? [{ debugName: "errors" }] : []));
168
+ errorSummary = computed(() => reduceChildren(this.node, this.errors(), (child, result) => [
169
+ ...result,
170
+ ...child.errorSummary(),
171
+ ]), ...(ngDevMode ? [{ debugName: "errorSummary" }] : []));
172
+ /**
173
+ * Whether this field has any asynchronous validators still pending.
174
+ */
175
+ pending = computed(() => reduceChildren(this.node, this.asyncErrors().includes('pending'), (child, value) => value || child.validationState.asyncErrors().includes('pending')), ...(ngDevMode ? [{ debugName: "pending" }] : []));
176
+ /**
177
+ * The validation status of the field.
178
+ * - The status is 'valid' if neither the field nor any of its children has any errors or pending
179
+ * validators.
180
+ * - The status is 'invalid' if the field or any of its children has an error
181
+ * (regardless of pending validators)
182
+ * - The status is 'unknown' if neither the field nor any of its children has any errors,
183
+ * but the field or any of its children does have a pending validator.
184
+ *
185
+ * A field is considered valid if *all* of the following are true:
186
+ * - It has no errors or pending validators
187
+ * - All of its children are considered valid
188
+ * A field is considered invalid if *any* of the following are true:
189
+ * - It has an error
190
+ * - Any of its children is considered invalid
191
+ * A field is considered to have unknown validity status if it is not valid or invalid.
192
+ */
193
+ status = computed(() => {
194
+ // Short-circuit checking children if validation doesn't apply to this field.
195
+ if (this.shouldSkipValidation()) {
196
+ return 'valid';
197
+ }
198
+ let ownStatus = calculateValidationSelfStatus(this);
199
+ return reduceChildren(this.node, ownStatus, (child, value) => {
200
+ if (value === 'invalid' || child.validationState.status() === 'invalid') {
201
+ return 'invalid';
202
+ }
203
+ else if (value === 'unknown' || child.validationState.status() === 'unknown') {
204
+ return 'unknown';
205
+ }
206
+ return 'valid';
207
+ }, (v) => v === 'invalid');
208
+ }, ...(ngDevMode ? [{ debugName: "status" }] : []));
209
+ /**
210
+ * Whether the field is considered valid.
211
+ *
212
+ * A field is considered valid if *all* of the following are true:
213
+ * - It has no errors or pending validators
214
+ * - All of its children are considered valid
215
+ *
216
+ * Note: `!valid()` is *not* the same as `invalid()`. Both `valid()` and `invalid()` can be false
217
+ * if there are currently no errors, but validators are still pending.
218
+ */
219
+ valid = computed(() => this.status() === 'valid', ...(ngDevMode ? [{ debugName: "valid" }] : []));
220
+ /**
221
+ * Whether the field is considered invalid.
222
+ *
223
+ * A field is considered invalid if *any* of the following are true:
224
+ * - It has an error
225
+ * - Any of its children is considered invalid
226
+ *
227
+ * Note: `!invalid()` is *not* the same as `valid()`. Both `valid()` and `invalid()` can be false
228
+ * if there are currently no errors, but validators are still pending.
229
+ */
230
+ invalid = computed(() => this.status() === 'invalid', ...(ngDevMode ? [{ debugName: "invalid" }] : []));
231
+ /**
232
+ * Indicates whether validation should be skipped for this field because it is hidden, disabled,
233
+ * or readonly.
234
+ */
235
+ shouldSkipValidation = computed(() => this.node.hidden() || this.node.disabled() || this.node.readonly(), ...(ngDevMode ? [{ debugName: "shouldSkipValidation" }] : []));
236
+ }
237
+ /** Normalizes a validation result to a list of validation errors. */
238
+ function normalizeErrors(error) {
239
+ if (error === undefined) {
240
+ return [];
241
+ }
242
+ if (isArray(error)) {
243
+ return error;
244
+ }
245
+ return [error];
246
+ }
247
+ function addDefaultField(errors, field) {
248
+ if (isArray(errors)) {
249
+ for (const error of errors) {
250
+ error.field ??= field;
251
+ }
252
+ }
253
+ else if (errors) {
254
+ errors.field ??= field;
255
+ }
256
+ return errors;
257
+ }
258
+
259
+ /**
260
+ * Special key which is used to represent a dynamic logic property in a `FieldPathNode` path.
261
+ * This property is used to represent logic that applies to every element of some dynamic form data
262
+ * (i.e. an array).
263
+ *
264
+ * For example, a rule like `applyEach(p.myArray, () => { ... })` will add logic to the `DYNAMIC`
265
+ * property of `p.myArray`.
266
+ */
267
+ const DYNAMIC = Symbol();
268
+ /** Represents a result that should be ignored because its predicate indicates it is not active. */
269
+ const IGNORED = Symbol();
270
+ /**
271
+ * Base class for all logic. It is responsible for combining the results from multiple individual
272
+ * logic functions registered in the schema, and using them to derive the value for some associated
273
+ * piece of field state.
274
+ */
275
+ class AbstractLogic {
276
+ predicates;
277
+ /** The set of logic functions that contribute to the value of the associated state. */
278
+ fns = [];
279
+ constructor(
280
+ /**
281
+ * A list of predicates that conditionally enable all logic in this logic instance.
282
+ * The logic is only enabled when *all* of the predicates evaluate to true.
283
+ */
284
+ predicates) {
285
+ this.predicates = predicates;
286
+ }
287
+ /** Registers a logic function with this logic instance. */
288
+ push(logicFn) {
289
+ this.fns.push(wrapWithPredicates(this.predicates, logicFn));
290
+ }
291
+ /**
292
+ * Merges in the logic from another logic instance, subject to the predicates of both the other
293
+ * instance and this instance.
294
+ */
295
+ mergeIn(other) {
296
+ const fns = this.predicates
297
+ ? other.fns.map((fn) => wrapWithPredicates(this.predicates, fn))
298
+ : other.fns;
299
+ this.fns.push(...fns);
300
+ }
301
+ }
302
+ /** Logic that combines its individual logic function results with logical OR. */
303
+ class BooleanOrLogic extends AbstractLogic {
304
+ get defaultValue() {
305
+ return false;
306
+ }
307
+ compute(arg) {
308
+ return this.fns.some((f) => {
309
+ const result = f(arg);
310
+ return result && result !== IGNORED;
311
+ });
312
+ }
313
+ }
314
+ /**
315
+ * Logic that combines its individual logic function results by aggregating them in an array.
316
+ * Depending on its `ignore` function it may ignore certain values, omitting them from the array.
317
+ */
318
+ class ArrayMergeIgnoreLogic extends AbstractLogic {
319
+ ignore;
320
+ /** Creates an instance of this class that ignores `null` values. */
321
+ static ignoreNull(predicates) {
322
+ return new ArrayMergeIgnoreLogic(predicates, (e) => e === null);
323
+ }
324
+ constructor(predicates, ignore) {
325
+ super(predicates);
326
+ this.ignore = ignore;
327
+ }
328
+ get defaultValue() {
329
+ return [];
330
+ }
331
+ compute(arg) {
332
+ return this.fns.reduce((prev, f) => {
333
+ const value = f(arg);
334
+ if (value === undefined || value === IGNORED) {
335
+ return prev;
336
+ }
337
+ else if (isArray(value)) {
338
+ return [...prev, ...(this.ignore ? value.filter((e) => !this.ignore(e)) : value)];
339
+ }
340
+ else {
341
+ if (this.ignore && this.ignore(value)) {
342
+ return prev;
343
+ }
344
+ return [...prev, value];
345
+ }
346
+ }, []);
347
+ }
348
+ }
349
+ /** Logic that combines its individual logic function results by aggregating them in an array. */
350
+ class ArrayMergeLogic extends ArrayMergeIgnoreLogic {
351
+ constructor(predicates) {
352
+ super(predicates, undefined);
353
+ }
354
+ }
355
+ /** Logic that combines an AggregateProperty according to the property's own reduce function. */
356
+ class AggregatePropertyMergeLogic extends AbstractLogic {
357
+ key;
358
+ get defaultValue() {
359
+ return this.key.getInitial();
360
+ }
361
+ constructor(predicates, key) {
362
+ super(predicates);
363
+ this.key = key;
364
+ }
365
+ compute(ctx) {
366
+ if (this.fns.length === 0) {
367
+ return this.key.getInitial();
368
+ }
369
+ let acc = this.key.getInitial();
370
+ for (let i = 0; i < this.fns.length; i++) {
371
+ const item = this.fns[i](ctx);
372
+ if (item !== IGNORED) {
373
+ acc = this.key.reduce(acc, item);
374
+ }
375
+ }
376
+ return acc;
377
+ }
378
+ }
379
+ /**
380
+ * Wraps a logic function such that it returns the special `IGNORED` sentinel value if any of the
381
+ * given predicates evaluates to false.
382
+ *
383
+ * @param predicates A list of bound predicates to apply to the logic function
384
+ * @param logicFn The logic function to wrap
385
+ * @returns A wrapped version of the logic function that may return `IGNORED`.
386
+ */
387
+ function wrapWithPredicates(predicates, logicFn) {
388
+ if (predicates.length === 0) {
389
+ return logicFn;
390
+ }
391
+ return (arg) => {
392
+ for (const predicate of predicates) {
393
+ let predicateField = arg.stateOf(predicate.path);
394
+ // Check the depth of the current field vs the depth this predicate is supposed to be
395
+ // evaluated at. If necessary, walk up the field tree to grab the correct context field.
396
+ // We can check the pathKeys as an untracked read since we know the structure of our fields
397
+ // doesn't change.
398
+ const depthDiff = untracked(predicateField.structure.pathKeys).length - predicate.depth;
399
+ for (let i = 0; i < depthDiff; i++) {
400
+ predicateField = predicateField.structure.parent;
401
+ }
402
+ // If any of the predicates don't match, don't actually run the logic function, just return
403
+ // the default value.
404
+ if (!predicate.fn(predicateField.context)) {
405
+ return IGNORED;
406
+ }
407
+ }
408
+ return logicFn(arg);
409
+ };
410
+ }
411
+ /**
412
+ * Container for all the different types of logic that can be applied to a field
413
+ * (disabled, hidden, errors, etc.)
414
+ */
415
+ class LogicContainer {
416
+ predicates;
417
+ /** Logic that determines if the field is hidden. */
418
+ hidden;
419
+ /** Logic that determines reasons for the field being disabled. */
420
+ disabledReasons;
421
+ /** Logic that determines if the field is read-only. */
422
+ readonly;
423
+ /** Logic that produces synchronous validation errors for the field. */
424
+ syncErrors;
425
+ /** Logic that produces synchronous validation errors for the field's subtree. */
426
+ syncTreeErrors;
427
+ /** Logic that produces asynchronous validation results (errors or 'pending'). */
428
+ asyncErrors;
429
+ /** A map of aggregate properties to the `AbstractLogic` instances that compute their values. */
430
+ aggregateProperties = new Map();
431
+ /** A map of property keys to the factory functions that create their values. */
432
+ propertyFactories = new Map();
433
+ /**
434
+ * Constructs a new `Logic` container.
435
+ * @param predicates An array of predicates that must all be true for the logic
436
+ * functions within this container to be active.
437
+ */
438
+ constructor(predicates) {
439
+ this.predicates = predicates;
440
+ this.hidden = new BooleanOrLogic(predicates);
441
+ this.disabledReasons = new ArrayMergeLogic(predicates);
442
+ this.readonly = new BooleanOrLogic(predicates);
443
+ this.syncErrors = ArrayMergeIgnoreLogic.ignoreNull(predicates);
444
+ this.syncTreeErrors = ArrayMergeIgnoreLogic.ignoreNull(predicates);
445
+ this.asyncErrors = ArrayMergeIgnoreLogic.ignoreNull(predicates);
446
+ }
447
+ /** Checks whether there is logic for the given aggregate property. */
448
+ hasAggregateProperty(prop) {
449
+ return this.aggregateProperties.has(prop);
450
+ }
451
+ /**
452
+ * Gets an iterable of [aggregate property, logic function] pairs.
453
+ * @returns An iterable of aggregate property entries.
454
+ */
455
+ getAggregatePropertyEntries() {
456
+ return this.aggregateProperties.entries();
457
+ }
458
+ /**
459
+ * Gets an iterable of [property, value factory function] pairs.
460
+ * @returns An iterable of property factory entries.
461
+ */
462
+ getPropertyFactoryEntries() {
463
+ return this.propertyFactories.entries();
464
+ }
465
+ /**
466
+ * Retrieves or creates the `AbstractLogic` for a given aggregate property.
467
+ * @param prop The `AggregateProperty` for which to get the logic.
468
+ * @returns The `AbstractLogic` associated with the key.
469
+ */
470
+ getAggregateProperty(prop) {
471
+ if (!this.aggregateProperties.has(prop)) {
472
+ this.aggregateProperties.set(prop, new AggregatePropertyMergeLogic(this.predicates, prop));
473
+ }
474
+ return this.aggregateProperties.get(prop);
475
+ }
476
+ /**
477
+ * Adds a factory function for a given property.
478
+ * @param prop The `Property` to associate the factory with.
479
+ * @param factory The factory function.
480
+ * @throws If a factory is already defined for the given key.
481
+ */
482
+ addPropertyFactory(prop, factory) {
483
+ if (this.propertyFactories.has(prop)) {
484
+ // TODO: name of the property?
485
+ throw new Error(`Can't define value twice for the same Property`);
486
+ }
487
+ this.propertyFactories.set(prop, factory);
488
+ }
489
+ /**
490
+ * Merges logic from another `Logic` instance into this one.
491
+ * @param other The `Logic` instance to merge from.
492
+ */
493
+ mergeIn(other) {
494
+ this.hidden.mergeIn(other.hidden);
495
+ this.disabledReasons.mergeIn(other.disabledReasons);
496
+ this.readonly.mergeIn(other.readonly);
497
+ this.syncErrors.mergeIn(other.syncErrors);
498
+ this.syncTreeErrors.mergeIn(other.syncTreeErrors);
499
+ this.asyncErrors.mergeIn(other.asyncErrors);
500
+ for (const [prop, propertyLogic] of other.getAggregatePropertyEntries()) {
501
+ this.getAggregateProperty(prop).mergeIn(propertyLogic);
502
+ }
503
+ for (const [prop, propertyFactory] of other.getPropertyFactoryEntries()) {
504
+ this.addPropertyFactory(prop, propertyFactory);
505
+ }
506
+ }
507
+ }
508
+
509
+ let boundPathDepth = 0;
510
+ /**
511
+ * The depth of the current path when evaluating a logic function.
512
+ * Do not set this directly, it is a context variable managed by `setBoundPathDepthForResolution`.
513
+ */
514
+ function getBoundPathDepth() {
515
+ return boundPathDepth;
516
+ }
517
+ /**
518
+ * Sets the bound path depth for the duration of the given logic function.
519
+ * This is used to ensure that the field resolution algorithm walks far enough up the field tree to
520
+ * reach the point where the root of the path we're bound to is applied. This normally isn't a big
521
+ * concern, but matters when we're dealing with recursive structures.
522
+ *
523
+ * Consider this example:
524
+ *
525
+ * ```
526
+ * const s = schema(p => {
527
+ * disabled(p.next, ({valueOf}) => valueOf(p.data));
528
+ * apply(p.next, s);
529
+ * });
530
+ * ```
531
+ *
532
+ * Here we need to know that the `disabled` logic was bound to a path of depth 1. Otherwise we'd
533
+ * attempt to resolve `p.data` in the context of the field corresponding to `p.next`.
534
+ * The resolution algorithm would start with the field for `p.next` and see that it *does* contain
535
+ * the logic for `s` (due to the fact that its recursively applied.) It would then decide not to
536
+ * walk up the field tree at all, and to immediately start walking down the keys for the target path
537
+ * `p.data`, leading it to grab the field corresponding to `p.next.data`.
538
+ *
539
+ * We avoid the problem described above by keeping track of the depth (relative to Schema root) of
540
+ * the path we were bound to. We then require the resolution algorithm to walk at least that far up
541
+ * the tree before finding a node that contains the logic for `s`.
542
+ *
543
+ * @param fn A logic function that is bound to a particular path
544
+ * @param depth The depth in the field tree of the field the logic is bound to
545
+ * @returns A version of the logic function that is aware of its depth.
546
+ */
547
+ function setBoundPathDepthForResolution(fn, depth) {
548
+ return (...args) => {
549
+ try {
550
+ boundPathDepth = depth;
551
+ return fn(...args);
552
+ }
553
+ finally {
554
+ boundPathDepth = 0;
555
+ }
556
+ };
557
+ }
558
+
559
+ /**
560
+ * Abstract base class for building a `LogicNode`.
561
+ * This class defines the interface for adding various logic rules (e.g., hidden, disabled)
562
+ * and data factories to a node in the logic tree.
563
+ * LogicNodeBuilders are 1:1 with nodes in the Schema tree.
564
+ */
565
+ class AbstractLogicNodeBuilder {
566
+ depth;
567
+ constructor(
568
+ /** The depth of this node in the schema tree. */
569
+ depth) {
570
+ this.depth = depth;
571
+ }
572
+ /**
573
+ * Builds the `LogicNode` from the accumulated rules and child builders.
574
+ * @returns The constructed `LogicNode`.
575
+ */
576
+ build() {
577
+ return new LeafLogicNode(this, [], 0);
578
+ }
579
+ }
580
+ /**
581
+ * A builder for `LogicNode`. Used to add logic to the final `LogicNode` tree.
582
+ * This builder supports merging multiple sources of logic, potentially with predicates,
583
+ * preserving the order of rule application.
584
+ */
585
+ class LogicNodeBuilder extends AbstractLogicNodeBuilder {
586
+ constructor(depth) {
587
+ super(depth);
588
+ }
589
+ /**
590
+ * The current `NonMergeableLogicNodeBuilder` being used to add rules directly to this
591
+ * `LogicNodeBuilder`. Do not use this directly, call `getCurrent()` which will create a current
592
+ * builder if there is none.
593
+ */
594
+ current;
595
+ /**
596
+ * Stores all builders that contribute to this node, along with any predicates
597
+ * that gate their application.
598
+ */
599
+ all = [];
600
+ addHiddenRule(logic) {
601
+ this.getCurrent().addHiddenRule(logic);
602
+ }
603
+ addDisabledReasonRule(logic) {
604
+ this.getCurrent().addDisabledReasonRule(logic);
605
+ }
606
+ addReadonlyRule(logic) {
607
+ this.getCurrent().addReadonlyRule(logic);
608
+ }
609
+ addSyncErrorRule(logic) {
610
+ this.getCurrent().addSyncErrorRule(logic);
611
+ }
612
+ addSyncTreeErrorRule(logic) {
613
+ this.getCurrent().addSyncTreeErrorRule(logic);
614
+ }
615
+ addAsyncErrorRule(logic) {
616
+ this.getCurrent().addAsyncErrorRule(logic);
617
+ }
618
+ addAggregatePropertyRule(key, logic) {
619
+ this.getCurrent().addAggregatePropertyRule(key, logic);
620
+ }
621
+ addPropertyFactory(key, factory) {
622
+ this.getCurrent().addPropertyFactory(key, factory);
623
+ }
624
+ getChild(key) {
625
+ return this.getCurrent().getChild(key);
626
+ }
627
+ hasLogic(builder) {
628
+ if (this === builder) {
629
+ return true;
630
+ }
631
+ return this.all.some(({ builder: subBuilder }) => subBuilder.hasLogic(builder));
632
+ }
633
+ /**
634
+ * Merges logic from another `LogicNodeBuilder` into this one.
635
+ * If a `predicate` is provided, all logic from the `other` builder will only apply
636
+ * when the predicate evaluates to true.
637
+ * @param other The `LogicNodeBuilder` to merge in.
638
+ * @param predicate An optional predicate to gate the merged logic.
639
+ */
640
+ mergeIn(other, predicate) {
641
+ // Add the other builder to our collection, we'll defer the actual merging of the logic until
642
+ // the logic node is requested to be created. In order to preserve the original ordering of the
643
+ // rules, we close off the current builder to any further edits. If additional logic is added,
644
+ // a new current builder will be created to capture it.
645
+ if (predicate) {
646
+ this.all.push({
647
+ builder: other,
648
+ predicate: {
649
+ fn: setBoundPathDepthForResolution(predicate.fn, this.depth),
650
+ path: predicate.path,
651
+ },
652
+ });
653
+ }
654
+ else {
655
+ this.all.push({ builder: other });
656
+ }
657
+ this.current = undefined;
658
+ }
659
+ /**
660
+ * Gets the current `NonMergeableLogicNodeBuilder` for adding rules directly to this
661
+ * `LogicNodeBuilder`. If no current builder exists, a new one is created.
662
+ * The current builder is cleared whenever `mergeIn` is called to preserve the order
663
+ * of rules when merging separate builder trees.
664
+ * @returns The current `NonMergeableLogicNodeBuilder`.
665
+ */
666
+ getCurrent() {
667
+ if (this.current === undefined) {
668
+ this.current = new NonMergeableLogicNodeBuilder(this.depth);
669
+ this.all.push({ builder: this.current });
670
+ }
671
+ return this.current;
672
+ }
673
+ /**
674
+ * Creates a new root `LogicNodeBuilder`.
675
+ * @returns A new instance of `LogicNodeBuilder`.
676
+ */
677
+ static newRoot() {
678
+ return new LogicNodeBuilder(0);
679
+ }
680
+ }
681
+ /**
682
+ * A type of `AbstractLogicNodeBuilder` used internally by the `LogicNodeBuilder` to record "pure"
683
+ * chunks of logic that do not require merging in other builders.
684
+ */
685
+ class NonMergeableLogicNodeBuilder extends AbstractLogicNodeBuilder {
686
+ /** The collection of logic rules directly added to this builder. */
687
+ logic = new LogicContainer([]);
688
+ /**
689
+ * A map of child property keys to their corresponding `LogicNodeBuilder` instances.
690
+ * This allows for building a tree of logic.
691
+ */
692
+ children = new Map();
693
+ constructor(depth) {
694
+ super(depth);
695
+ }
696
+ addHiddenRule(logic) {
697
+ this.logic.hidden.push(setBoundPathDepthForResolution(logic, this.depth));
698
+ }
699
+ addDisabledReasonRule(logic) {
700
+ this.logic.disabledReasons.push(setBoundPathDepthForResolution(logic, this.depth));
701
+ }
702
+ addReadonlyRule(logic) {
703
+ this.logic.readonly.push(setBoundPathDepthForResolution(logic, this.depth));
704
+ }
705
+ addSyncErrorRule(logic) {
706
+ this.logic.syncErrors.push(setBoundPathDepthForResolution(logic, this.depth));
707
+ }
708
+ addSyncTreeErrorRule(logic) {
709
+ this.logic.syncTreeErrors.push(setBoundPathDepthForResolution(logic, this.depth));
710
+ }
711
+ addAsyncErrorRule(logic) {
712
+ this.logic.asyncErrors.push(setBoundPathDepthForResolution(logic, this.depth));
713
+ }
714
+ addAggregatePropertyRule(key, logic) {
715
+ this.logic.getAggregateProperty(key).push(setBoundPathDepthForResolution(logic, this.depth));
716
+ }
717
+ addPropertyFactory(key, factory) {
718
+ this.logic.addPropertyFactory(key, setBoundPathDepthForResolution(factory, this.depth));
719
+ }
720
+ getChild(key) {
721
+ if (!this.children.has(key)) {
722
+ this.children.set(key, new LogicNodeBuilder(this.depth + 1));
723
+ }
724
+ return this.children.get(key);
725
+ }
726
+ hasLogic(builder) {
727
+ return this === builder;
728
+ }
729
+ }
730
+ /**
731
+ * A tree structure of `Logic` corresponding to a tree of fields.
732
+ * This implementation represents a leaf in the sense that its logic is derived
733
+ * from a single builder.
734
+ */
735
+ class LeafLogicNode {
736
+ builder;
737
+ predicates;
738
+ depth;
739
+ /** The computed logic for this node. */
740
+ logic;
741
+ /**
742
+ * Constructs a `LeafLogicNode`.
743
+ * @param builder The `AbstractLogicNodeBuilder` from which to derive the logic.
744
+ * If undefined, an empty `Logic` instance is created.
745
+ * @param predicates An array of predicates that gate the logic from the builder.
746
+ */
747
+ constructor(builder, predicates,
748
+ /** The depth of this node in the field tree. */
749
+ depth) {
750
+ this.builder = builder;
751
+ this.predicates = predicates;
752
+ this.depth = depth;
753
+ this.logic = builder ? createLogic(builder, predicates, depth) : new LogicContainer([]);
754
+ }
755
+ // TODO: cache here, or just rely on the user of this API to do caching?
756
+ /**
757
+ * Retrieves the `LogicNode` for a child identified by the given property key.
758
+ * @param key The property key of the child.
759
+ * @returns The `LogicNode` for the specified child.
760
+ */
761
+ getChild(key) {
762
+ // The logic for a particular child may be spread across multiple builders. We lazily combine
763
+ // this logic at the time the child logic node is requested to be created.
764
+ const childBuilders = this.builder ? getAllChildBuilders(this.builder, key) : [];
765
+ if (childBuilders.length === 0) {
766
+ return new LeafLogicNode(undefined, [], this.depth + 1);
767
+ }
768
+ else if (childBuilders.length === 1) {
769
+ const { builder, predicates } = childBuilders[0];
770
+ return new LeafLogicNode(builder, [...this.predicates, ...predicates.map((p) => bindLevel(p, this.depth))], this.depth + 1);
771
+ }
772
+ else {
773
+ const builtNodes = childBuilders.map(({ builder, predicates }) => new LeafLogicNode(builder, [...this.predicates, ...predicates.map((p) => bindLevel(p, this.depth))], this.depth + 1));
774
+ return new CompositeLogicNode(builtNodes);
775
+ }
776
+ }
777
+ /**
778
+ * Checks whether the logic from a particular `AbstractLogicNodeBuilder` has been merged into this
779
+ * node.
780
+ * @param builder The builder to check for.
781
+ * @returns True if the builder has been merged, false otherwise.
782
+ */
783
+ hasLogic(builder) {
784
+ return this.builder?.hasLogic(builder) ?? false;
785
+ }
786
+ }
787
+ /**
788
+ * A `LogicNode` that represents the composition of multiple `LogicNode` instances.
789
+ * This is used when logic for a particular path is contributed by several distinct
790
+ * builder branches that need to be merged.
791
+ */
792
+ class CompositeLogicNode {
793
+ all;
794
+ /** The merged logic from all composed nodes. */
795
+ logic;
796
+ /**
797
+ * Constructs a `CompositeLogicNode`.
798
+ * @param all An array of `LogicNode` instances to compose.
799
+ */
800
+ constructor(all) {
801
+ this.all = all;
802
+ this.logic = new LogicContainer([]);
803
+ for (const node of all) {
804
+ this.logic.mergeIn(node.logic);
805
+ }
806
+ }
807
+ /**
808
+ * Retrieves the child `LogicNode` by composing the results of `getChild` from all
809
+ * underlying `LogicNode` instances.
810
+ * @param key The property key of the child.
811
+ * @returns A `CompositeLogicNode` representing the composed child.
812
+ */
813
+ getChild(key) {
814
+ return new CompositeLogicNode(this.all.flatMap((child) => child.getChild(key)));
815
+ }
816
+ /**
817
+ * Checks whether the logic from a particular `AbstractLogicNodeBuilder` has been merged into this
818
+ * node.
819
+ * @param builder The builder to check for.
820
+ * @returns True if the builder has been merged, false otherwise.
821
+ */
822
+ hasLogic(builder) {
823
+ return this.all.some((node) => node.hasLogic(builder));
824
+ }
825
+ }
826
+ /**
827
+ * Gets all of the builders that contribute logic to the given child of the parent builder.
828
+ * This function recursively traverses the builder hierarchy.
829
+ * @param builder The parent `AbstractLogicNodeBuilder`.
830
+ * @param key The property key of the child.
831
+ * @returns An array of objects, each containing a `LogicNodeBuilder` for the child and any associated predicates.
832
+ */
833
+ function getAllChildBuilders(builder, key) {
834
+ if (builder instanceof LogicNodeBuilder) {
835
+ return builder.all.flatMap(({ builder, predicate }) => {
836
+ const children = getAllChildBuilders(builder, key);
837
+ if (predicate) {
838
+ return children.map(({ builder, predicates }) => ({
839
+ builder,
840
+ predicates: [...predicates, predicate],
841
+ }));
842
+ }
843
+ return children;
844
+ });
845
+ }
846
+ else if (builder instanceof NonMergeableLogicNodeBuilder) {
847
+ if (builder.children.has(key)) {
848
+ return [{ builder: builder.children.get(key), predicates: [] }];
849
+ }
850
+ }
851
+ else {
852
+ throw new Error('Unknown LogicNodeBuilder type');
853
+ }
854
+ return [];
855
+ }
856
+ /**
857
+ * Creates the full `Logic` for a given builder.
858
+ * This function handles different types of builders (`LogicNodeBuilder`, `NonMergeableLogicNodeBuilder`)
859
+ * and applies the provided predicates.
860
+ * @param builder The `AbstractLogicNodeBuilder` to process.
861
+ * @param predicates Predicates to apply to the logic derived from the builder.
862
+ * @param depth The depth in the field tree of the field which this logic applies to.
863
+ * @returns The `Logic` instance.
864
+ */
865
+ function createLogic(builder, predicates, depth) {
866
+ const logic = new LogicContainer(predicates);
867
+ if (builder instanceof LogicNodeBuilder) {
868
+ const builtNodes = builder.all.map(({ builder, predicate }) => new LeafLogicNode(builder, predicate ? [...predicates, bindLevel(predicate, depth)] : predicates, depth));
869
+ for (const node of builtNodes) {
870
+ logic.mergeIn(node.logic);
871
+ }
872
+ }
873
+ else if (builder instanceof NonMergeableLogicNodeBuilder) {
874
+ logic.mergeIn(builder.logic);
875
+ }
876
+ else {
877
+ throw new Error('Unknown LogicNodeBuilder type');
878
+ }
879
+ return logic;
880
+ }
881
+ /**
882
+ * Create a bound version of the given predicate to a specific depth in the field tree.
883
+ * This allows us to unambiguously know which `FieldContext` the predicate function should receive.
884
+ *
885
+ * This is of particular concern when a schema is applied recursively to itself. Since the schema is
886
+ * only compiled once, each nested application adds the same predicate instance. We differentiate
887
+ * these by recording the depth of the field they're bound to.
888
+ *
889
+ * @param predicate The unbound predicate
890
+ * @param depth The depth of the field the predicate is bound to
891
+ * @returns A bound predicate
892
+ */
893
+ function bindLevel(predicate, depth) {
894
+ return { ...predicate, depth: depth };
895
+ }
896
+
897
+ /**
898
+ * Special key which is used to retrieve the `FieldPathNode` instance from its `FieldPath` proxy wrapper.
899
+ */
900
+ const PATH = Symbol('PATH');
901
+ /**
902
+ * A path in the schema on which logic is stored so that it can be added to the corresponding field
903
+ * when the field is created.
904
+ */
905
+ class FieldPathNode {
906
+ keys;
907
+ logic;
908
+ /** The root path node from which this path node is descended. */
909
+ root;
910
+ /**
911
+ * A map containing all child path nodes that have been created on this path.
912
+ * Child path nodes are created automatically on first access if they do not exist already.
913
+ */
914
+ children = new Map();
915
+ /**
916
+ * A proxy that wraps the path node, allowing navigation to its child paths via property access.
917
+ */
918
+ fieldPathProxy = new Proxy(this, FIELD_PATH_PROXY_HANDLER);
919
+ constructor(
920
+ /** The property keys used to navigate from the root path to this path. */
921
+ keys,
922
+ /** The logic builder used to accumulate logic on this path node. */
923
+ logic, root) {
924
+ this.keys = keys;
925
+ this.logic = logic;
926
+ this.root = root ?? this;
927
+ }
928
+ /**
929
+ * Gets the special path node containing the per-element logic that applies to *all* children paths.
930
+ */
931
+ get element() {
932
+ return this.getChild(DYNAMIC);
933
+ }
934
+ /**
935
+ * Gets the path node for the given child property key.
936
+ * Child paths are created automatically on first access if they do not exist already.
937
+ */
938
+ getChild(key) {
939
+ if (!this.children.has(key)) {
940
+ this.children.set(key, new FieldPathNode([...this.keys, key], this.logic.getChild(key), this.root));
941
+ }
942
+ return this.children.get(key);
943
+ }
944
+ /**
945
+ * Merges in logic from another schema to this one.
946
+ * @param other The other schema to merge in the logic from
947
+ * @param predicate A predicate indicating when the merged in logic should be active.
948
+ */
949
+ mergeIn(other, predicate) {
950
+ const path = other.compile();
951
+ this.logic.mergeIn(path.logic, predicate);
952
+ }
953
+ /** Extracts the underlying path node from the given path proxy. */
954
+ static unwrapFieldPath(formPath) {
955
+ return formPath[PATH];
956
+ }
957
+ /** Creates a new root path node to be passed in to a schema function. */
958
+ static newRoot() {
959
+ return new FieldPathNode([], LogicNodeBuilder.newRoot(), undefined);
960
+ }
961
+ }
962
+ /** Proxy handler which implements `FieldPath` on top of a `FieldPathNode`. */
963
+ const FIELD_PATH_PROXY_HANDLER = {
964
+ get(node, property) {
965
+ if (property === PATH) {
966
+ return node;
967
+ }
968
+ return node.getChild(property).fieldPathProxy;
969
+ },
970
+ };
971
+
972
+ /**
973
+ * Keeps track of the path node for the schema function that is currently being compiled. This is
974
+ * used to detect erroneous references to a path node outside of the context of its schema function.
975
+ * Do not set this directly, it is a context variable managed by `SchemaImpl.compile`.
976
+ */
977
+ let currentCompilingNode = undefined;
978
+ /**
979
+ * A cache of all schemas compiled under the current root compilation. This is used to avoid doing
980
+ * extra work when compiling a schema that reuses references to the same sub-schema. For example:
981
+ *
982
+ * ```
983
+ * const sub = schema(p => ...);
984
+ * const s = schema(p => {
985
+ * apply(p.a, sub);
986
+ * apply(p.b, sub);
987
+ * });
988
+ * ```
989
+ *
990
+ * This also ensures that we don't go into an infinite loop when compiling a schema that references
991
+ * itself.
992
+ *
993
+ * Do not directly add or remove entries from this map, it is a context variable managed by
994
+ * `SchemaImpl.compile` and `SchemaImpl.rootCompile`.
995
+ */
996
+ const compiledSchemas = new Map();
997
+ /**
998
+ * Implements the `Schema` concept.
999
+ */
1000
+ class SchemaImpl {
1001
+ schemaFn;
1002
+ constructor(schemaFn) {
1003
+ this.schemaFn = schemaFn;
1004
+ }
1005
+ /**
1006
+ * Compiles this schema within the current root compilation context. If the schema was previously
1007
+ * compiled within this context, we reuse the cached FieldPathNode, otherwise we create a new one
1008
+ * and cache it in the compilation context.
1009
+ */
1010
+ compile() {
1011
+ if (compiledSchemas.has(this)) {
1012
+ return compiledSchemas.get(this);
1013
+ }
1014
+ const path = FieldPathNode.newRoot();
1015
+ compiledSchemas.set(this, path);
1016
+ let prevCompilingNode = currentCompilingNode;
1017
+ try {
1018
+ currentCompilingNode = path;
1019
+ this.schemaFn(path.fieldPathProxy);
1020
+ }
1021
+ finally {
1022
+ // Use a try/finally to ensure we restore the previous root upon completion,
1023
+ // even if there are errors while compiling the schema.
1024
+ currentCompilingNode = prevCompilingNode;
1025
+ }
1026
+ return path;
1027
+ }
1028
+ /**
1029
+ * Creates a SchemaImpl from the given SchemaOrSchemaFn.
1030
+ */
1031
+ static create(schema) {
1032
+ if (schema instanceof SchemaImpl) {
1033
+ return schema;
1034
+ }
1035
+ return new SchemaImpl(schema);
1036
+ }
1037
+ /**
1038
+ * Compiles the given schema in a fresh compilation context. This clears the cached results of any
1039
+ * previous compilations.
1040
+ */
1041
+ static rootCompile(schema) {
1042
+ try {
1043
+ compiledSchemas.clear();
1044
+ if (schema === undefined) {
1045
+ return FieldPathNode.newRoot();
1046
+ }
1047
+ if (schema instanceof SchemaImpl) {
1048
+ return schema.compile();
1049
+ }
1050
+ return new SchemaImpl(schema).compile();
1051
+ }
1052
+ finally {
1053
+ // Use a try/finally to ensure we properly reset the compilation context upon completion,
1054
+ // even if there are errors while compiling the schema.
1055
+ compiledSchemas.clear();
1056
+ }
1057
+ }
1058
+ }
1059
+ /** Checks if the given value is a schema or schema function. */
1060
+ function isSchemaOrSchemaFn(value) {
1061
+ return value instanceof SchemaImpl || typeof value === 'function';
1062
+ }
1063
+ /** Checks that a path node belongs to the schema function currently being compiled. */
1064
+ function assertPathIsCurrent(path) {
1065
+ if (currentCompilingNode !== FieldPathNode.unwrapFieldPath(path).root) {
1066
+ throw new Error(`A FieldPath can only be used directly within the Schema that owns it,` +
1067
+ ` **not** outside of it or within a sub-schema.`);
1068
+ }
1069
+ }
1070
+
1071
+ /**
1072
+ * Represents a property that may be defined on a field when it is created using a `property` rule
1073
+ * in the schema. A particular `Property` can only be defined on a particular field **once**.
1074
+ *
1075
+ * @experimental 21.0.0
1076
+ */
1077
+ class Property {
1078
+ brand;
1079
+ /** Use {@link createProperty}. */
1080
+ constructor() { }
1081
+ }
1082
+ /**
1083
+ * Creates a {@link Property}.
1084
+ *
1085
+ * @experimental 21.0.0
1086
+ */
1087
+ function createProperty() {
1088
+ return new Property();
1089
+ }
1090
+ /**
1091
+ * Represents a property that is aggregated from multiple parts according to the property's reducer
1092
+ * function. A value can be contributed to the aggregated value for a field using an
1093
+ * `aggregateProperty` rule in the schema. There may be multiple rules in a schema that contribute
1094
+ * values to the same `AggregateProperty` of the same field.
1095
+ *
1096
+ * @experimental 21.0.0
1097
+ */
1098
+ class AggregateProperty {
1099
+ reduce;
1100
+ getInitial;
1101
+ brand;
1102
+ /** Use {@link reducedProperty}. */
1103
+ constructor(reduce, getInitial) {
1104
+ this.reduce = reduce;
1105
+ this.getInitial = getInitial;
1106
+ }
1107
+ }
1108
+ /**
1109
+ * Creates an aggregate property that reduces its individual values into an accumulated value using
1110
+ * the given `reduce` and `getInitial` functions.
1111
+ * @param reduce The reducer function.
1112
+ * @param getInitial A function that gets the initial value for the reduce operation.
1113
+ *
1114
+ * @experimental 21.0.0
1115
+ */
1116
+ function reducedProperty(reduce, getInitial) {
1117
+ return new AggregateProperty(reduce, getInitial);
1118
+ }
1119
+ /**
1120
+ * Creates an aggregate property that reduces its individual values into a list.
1121
+ *
1122
+ * @experimental 21.0.0
1123
+ */
1124
+ function listProperty() {
1125
+ return reducedProperty((acc, item) => (item === undefined ? acc : [...acc, item]), () => []);
1126
+ }
1127
+ /**
1128
+ * Creates an aggregate property that reduces its individual values by taking their min.
1129
+ *
1130
+ * @experimental 21.0.0
1131
+ */
1132
+ function minProperty() {
1133
+ return reducedProperty((prev, next) => {
1134
+ if (prev === undefined) {
1135
+ return next;
1136
+ }
1137
+ if (next === undefined) {
1138
+ return prev;
1139
+ }
1140
+ return Math.min(prev, next);
1141
+ }, () => undefined);
1142
+ }
1143
+ /**
1144
+ * Creates an aggregate property that reduces its individual values by taking their max.
1145
+ *
1146
+ * @experimental 21.0.0
1147
+ */
1148
+ function maxProperty() {
1149
+ return reducedProperty((prev, next) => {
1150
+ if (prev === undefined) {
1151
+ return next;
1152
+ }
1153
+ if (next === undefined) {
1154
+ return prev;
1155
+ }
1156
+ return Math.max(prev, next);
1157
+ }, () => undefined);
1158
+ }
1159
+ /**
1160
+ * Creates an aggregate property that reduces its individual values by logically or-ing them.
1161
+ *
1162
+ * @experimental 21.0.0
1163
+ */
1164
+ function orProperty() {
1165
+ return reducedProperty((prev, next) => prev || next, () => false);
1166
+ }
1167
+ /**
1168
+ * Creates an aggregate property that reduces its individual values by logically and-ing them.
1169
+ *
1170
+ * @experimental 21.0.0
1171
+ */
1172
+ function andProperty() {
1173
+ return reducedProperty((prev, next) => prev && next, () => true);
1174
+ }
1175
+ /**
1176
+ * An aggregate property representing whether the field is required.
1177
+ *
1178
+ * @experimental 21.0.0
1179
+ */
1180
+ const REQUIRED = orProperty();
1181
+ /**
1182
+ * An aggregate property representing the min value of the field.
1183
+ *
1184
+ * @experimental 21.0.0
1185
+ */
1186
+ const MIN = maxProperty();
1187
+ /**
1188
+ * An aggregate property representing the max value of the field.
1189
+ *
1190
+ * @experimental 21.0.0
1191
+ */
1192
+ const MAX = minProperty();
1193
+ /**
1194
+ * An aggregate property representing the min length of the field.
1195
+ *
1196
+ * @experimental 21.0.0
1197
+ */
1198
+ const MIN_LENGTH = maxProperty();
1199
+ /**
1200
+ * An aggregate property representing the max length of the field.
1201
+ *
1202
+ * @experimental 21.0.0
1203
+ */
1204
+ const MAX_LENGTH = minProperty();
1205
+ /**
1206
+ * An aggregate property representing the patterns the field must match.
1207
+ *
1208
+ * @experimental 21.0.0
1209
+ */
1210
+ const PATTERN = listProperty();
1211
+
1212
+ /**
1213
+ * Adds logic to a field to conditionally disable it. A disabled field does not contribute to the
1214
+ * validation, touched/dirty, or other state of its parent field.
1215
+ *
1216
+ * @param path The target path to add the disabled logic to.
1217
+ * @param logic A reactive function that returns `true` (or a string reason) when the field is disabled,
1218
+ * and `false` when it is not disabled.
1219
+ * @template TValue The type of value stored in the field the logic is bound to.
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
1223
+ */
1224
+ function disabled(path, logic) {
1225
+ assertPathIsCurrent(path);
1226
+ const pathNode = FieldPathNode.unwrapFieldPath(path);
1227
+ pathNode.logic.addDisabledReasonRule((ctx) => {
1228
+ let result = true;
1229
+ if (typeof logic === 'string') {
1230
+ result = logic;
1231
+ }
1232
+ else if (logic) {
1233
+ result = logic(ctx);
1234
+ }
1235
+ if (typeof result === 'string') {
1236
+ return { field: ctx.field, message: result };
1237
+ }
1238
+ return result ? { field: ctx.field } : undefined;
1239
+ });
1240
+ }
1241
+ /**
1242
+ * Adds logic to a field to conditionally make it readonly. A readonly field does not contribute to
1243
+ * the validation, touched/dirty, or other state of its parent field.
1244
+ *
1245
+ * @param path The target path to make readonly.
1246
+ * @param logic A reactive function that returns `true` when the field is readonly.
1247
+ * @template TValue The type of value stored in the field the logic is bound to.
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
1251
+ */
1252
+ function readonly(path, logic = () => true) {
1253
+ assertPathIsCurrent(path);
1254
+ const pathNode = FieldPathNode.unwrapFieldPath(path);
1255
+ pathNode.logic.addReadonlyRule(logic);
1256
+ }
1257
+ /**
1258
+ * Adds logic to a field to conditionally hide it. A hidden field does not contribute to the
1259
+ * validation, touched/dirty, or other state of its parent field.
1260
+ *
1261
+ * If a field may be hidden it is recommended to guard it with an `@if` in the template:
1262
+ * ```
1263
+ * @if (!email().hidden()) {
1264
+ * <label for="email">Email</label>
1265
+ * <input id="email" type="email" [control]="email" />
1266
+ * }
1267
+ * ```
1268
+ *
1269
+ * @param path The target path to add the hidden logic to.
1270
+ * @param logic A reactive function that returns `true` when the field is hidden.
1271
+ * @template TValue The type of value stored in the field the logic is bound to.
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
1275
+ */
1276
+ function hidden(path, logic) {
1277
+ assertPathIsCurrent(path);
1278
+ const pathNode = FieldPathNode.unwrapFieldPath(path);
1279
+ pathNode.logic.addHiddenRule(logic);
1280
+ }
1281
+ /**
1282
+ * Adds logic to a field to determine if the field has validation errors.
1283
+ *
1284
+ * @param path The target path to add the validation logic to.
1285
+ * @param logic A `Validator` that returns the current validation errors.
1286
+ * @template TValue The type of value stored in the field the logic is bound to.
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
1290
+ */
1291
+ function validate(path, logic) {
1292
+ assertPathIsCurrent(path);
1293
+ const pathNode = FieldPathNode.unwrapFieldPath(path);
1294
+ pathNode.logic.addSyncErrorRule((ctx) => addDefaultField(logic(ctx), ctx.field));
1295
+ }
1296
+ /**
1297
+ * Adds logic to a field to determine if the field or any of its child fields has validation errors.
1298
+ *
1299
+ * @param path The target path to add the validation logic to.
1300
+ * @param logic A `TreeValidator` that returns the current validation errors.
1301
+ * Errors returned by the validator may specify a target field to indicate an error on a child field.
1302
+ * @template TValue The type of value stored in the field the logic is bound to.
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
1306
+ */
1307
+ function validateTree(path, logic) {
1308
+ assertPathIsCurrent(path);
1309
+ const pathNode = FieldPathNode.unwrapFieldPath(path);
1310
+ pathNode.logic.addSyncTreeErrorRule((ctx) => addDefaultField(logic(ctx), ctx.field));
1311
+ }
1312
+ /**
1313
+ * Adds a value to an `AggregateProperty` of a field.
1314
+ *
1315
+ * @param path The target path to set the aggregate property on.
1316
+ * @param prop The aggregate property
1317
+ * @param logic A function that receives the `FieldContext` and returns a value to add to the aggregate property.
1318
+ * @template TValue The type of value stored in the field the logic is bound to.
1319
+ * @template TPropItem The type of value the property aggregates over.
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
1323
+ */
1324
+ function aggregateProperty(path, prop, logic) {
1325
+ assertPathIsCurrent(path);
1326
+ const pathNode = FieldPathNode.unwrapFieldPath(path);
1327
+ pathNode.logic.addAggregatePropertyRule(prop, logic);
1328
+ }
1329
+ function property(path, ...rest) {
1330
+ assertPathIsCurrent(path);
1331
+ let key;
1332
+ let factory;
1333
+ if (rest.length === 2) {
1334
+ [key, factory] = rest;
1335
+ }
1336
+ else {
1337
+ [factory] = rest;
1338
+ }
1339
+ key ??= createProperty();
1340
+ const pathNode = FieldPathNode.unwrapFieldPath(path);
1341
+ pathNode.logic.addPropertyFactory(key, factory);
1342
+ return key;
1343
+ }
1344
+
1345
+ /**
1346
+ * Adds async validation to the field corresponding to the given path based on a resource.
1347
+ * Async validation for a field only runs once all synchronous validation is passing.
1348
+ *
1349
+ * @param path A path indicating the field to bind the async validation logic to.
1350
+ * @param opts The async validation options.
1351
+ * @template TValue The type of value stored in the field being validated.
1352
+ * @template TParams The type of parameters to the resource.
1353
+ * @template TResult The type of result returned by the resource
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
1357
+ */
1358
+ function validateAsync(path, opts) {
1359
+ assertPathIsCurrent(path);
1360
+ const pathNode = FieldPathNode.unwrapFieldPath(path);
1361
+ const RESOURCE = property(path, (ctx) => {
1362
+ const params = computed(() => {
1363
+ const node = ctx.stateOf(path);
1364
+ const validationState = node.validationState;
1365
+ if (validationState.shouldSkipValidation() || !validationState.syncValid()) {
1366
+ return undefined;
1367
+ }
1368
+ return opts.params(ctx);
1369
+ }, ...(ngDevMode ? [{ debugName: "params" }] : []));
1370
+ return opts.factory(params);
1371
+ });
1372
+ pathNode.logic.addAsyncErrorRule((ctx) => {
1373
+ const res = ctx.state.property(RESOURCE);
1374
+ switch (res.status()) {
1375
+ case 'idle':
1376
+ return undefined;
1377
+ case 'loading':
1378
+ case 'reloading':
1379
+ return 'pending';
1380
+ case 'resolved':
1381
+ case 'local':
1382
+ if (!res.hasValue()) {
1383
+ return undefined;
1384
+ }
1385
+ const errors = opts.errors(res.value(), ctx);
1386
+ return addDefaultField(errors, ctx.field);
1387
+ case 'error':
1388
+ // TODO: Design error handling for async validation. For now, just throw the error.
1389
+ throw res.error();
1390
+ }
1391
+ });
1392
+ }
1393
+ /**
1394
+ * Adds async validation to the field corresponding to the given path based on an httpResource.
1395
+ * Async validation for a field only runs once all synchronous validation is passing.
1396
+ *
1397
+ * @param path A path indicating the field to bind the async validation logic to.
1398
+ * @param opts The http validation options.
1399
+ * @template TValue The type of value stored in the field being validated.
1400
+ * @template TResult The type of result returned by the httpResource
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
1404
+ */
1405
+ function validateHttp(path, opts) {
1406
+ validateAsync(path, {
1407
+ params: opts.request,
1408
+ factory: (request) => httpResource(request, opts.options),
1409
+ errors: opts.errors,
1410
+ });
1411
+ }
1412
+
1413
+ /**
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.
1419
+ */
1420
+ class InteropNgControl {
1421
+ field;
1422
+ constructor(field) {
1423
+ this.field = field;
1424
+ }
1425
+ control = this;
1426
+ get value() {
1427
+ return this.field().value();
1428
+ }
1429
+ get valid() {
1430
+ return this.field().valid();
1431
+ }
1432
+ get invalid() {
1433
+ return this.field().invalid();
1434
+ }
1435
+ get pending() {
1436
+ return this.field().pending();
1437
+ }
1438
+ get disabled() {
1439
+ return this.field().disabled();
1440
+ }
1441
+ get enabled() {
1442
+ return !this.field().disabled();
1443
+ }
1444
+ get errors() {
1445
+ const errors = this.field().errors();
1446
+ if (errors.length === 0) {
1447
+ return null;
1448
+ }
1449
+ const errObj = {};
1450
+ for (const error of errors) {
1451
+ errObj[error.kind] = error;
1452
+ }
1453
+ return errObj;
1454
+ }
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';
1470
+ }
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)();
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.
1494
+ }
1495
+ }
1496
+
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) {
1501
+ return undefined;
1502
+ }
1503
+ return injector._lView[injector._tNode.directiveStart + injector._tNode.componentOffset];
1504
+ }
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');
1520
+ }
1521
+ }
1522
+ function isInputSignal(value) {
1523
+ if (!isObject(value) || !(_SIGNAL in value)) {
1524
+ return false;
1525
+ }
1526
+ const node = value[_SIGNAL];
1527
+ return isObject(node) && 'applyValueToInputSignal' in node;
1528
+ }
1529
+
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
+ }
1571
+ }
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()));
1583
+ }
1584
+ /** The ControlValueAccessor for the host component. */
1585
+ get cva() {
1586
+ return this.cvaArray?.[0] ?? this._ngControl?.valueAccessor ?? undefined;
1587
+ }
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;
1598
+ }
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);
1603
+ }
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 });
1629
+ }
1630
+ /**
1631
+ * Set up state synchronization between the field and a native <input>, <textarea>, or <select>.
1632
+ */
1633
+ setupNativeInput(input) {
1634
+ const inputType = input instanceof HTMLTextAreaElement
1635
+ ? 'text'
1636
+ : input instanceof HTMLSelectElement
1637
+ ? 'select'
1638
+ : input.type;
1639
+ input.addEventListener('input', () => {
1640
+ switch (inputType) {
1641
+ case 'checkbox':
1642
+ this.state().value.set(input.checked);
1643
+ break;
1644
+ case 'radio':
1645
+ // The `input` event only fires when a radio button becomes selected, so write its `value`
1646
+ // into the state.
1647
+ this.state().value.set(input.value);
1648
+ break;
1649
+ case 'number':
1650
+ case 'range':
1651
+ case 'datetime-local':
1652
+ // We can read a `number` or a `string` from this input type.
1653
+ // Prefer whichever is consistent with the current type.
1654
+ if (typeof this.state().value() === 'number') {
1655
+ this.state().value.set(input.valueAsNumber);
1656
+ }
1657
+ else {
1658
+ this.state().value.set(input.value);
1659
+ }
1660
+ break;
1661
+ case 'date':
1662
+ case 'month':
1663
+ case 'week':
1664
+ case 'time':
1665
+ // We can read a `Date | null` or a `number` or a `string` from this input type.
1666
+ // Prefer whichever is consistent with the current type.
1667
+ if (isDateOrNull(this.state().value())) {
1668
+ this.state().value.set(input.valueAsDate);
1669
+ }
1670
+ else if (typeof this.state().value() === 'number') {
1671
+ this.state().value.set(input.valueAsNumber);
1672
+ }
1673
+ else {
1674
+ this.state().value.set(input.value);
1675
+ }
1676
+ break;
1677
+ default:
1678
+ this.state().value.set(input.value);
1679
+ break;
1680
+ }
1681
+ this.state().markAsDirty();
1682
+ });
1683
+ input.addEventListener('blur', () => this.state().markAsTouched());
1684
+ this.maybeSynchronize(() => this.state().readonly(), this.withBooleanAttribute(input, 'readonly'));
1685
+ // TODO: consider making a global configuration option for using aria-disabled instead.
1686
+ this.maybeSynchronize(() => this.state().disabled(), this.withBooleanAttribute(input, 'disabled'));
1687
+ this.maybeSynchronize(() => this.state().name(), this.withAttribute(input, 'name'));
1688
+ this.maybeSynchronize(this.propertySource(REQUIRED), this.withBooleanAttribute(input, 'required'));
1689
+ this.maybeSynchronize(this.propertySource(MIN), this.withAttribute(input, 'min'));
1690
+ this.maybeSynchronize(this.propertySource(MIN_LENGTH), this.withAttribute(input, 'minLength'));
1691
+ this.maybeSynchronize(this.propertySource(MAX), this.withAttribute(input, 'max'));
1692
+ this.maybeSynchronize(this.propertySource(MAX_LENGTH), this.withAttribute(input, 'maxLength'));
1693
+ switch (inputType) {
1694
+ case 'checkbox':
1695
+ this.maybeSynchronize(() => this.state().value(), (value) => (input.checked = value));
1696
+ break;
1697
+ case 'radio':
1698
+ this.maybeSynchronize(() => this.state().value(), (value) => {
1699
+ // Although HTML behavior is to clear the input already, we do this just in case.
1700
+ // It seems like it might be necessary in certain environments (e.g. Domino).
1701
+ input.checked = input.value === value;
1702
+ });
1703
+ break;
1704
+ case 'select':
1705
+ this.maybeSynchronize(() => this.state().value(), (value) => {
1706
+ // A select will not take a value unil the value's option has rendered.
1707
+ afterNextRender(() => (input.value = value), { injector: this.injector });
1708
+ });
1709
+ break;
1710
+ case 'number':
1711
+ case 'range':
1712
+ case 'datetime-local':
1713
+ // This input type can receive a `number` or a `string`.
1714
+ this.maybeSynchronize(() => this.state().value(), (value) => {
1715
+ if (typeof value === 'number') {
1716
+ input.valueAsNumber = value;
1717
+ }
1718
+ else {
1719
+ input.value = value;
1720
+ }
1721
+ });
1722
+ break;
1723
+ case 'date':
1724
+ case 'month':
1725
+ case 'week':
1726
+ case 'time':
1727
+ // This input type can receive a `Date | null` or a `number` or a `string`.
1728
+ this.maybeSynchronize(() => this.state().value(), (value) => {
1729
+ if (isDateOrNull(value)) {
1730
+ input.valueAsDate = value;
1731
+ }
1732
+ else if (typeof value === 'number') {
1733
+ input.valueAsNumber = value;
1734
+ }
1735
+ else {
1736
+ input.value = value;
1737
+ }
1738
+ });
1739
+ break;
1740
+ default:
1741
+ this.maybeSynchronize(() => this.state().value(), (value) => {
1742
+ input.value = value;
1743
+ });
1744
+ break;
1745
+ }
1746
+ }
1747
+ /** Set up state synchronization between the field and a ControlValueAccessor. */
1748
+ setupControlValueAccessor(cva) {
1749
+ cva.registerOnChange((value) => this.state().value.set(value));
1750
+ cva.registerOnTouched(() => this.state().markAsTouched());
1751
+ this.maybeSynchronize(() => this.state().value(), (value) => cva.writeValue(value));
1752
+ if (cva.setDisabledState) {
1753
+ this.maybeSynchronize(() => this.state().disabled(), (value) => cva.setDisabledState(value));
1754
+ }
1755
+ cva.writeValue(this.state().value());
1756
+ cva.setDisabledState?.(this.state().disabled());
1757
+ }
1758
+ /** Set up state synchronization between the field and a FormUiControl. */
1759
+ setupCustomUiControl(cmp) {
1760
+ // Handle the property side of the model binding. How we do this depends on the shape of the
1761
+ // component. There are 2 options:
1762
+ // * it provides a `value` model (most controls that edit a single value)
1763
+ // * it provides a `checked` model with no `value` signal (custom checkbox)
1764
+ let cleanupValue;
1765
+ if (isFormValueControl(cmp)) {
1766
+ // <custom-input [(value)]="state().value">
1767
+ this.maybeSynchronize(() => this.state().value(), withInput(cmp.value));
1768
+ cleanupValue = cmp.value.subscribe((newValue) => this.state().value.set(newValue));
1769
+ }
1770
+ else if (isFormCheckboxControl(cmp)) {
1771
+ // <custom-checkbox [(checked)]="state().value" />
1772
+ this.maybeSynchronize(() => this.state().value(), withInput(cmp.checked));
1773
+ cleanupValue = cmp.checked.subscribe((newValue) => this.state().value.set(newValue));
1774
+ }
1775
+ else {
1776
+ throw new Error(`Unknown custom control subtype`);
1777
+ }
1778
+ this.maybeSynchronize(() => this.state().name(), withInput(cmp.name));
1779
+ this.maybeSynchronize(() => this.state().disabled(), withInput(cmp.disabled));
1780
+ this.maybeSynchronize(() => this.state().disabledReasons(), withInput(cmp.disabledReasons));
1781
+ this.maybeSynchronize(() => this.state().readonly(), withInput(cmp.readonly));
1782
+ this.maybeSynchronize(() => this.state().hidden(), withInput(cmp.hidden));
1783
+ this.maybeSynchronize(() => this.state().errors(), withInput(cmp.errors));
1784
+ if (privateIsModelInput(cmp.touched) || privateIsSignalInput(cmp.touched)) {
1785
+ this.maybeSynchronize(() => this.state().touched(), withInput(cmp.touched));
1786
+ }
1787
+ this.maybeSynchronize(() => this.state().dirty(), withInput(cmp.dirty));
1788
+ this.maybeSynchronize(() => this.state().invalid(), withInput(cmp.invalid));
1789
+ this.maybeSynchronize(() => this.state().pending(), withInput(cmp.pending));
1790
+ this.maybeSynchronize(this.propertySource(REQUIRED), withInput(cmp.required));
1791
+ this.maybeSynchronize(this.propertySource(MIN), withInput(cmp.min));
1792
+ this.maybeSynchronize(this.propertySource(MIN_LENGTH), withInput(cmp.minLength));
1793
+ this.maybeSynchronize(this.propertySource(MAX), withInput(cmp.max));
1794
+ this.maybeSynchronize(this.propertySource(MAX_LENGTH), withInput(cmp.maxLength));
1795
+ this.maybeSynchronize(this.propertySource(PATTERN), withInput(cmp.pattern));
1796
+ let cleanupTouch;
1797
+ let cleanupDefaultTouch;
1798
+ if (privateIsModelInput(cmp.touched) || isOutputRef(cmp.touched)) {
1799
+ cleanupTouch = cmp.touched.subscribe(() => this.state().markAsTouched());
1800
+ }
1801
+ else {
1802
+ // If the component did not give us a touch event stream, use the standard touch logic,
1803
+ // marking it touched when the focus moves from inside the host element to outside.
1804
+ const listener = (event) => {
1805
+ const newActiveEl = event.relatedTarget;
1806
+ if (!this.el.nativeElement.contains(newActiveEl)) {
1807
+ this.state().markAsTouched();
1808
+ }
1809
+ };
1810
+ this.el.nativeElement.addEventListener('focusout', listener);
1811
+ cleanupDefaultTouch = () => this.el.nativeElement.removeEventListener('focusout', listener);
1812
+ }
1813
+ // Cleanup for output binding subscriptions:
1814
+ this.injector.get(DestroyRef).onDestroy(() => {
1815
+ cleanupValue?.unsubscribe();
1816
+ cleanupTouch?.unsubscribe();
1817
+ cleanupDefaultTouch?.();
1818
+ });
1819
+ }
1820
+ /** Synchronize a value from a reactive source to a given sink. */
1821
+ maybeSynchronize(source, sink) {
1822
+ if (!sink) {
1823
+ return undefined;
1824
+ }
1825
+ const ref = effect(() => {
1826
+ const value = source();
1827
+ untracked(() => sink(value));
1828
+ }, ...(ngDevMode ? [{ debugName: "ref", injector: this.injector }] : [{ injector: this.injector }]));
1829
+ // Run the effect immediately to ensure sinks which are required inputs are set before they can
1830
+ // be observed. See the note on `_field` for more details.
1831
+ privateRunEffect(ref);
1832
+ }
1833
+ /** Creates a reactive value source by reading the given AggregateProperty from the field. */
1834
+ propertySource(key) {
1835
+ const metaSource = computed(() => this.state().hasProperty(key) ? this.state().property(key) : key.getInitial, ...(ngDevMode ? [{ debugName: "metaSource" }] : []));
1836
+ return () => metaSource()?.();
1837
+ }
1838
+ /** Creates a (non-boolean) value sync that writes the given attribute of the given element. */
1839
+ withAttribute(element, attribute) {
1840
+ return (value) => {
1841
+ if (value !== undefined) {
1842
+ this.renderer.setAttribute(element, attribute, value.toString());
1843
+ }
1844
+ else {
1845
+ this.renderer.removeAttribute(element, attribute);
1846
+ }
1847
+ };
1848
+ }
1849
+ /** Creates a boolean value sync that writes the given attribute of the given element. */
1850
+ withBooleanAttribute(element, attribute) {
1851
+ return (value) => {
1852
+ if (value) {
1853
+ this.renderer.setAttribute(element, attribute, '');
1854
+ }
1855
+ else {
1856
+ this.renderer.removeAttribute(element, attribute);
1857
+ }
1858
+ };
1859
+ }
1860
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0-next.3", ngImport: i0, type: Control, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1861
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.0-next.3", type: Control, isStandalone: true, selector: "[control]", inputs: { _field: ["control", "_field"] }, providers: [
1862
+ {
1863
+ provide: NgControl,
1864
+ useFactory: () => inject(Control).ngControl,
1865
+ },
1866
+ ], ngImport: i0 });
1867
+ }
1868
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.0-next.3", ngImport: i0, type: Control, decorators: [{
1869
+ type: Directive,
1870
+ args: [{
1871
+ selector: '[control]',
1872
+ providers: [
1873
+ {
1874
+ provide: NgControl,
1875
+ useFactory: () => inject(Control).ngControl,
1876
+ },
1877
+ ],
1878
+ }]
1879
+ }], propDecorators: { _field: [{
1880
+ type: Input,
1881
+ args: [{ required: true, alias: 'control' }]
1882
+ }] } });
1883
+ /** Creates a value sync from an input signal. */
1884
+ function withInput(input) {
1885
+ return input ? (value) => privateSetComponentInput(input, value) : undefined;
1886
+ }
1887
+ /**
1888
+ * Checks whether the given component matches the contract for either FormValueControl or
1889
+ * FormCheckboxControl.
1890
+ */
1891
+ function isFormUiControl(cmp) {
1892
+ const castCmp = cmp;
1893
+ return ((isFormValueControl(castCmp) || isFormCheckboxControl(castCmp)) &&
1894
+ (castCmp.readonly === undefined || privateIsSignalInput(castCmp.readonly)) &&
1895
+ (castCmp.disabled === undefined || privateIsSignalInput(castCmp.disabled)) &&
1896
+ (castCmp.disabledReasons === undefined || privateIsSignalInput(castCmp.disabledReasons)) &&
1897
+ (castCmp.errors === undefined || privateIsSignalInput(castCmp.errors)) &&
1898
+ (castCmp.invalid === undefined || privateIsSignalInput(castCmp.invalid)) &&
1899
+ (castCmp.pending === undefined || privateIsSignalInput(castCmp.pending)) &&
1900
+ (castCmp.touched === undefined ||
1901
+ privateIsModelInput(castCmp.touched) ||
1902
+ privateIsSignalInput(castCmp.touched) ||
1903
+ isOutputRef(castCmp.touched)) &&
1904
+ (castCmp.dirty === undefined || privateIsSignalInput(castCmp.dirty)) &&
1905
+ (castCmp.min === undefined || privateIsSignalInput(castCmp.min)) &&
1906
+ (castCmp.minLength === undefined || privateIsSignalInput(castCmp.minLength)) &&
1907
+ (castCmp.max === undefined || privateIsSignalInput(castCmp.max)) &&
1908
+ (castCmp.maxLength === undefined || privateIsSignalInput(castCmp.maxLength)));
1909
+ }
1910
+ /** Checks whether the given FormUiControl is a FormValueControl. */
1911
+ function isFormValueControl(cmp) {
1912
+ return privateIsModelInput(cmp.value);
1913
+ }
1914
+ /** Checks whether the given FormUiControl is a FormCheckboxControl. */
1915
+ function isFormCheckboxControl(cmp) {
1916
+ return (privateIsModelInput(cmp.checked) &&
1917
+ cmp.value === undefined);
1918
+ }
1919
+ /** Checks whether the given component has an input called `control`. */
1920
+ function isShadowedControlComponent(cmp) {
1921
+ const mirror = reflectComponentType(cmp.constructor);
1922
+ return mirror?.inputs.some((input) => input.templateName === 'control') ?? false;
1923
+ }
1924
+ /** Checks whether the given object is an output ref. */
1925
+ function isOutputRef(value) {
1926
+ return value instanceof OutputEmitterRef || value instanceof EventEmitter;
1927
+ }
1928
+ /** Checks if a given value is a Date or null */
1929
+ function isDateOrNull(value) {
1930
+ return value === null || value instanceof Date;
1931
+ }
1932
+
1933
+ /**
1934
+ * `FieldContext` implementation, backed by a `FieldNode`.
1935
+ */
1936
+ class FieldNodeContext {
1937
+ node;
1938
+ /**
1939
+ * Cache of paths that have been resolved for this context.
1940
+ *
1941
+ * For each resolved path we keep track of a signal of field that it maps to rather than a static
1942
+ * field, since it theoretically could change. In practice for the current system it should not
1943
+ * actually change, as they only place we currently track fields moving within the parent
1944
+ * structure is for arrays, and paths do not currently support array indexing.
1945
+ */
1946
+ cache = new WeakMap();
1947
+ constructor(
1948
+ /** The field node this context corresponds to. */
1949
+ node) {
1950
+ this.node = node;
1951
+ }
1952
+ /**
1953
+ * Resolves a target path relative to this context.
1954
+ * @param target The path to resolve
1955
+ * @returns The field corresponding to the target path.
1956
+ */
1957
+ resolve(target) {
1958
+ if (!this.cache.has(target)) {
1959
+ const resolver = computed(() => {
1960
+ const targetPathNode = FieldPathNode.unwrapFieldPath(target);
1961
+ // First, find the field where the root our target path was merged in.
1962
+ // We determine this by walking up the field tree from the current field and looking for
1963
+ // the place where the LogicNodeBuilder from the target path's root was merged in.
1964
+ // We always make sure to walk up at least as far as the depth of the path we were bound to.
1965
+ // This ensures that we do not accidentally match on the wrong application of a recursively
1966
+ // applied schema.
1967
+ let field = this.node;
1968
+ let stepsRemaining = getBoundPathDepth();
1969
+ while (stepsRemaining > 0 || !field.structure.logic.hasLogic(targetPathNode.root.logic)) {
1970
+ stepsRemaining--;
1971
+ field = field.structure.parent;
1972
+ if (field === undefined) {
1973
+ throw new Error('Path is not part of this field tree.');
1974
+ }
1975
+ }
1976
+ // Now, we can navigate to the target field using the relative path in the target path node
1977
+ // to traverse down from the field we just found.
1978
+ for (let key of targetPathNode.keys) {
1979
+ field = field.structure.getChild(key);
1980
+ if (field === undefined) {
1981
+ throw new Error(`Cannot resolve path .${targetPathNode.keys.join('.')} relative to field ${[
1982
+ '<root>',
1983
+ ...this.node.structure.pathKeys(),
1984
+ ].join('.')}.`);
1985
+ }
1986
+ }
1987
+ return field.fieldProxy;
1988
+ }, ...(ngDevMode ? [{ debugName: "resolver" }] : []));
1989
+ this.cache.set(target, resolver);
1990
+ }
1991
+ return this.cache.get(target)();
1992
+ }
1993
+ get field() {
1994
+ return this.node.fieldProxy;
1995
+ }
1996
+ get state() {
1997
+ return this.node;
1998
+ }
1999
+ get value() {
2000
+ return this.node.structure.value;
2001
+ }
2002
+ get key() {
2003
+ return this.node.structure.keyInParent;
2004
+ }
2005
+ index = computed(() => {
2006
+ // Attempt to read the key first, this will throw an error if we're on a root field.
2007
+ const key = this.key();
2008
+ // Assert that the parent is actually an array.
2009
+ if (!isArray(untracked(this.node.structure.parent.value))) {
2010
+ throw new Error(`RuntimeError: cannot access index, parent field is not an array`);
2011
+ }
2012
+ // Return the key as a number if we are indeed inside an array field.
2013
+ return Number(key);
2014
+ }, ...(ngDevMode ? [{ debugName: "index" }] : []));
2015
+ fieldOf = (p) => this.resolve(p);
2016
+ stateOf = (p) => this.resolve(p)();
2017
+ valueOf = (p) => this.resolve(p)().value();
2018
+ }
2019
+
2020
+ /**
2021
+ * Tracks custom properties associated with a `FieldNode`.
2022
+ */
2023
+ class FieldPropertyState {
2024
+ node;
2025
+ /** A map of all `Property` and `AggregateProperty` that have been defined for this field. */
2026
+ properties = new Map();
2027
+ constructor(node) {
2028
+ this.node = node;
2029
+ // Field nodes (and thus their property state) are created in a linkedSignal in order to mirror
2030
+ // the structure of the model data. We need to run the property factories untracked so that they
2031
+ // do not cause recomputation of the linkedSignal.
2032
+ untracked(() =>
2033
+ // Property factories are run in the form's injection context so they can create resources
2034
+ // and inject DI dependencies.
2035
+ runInInjectionContext(this.node.structure.injector, () => {
2036
+ for (const [key, factory] of this.node.logicNode.logic.getPropertyFactoryEntries()) {
2037
+ this.properties.set(key, factory(this.node.context));
2038
+ }
2039
+ }));
2040
+ }
2041
+ /** Gets the value of a `Property` or `AggregateProperty` for the field. */
2042
+ get(prop) {
2043
+ if (prop instanceof Property) {
2044
+ return this.properties.get(prop);
2045
+ }
2046
+ if (!this.properties.has(prop)) {
2047
+ const logic = this.node.logicNode.logic.getAggregateProperty(prop);
2048
+ const result = computed(() => logic.compute(this.node.context), ...(ngDevMode ? [{ debugName: "result" }] : []));
2049
+ this.properties.set(prop, result);
2050
+ }
2051
+ return this.properties.get(prop);
2052
+ }
2053
+ /**
2054
+ * Checks whether the current property state has the given property.
2055
+ * @param prop
2056
+ * @returns
2057
+ */
2058
+ has(prop) {
2059
+ if (prop instanceof AggregateProperty) {
2060
+ // For aggregate properties, they get added to the map lazily, on first access, so we can't
2061
+ // rely on checking presence in the properties map. Instead we check if there is any logic for
2062
+ // the given property.
2063
+ return this.node.logicNode.logic.hasAggregateProperty(prop);
2064
+ }
2065
+ else {
2066
+ // Non-aggregate proeprties get added to our properties map on construction, so we can just
2067
+ // refer to their presence in the map.
2068
+ return this.properties.has(prop);
2069
+ }
2070
+ }
2071
+ }
2072
+
2073
+ /**
2074
+ * Proxy handler which implements `Field<T>` on top of `FieldNode`.
2075
+ */
2076
+ const FIELD_PROXY_HANDLER = {
2077
+ get(getTgt, p) {
2078
+ const tgt = getTgt();
2079
+ // First, check whether the requested property is a defined child node of this node.
2080
+ const child = tgt.structure.getChild(p);
2081
+ if (child !== undefined) {
2082
+ // If so, return the child node's `Field` proxy, allowing the developer to continue navigating
2083
+ // the form structure.
2084
+ return child.fieldProxy;
2085
+ }
2086
+ // Otherwise, we need to consider whether the properties they're accessing are related to array
2087
+ // iteration. We're specifically interested in `length`, but we only want to pass this through
2088
+ // if the value is actually an array.
2089
+ //
2090
+ // We untrack the value here to avoid spurious reactive notifications. In reality, we've already
2091
+ // incurred a dependency on the value via `tgt.getChild()` above.
2092
+ const value = untracked(tgt.value);
2093
+ if (isArray(value)) {
2094
+ // Allow access to the length for field arrays, it should be the same as the length of the data.
2095
+ if (p === 'length') {
2096
+ return tgt.value().length;
2097
+ }
2098
+ // Allow access to the iterator. This allows the user to spread the field array into a
2099
+ // standard array in order to call methods like `filter`, `map`, etc.
2100
+ if (p === Symbol.iterator) {
2101
+ return Array.prototype[p];
2102
+ }
2103
+ // Note: We can consider supporting additional array methods if we want in the future,
2104
+ // but they should be thoroughly tested. Just forwarding the method directly from the
2105
+ // `Array` prototype results in broken behavior for some methods like `map`.
2106
+ }
2107
+ // Otherwise, this property doesn't exist.
2108
+ return undefined;
2109
+ },
2110
+ };
2111
+
2112
+ /**
2113
+ * Creates a writable signal for a specific property on a source writeable signal.
2114
+ * @param source A writeable signal to derive from
2115
+ * @param prop A signal of a property key of the source value
2116
+ * @returns A writeable signal for the given property of the source value.
2117
+ * @template S The source value type
2118
+ * @template K The key type for S
2119
+ */
2120
+ function deepSignal(source, prop) {
2121
+ // Memoize the property.
2122
+ const read = computed(() => source()[prop()]);
2123
+ read[SIGNAL] = source[SIGNAL];
2124
+ read.set = (value) => {
2125
+ source.update((current) => valueForWrite(current, value, prop()));
2126
+ };
2127
+ read.update = (fn) => {
2128
+ read.set(fn(untracked(read)));
2129
+ };
2130
+ read.asReadonly = () => read;
2131
+ return read;
2132
+ }
2133
+ /**
2134
+ * Gets an updated root value to use when setting a value on a deepSignal with the given path.
2135
+ * @param sourceValue The current value of the deepSignal's source.
2136
+ * @param newPropValue The value being written to the deepSignal's property
2137
+ * @param prop The deepSignal's property key
2138
+ * @returns An updated value for the deepSignal's source
2139
+ */
2140
+ function valueForWrite(sourceValue, newPropValue, prop) {
2141
+ if (isArray(sourceValue)) {
2142
+ const newValue = [...sourceValue];
2143
+ newValue[prop] = newPropValue;
2144
+ return newValue;
2145
+ }
2146
+ else {
2147
+ return { ...sourceValue, [prop]: newPropValue };
2148
+ }
2149
+ }
2150
+
2151
+ /** Structural component of a `FieldNode` which tracks its path, parent, and children. */
2152
+ class FieldNodeStructure {
2153
+ logic;
2154
+ /** Added to array elements for tracking purposes. */
2155
+ // TODO: given that we don't ever let a field move between parents, is it safe to just extract
2156
+ // this to a shared symbol for all fields, rather than having a separate one per parent?
2157
+ identitySymbol = Symbol();
2158
+ /** Lazily initialized injector. Do not access directly, access via `injector` getter instead. */
2159
+ _injector = undefined;
2160
+ /** Lazily initialized injector. */
2161
+ get injector() {
2162
+ this._injector ??= Injector.create({
2163
+ providers: [],
2164
+ parent: this.fieldManager.injector,
2165
+ });
2166
+ return this._injector;
2167
+ }
2168
+ constructor(
2169
+ /** The logic to apply to this field. */
2170
+ logic) {
2171
+ this.logic = logic;
2172
+ }
2173
+ /** Gets the child fields of this field. */
2174
+ children() {
2175
+ return this.childrenMap()?.values() ?? [];
2176
+ }
2177
+ /** Retrieve a child `FieldNode` of this node by property key. */
2178
+ getChild(key) {
2179
+ const map = this.childrenMap();
2180
+ const value = this.value();
2181
+ if (!map || !isObject(value)) {
2182
+ return undefined;
2183
+ }
2184
+ if (isArray(value)) {
2185
+ const childValue = value[key];
2186
+ if (isObject(childValue) && childValue.hasOwnProperty(this.identitySymbol)) {
2187
+ // For arrays, we want to use the tracking identity of the value instead of the raw property
2188
+ // as our index into the `childrenMap`.
2189
+ key = childValue[this.identitySymbol];
2190
+ }
2191
+ }
2192
+ return map.get((typeof key === 'number' ? key.toString() : key));
2193
+ }
2194
+ /** Destroys the field when it is no longer needed. */
2195
+ destroy() {
2196
+ this.injector.destroy();
2197
+ }
2198
+ }
2199
+ /** The structural component of a `FieldNode` that is the root of its field tree. */
2200
+ class RootFieldNodeStructure extends FieldNodeStructure {
2201
+ node;
2202
+ fieldManager;
2203
+ value;
2204
+ get parent() {
2205
+ return undefined;
2206
+ }
2207
+ get root() {
2208
+ return this.node;
2209
+ }
2210
+ get pathKeys() {
2211
+ return ROOT_PATH_KEYS;
2212
+ }
2213
+ get keyInParent() {
2214
+ return ROOT_KEY_IN_PARENT;
2215
+ }
2216
+ childrenMap;
2217
+ /**
2218
+ * Creates the structure for the root node of a field tree.
2219
+ *
2220
+ * @param node The full field node that this structure belongs to
2221
+ * @param pathNode The path corresponding to this node in the schema
2222
+ * @param logic The logic to apply to this field
2223
+ * @param fieldManager The field manager for this field
2224
+ * @param value The value signal for this field
2225
+ * @param adapter Adapter that knows how to create new fields and appropriate state.
2226
+ * @param createChildNode A factory function to create child nodes for this field.
2227
+ */
2228
+ constructor(
2229
+ /** The full field node that corresponds to this structure. */
2230
+ node, pathNode, logic, fieldManager, value, adapter, createChildNode) {
2231
+ super(logic);
2232
+ this.node = node;
2233
+ this.fieldManager = fieldManager;
2234
+ this.value = value;
2235
+ this.childrenMap = makeChildrenMapSignal(node, value, this.identitySymbol, pathNode, logic, adapter, createChildNode);
2236
+ }
2237
+ }
2238
+ /** The structural component of a child `FieldNode` within a field tree. */
2239
+ class ChildFieldNodeStructure extends FieldNodeStructure {
2240
+ parent;
2241
+ root;
2242
+ pathKeys;
2243
+ keyInParent;
2244
+ value;
2245
+ childrenMap;
2246
+ get fieldManager() {
2247
+ return this.root.structure.fieldManager;
2248
+ }
2249
+ /**
2250
+ * Creates the structure for a child field node in a field tree.
2251
+ *
2252
+ * @param node The full field node that this structure belongs to
2253
+ * @param pathNode The path corresponding to this node in the schema
2254
+ * @param logic The logic to apply to this field
2255
+ * @param parent The parent field node for this node
2256
+ * @param identityInParent The identity used to track this field in its parent
2257
+ * @param initialKeyInParent The key of this field in its parent at the time of creation
2258
+ * @param adapter Adapter that knows how to create new fields and appropriate state.
2259
+ * @param createChildNode A factory function to create child nodes for this field.
2260
+ */
2261
+ constructor(node, pathNode, logic, parent, identityInParent, initialKeyInParent, adapter, createChildNode) {
2262
+ super(logic);
2263
+ this.parent = parent;
2264
+ this.root = this.parent.structure.root;
2265
+ this.pathKeys = computed(() => [...parent.structure.pathKeys(), this.keyInParent()], ...(ngDevMode ? [{ debugName: "pathKeys" }] : []));
2266
+ if (identityInParent === undefined) {
2267
+ const key = initialKeyInParent;
2268
+ this.keyInParent = computed(() => {
2269
+ if (parent.structure.childrenMap()?.get(key) !== node) {
2270
+ throw new Error(`RuntimeError: orphan field, looking for property '${key}' of ${getDebugName(parent)}`);
2271
+ }
2272
+ return key;
2273
+ }, ...(ngDevMode ? [{ debugName: "keyInParent" }] : []));
2274
+ }
2275
+ else {
2276
+ let lastKnownKey = initialKeyInParent;
2277
+ this.keyInParent = computed(() => {
2278
+ // TODO(alxhub): future perf optimization: here we depend on the parent's value, but most
2279
+ // changes to the value aren't structural - they aren't moving around objects and thus
2280
+ // shouldn't affect `keyInParent`. We currently mitigate this issue via `lastKnownKey`
2281
+ // which avoids a search.
2282
+ const parentValue = parent.structure.value();
2283
+ if (!isArray(parentValue)) {
2284
+ // It should not be possible to encounter this error. It would require the parent to
2285
+ // change from an array field to non-array field. However, in the current implementation
2286
+ // a field's parent can never change.
2287
+ throw new Error(`RuntimeError: orphan field, expected ${getDebugName(parent)} to be an array`);
2288
+ }
2289
+ // Check the parent value at the last known key to avoid a scan.
2290
+ // Note: lastKnownKey is a string, but we pretend to typescript like its a number,
2291
+ // since accessing someArray['1'] is the same as accessing someArray[1]
2292
+ const data = parentValue[lastKnownKey];
2293
+ if (isObject(data) &&
2294
+ data.hasOwnProperty(parent.structure.identitySymbol) &&
2295
+ data[parent.structure.identitySymbol] === identityInParent) {
2296
+ return lastKnownKey;
2297
+ }
2298
+ // Otherwise, we need to check all the keys in the parent.
2299
+ for (let i = 0; i < parentValue.length; i++) {
2300
+ const data = parentValue[i];
2301
+ if (isObject(data) &&
2302
+ data.hasOwnProperty(parent.structure.identitySymbol) &&
2303
+ data[parent.structure.identitySymbol] === identityInParent) {
2304
+ return (lastKnownKey = i.toString());
2305
+ }
2306
+ }
2307
+ throw new Error(`RuntimeError: orphan field, can't find element in array ${getDebugName(parent)}`);
2308
+ }, ...(ngDevMode ? [{ debugName: "keyInParent" }] : []));
2309
+ }
2310
+ this.value = deepSignal(this.parent.structure.value, this.keyInParent);
2311
+ this.childrenMap = makeChildrenMapSignal(node, this.value, this.identitySymbol, pathNode, logic, adapter, createChildNode);
2312
+ this.fieldManager.structures.add(this);
2313
+ }
2314
+ }
2315
+ /** Global id used for tracking keys. */
2316
+ let globalId = 0;
2317
+ /** A signal representing an empty list of path keys, used for root fields. */
2318
+ const ROOT_PATH_KEYS = computed(() => [], ...(ngDevMode ? [{ debugName: "ROOT_PATH_KEYS" }] : []));
2319
+ /**
2320
+ * A signal representing a non-existent key of the field in its parent, used for root fields which
2321
+ * do not have a parent. This signal will throw if it is read.
2322
+ */
2323
+ const ROOT_KEY_IN_PARENT = computed(() => {
2324
+ throw new Error(`RuntimeError: the top-level field in the form has no parent`);
2325
+ }, ...(ngDevMode ? [{ debugName: "ROOT_KEY_IN_PARENT" }] : []));
2326
+ /**
2327
+ * Creates a linked signal map of all child fields for a field.
2328
+ *
2329
+ * @param node The field to create the children map signal for.
2330
+ * @param valueSignal The value signal for the field.
2331
+ * @param identitySymbol The key used to access the tracking id of a field.
2332
+ * @param pathNode The path node corresponding to the field in the schema.
2333
+ * @param logic The logic to apply to the field.
2334
+ * @param adapter Adapter that knows how to create new fields and appropriate state.
2335
+ * @param createChildNode A factory function to create child nodes for this field.
2336
+ * @returns
2337
+ */
2338
+ function makeChildrenMapSignal(node, valueSignal, identitySymbol, pathNode, logic, adapter, createChildNode) {
2339
+ // We use a `linkedSignal` to preserve the instances of `FieldNode` for each child field even if
2340
+ // the value of this field changes its object identity. The computation creates or updates the map
2341
+ // of child `FieldNode`s for `node` based on its current value.
2342
+ return linkedSignal({
2343
+ source: valueSignal,
2344
+ computation: (value, previous) => {
2345
+ // We may or may not have a previous map. If there isn't one, then `childrenMap` will be lazily
2346
+ // initialized to a new map instance if needed.
2347
+ let childrenMap = previous?.value;
2348
+ if (!isObject(value)) {
2349
+ // Non-object values have no children.
2350
+ return undefined;
2351
+ }
2352
+ const isValueArray = isArray(value);
2353
+ // Remove fields that have disappeared since the last time this map was computed.
2354
+ if (childrenMap !== undefined) {
2355
+ let oldKeys = undefined;
2356
+ if (isValueArray) {
2357
+ oldKeys = new Set(childrenMap.keys());
2358
+ for (let i = 0; i < value.length; i++) {
2359
+ const childValue = value[i];
2360
+ if (isObject(childValue) && childValue.hasOwnProperty(identitySymbol)) {
2361
+ oldKeys.delete(childValue[identitySymbol]);
2362
+ }
2363
+ else {
2364
+ oldKeys.delete(i.toString());
2365
+ }
2366
+ }
2367
+ for (const key of oldKeys) {
2368
+ childrenMap.delete(key);
2369
+ }
2370
+ }
2371
+ else {
2372
+ for (let key of childrenMap.keys()) {
2373
+ if (!value.hasOwnProperty(key)) {
2374
+ childrenMap.delete(key);
2375
+ }
2376
+ }
2377
+ }
2378
+ }
2379
+ // Add fields that exist in the value but don't yet have instances in the map.
2380
+ for (let key of Object.keys(value)) {
2381
+ let trackingId = undefined;
2382
+ const childValue = value[key];
2383
+ // Fields explicitly set to `undefined` are treated as if they don't exist.
2384
+ // This ensures that `{value: undefined}` and `{}` have the same behavior for their `value`
2385
+ // field.
2386
+ if (childValue === undefined) {
2387
+ // The value might have _become_ `undefined`, so we need to delete it here.
2388
+ childrenMap?.delete(key);
2389
+ continue;
2390
+ }
2391
+ if (isValueArray && isObject(childValue)) {
2392
+ // For object values in arrays, assign a synthetic identity instead.
2393
+ trackingId = childValue[identitySymbol] ??= Symbol(ngDevMode ? `id:${globalId++}` : '');
2394
+ }
2395
+ const identity = trackingId ?? key;
2396
+ if (childrenMap?.has(identity)) {
2397
+ continue;
2398
+ }
2399
+ // Determine the logic for the field that we're defining.
2400
+ let childPath;
2401
+ let childLogic;
2402
+ if (isValueArray) {
2403
+ // Fields for array elements have their logic defined by the `element` mechanism.
2404
+ // TODO: other dynamic data
2405
+ childPath = pathNode.getChild(DYNAMIC);
2406
+ childLogic = logic.getChild(DYNAMIC);
2407
+ }
2408
+ else {
2409
+ // Fields for plain properties exist in our logic node's child map.
2410
+ childPath = pathNode.getChild(key);
2411
+ childLogic = logic.getChild(key);
2412
+ }
2413
+ childrenMap ??= new Map();
2414
+ childrenMap.set(identity, createChildNode({
2415
+ kind: 'child',
2416
+ parent: node,
2417
+ pathNode: childPath,
2418
+ logic: childLogic,
2419
+ initialKeyInParent: key,
2420
+ identityInParent: trackingId,
2421
+ fieldAdapter: adapter,
2422
+ }));
2423
+ }
2424
+ return childrenMap;
2425
+ },
2426
+ equal: () => false,
2427
+ });
2428
+ }
2429
+ /** Gets a human readable name for a field node for use in error messages. */
2430
+ function getDebugName(node) {
2431
+ return `<root>.${node.structure.pathKeys().join('.')}`;
2432
+ }
2433
+
2434
+ /**
2435
+ * State of a `FieldNode` that's associated with form submission.
2436
+ */
2437
+ class FieldSubmitState {
2438
+ node;
2439
+ /**
2440
+ * Whether this field was directly submitted (as opposed to indirectly by a parent field being submitted)
2441
+ * and is still in the process of submitting.
2442
+ */
2443
+ selfSubmitting = signal(false, ...(ngDevMode ? [{ debugName: "selfSubmitting" }] : []));
2444
+ /** Server errors that are associated with this field. */
2445
+ serverErrors;
2446
+ constructor(node) {
2447
+ this.node = node;
2448
+ this.serverErrors = linkedSignal({
2449
+ source: this.node.structure.value,
2450
+ computation: () => [],
2451
+ });
2452
+ }
2453
+ /**
2454
+ * Whether this form is currently in the process of being submitted.
2455
+ * Either because the field was submitted directly, or because a parent field was submitted.
2456
+ */
2457
+ submitting = computed(() => {
2458
+ return this.selfSubmitting() || (this.node.structure.parent?.submitting() ?? false);
2459
+ }, ...(ngDevMode ? [{ debugName: "submitting" }] : []));
2460
+ }
2461
+
2462
+ /**
2463
+ * Internal node in the form tree for a given field.
2464
+ *
2465
+ * Field nodes have several responsibilities:
2466
+ * - They track instance state for the particular field (touched)
2467
+ * - They compute signals for derived state (valid, disabled, etc) based on their associated
2468
+ * `LogicNode`
2469
+ * - They act as the public API for the field (they implement the `FieldState` interface)
2470
+ * - They implement navigation of the form tree via `.parent` and `.getChild()`.
2471
+ *
2472
+ * This class is largely a wrapper that aggregates several smaller pieces that each manage a subset of
2473
+ * the responsibilities.
2474
+ */
2475
+ class FieldNode {
2476
+ structure;
2477
+ validationState;
2478
+ propertyState;
2479
+ nodeState;
2480
+ submitState;
2481
+ _context = undefined;
2482
+ fieldAdapter;
2483
+ get context() {
2484
+ return (this._context ??= new FieldNodeContext(this));
2485
+ }
2486
+ /**
2487
+ * Proxy to this node which allows navigation of the form graph below it.
2488
+ */
2489
+ fieldProxy = new Proxy(() => this, FIELD_PROXY_HANDLER);
2490
+ constructor(options) {
2491
+ this.fieldAdapter = options.fieldAdapter;
2492
+ this.structure = this.fieldAdapter.createStructure(this, options);
2493
+ this.validationState = this.fieldAdapter.createValidationState(this, options);
2494
+ this.nodeState = this.fieldAdapter.createNodeState(this, options);
2495
+ this.propertyState = new FieldPropertyState(this);
2496
+ this.submitState = new FieldSubmitState(this);
2497
+ }
2498
+ get logicNode() {
2499
+ return this.structure.logic;
2500
+ }
2501
+ get value() {
2502
+ return this.structure.value;
2503
+ }
2504
+ get keyInParent() {
2505
+ return this.structure.keyInParent;
2506
+ }
2507
+ get errors() {
2508
+ return this.validationState.errors;
2509
+ }
2510
+ get errorSummary() {
2511
+ return this.validationState.errorSummary;
2512
+ }
2513
+ get pending() {
2514
+ return this.validationState.pending;
2515
+ }
2516
+ get valid() {
2517
+ return this.validationState.valid;
2518
+ }
2519
+ get invalid() {
2520
+ return this.validationState.invalid;
2521
+ }
2522
+ get dirty() {
2523
+ return this.nodeState.dirty;
2524
+ }
2525
+ get touched() {
2526
+ return this.nodeState.touched;
2527
+ }
2528
+ get disabled() {
2529
+ return this.nodeState.disabled;
2530
+ }
2531
+ get disabledReasons() {
2532
+ return this.nodeState.disabledReasons;
2533
+ }
2534
+ get hidden() {
2535
+ return this.nodeState.hidden;
2536
+ }
2537
+ get readonly() {
2538
+ return this.nodeState.readonly;
2539
+ }
2540
+ get controls() {
2541
+ return this.nodeState.controls;
2542
+ }
2543
+ get submitting() {
2544
+ return this.submitState.submitting;
2545
+ }
2546
+ get name() {
2547
+ return this.nodeState.name;
2548
+ }
2549
+ property(prop) {
2550
+ return this.propertyState.get(prop);
2551
+ }
2552
+ hasProperty(prop) {
2553
+ return this.propertyState.has(prop);
2554
+ }
2555
+ /**
2556
+ * Marks this specific field as touched.
2557
+ */
2558
+ markAsTouched() {
2559
+ this.nodeState.markAsTouched();
2560
+ }
2561
+ /**
2562
+ * Marks this specific field as dirty.
2563
+ */
2564
+ markAsDirty() {
2565
+ this.nodeState.markAsDirty();
2566
+ }
2567
+ /**
2568
+ * Resets the {@link touched} and {@link dirty} state of the field and its descendants.
2569
+ *
2570
+ * Note this does not change the data model, which can be reset directly if desired.
2571
+ */
2572
+ reset() {
2573
+ this.nodeState.markAsUntouched();
2574
+ this.nodeState.markAsPristine();
2575
+ for (const child of this.structure.children()) {
2576
+ child.reset();
2577
+ }
2578
+ }
2579
+ /**
2580
+ * Creates a new root field node for a new form.
2581
+ */
2582
+ static newRoot(fieldManager, value, pathNode, adapter) {
2583
+ return adapter.newRoot(fieldManager, value, pathNode, adapter);
2584
+ }
2585
+ /**
2586
+ * Creates a child field node based on the given options.
2587
+ */
2588
+ static newChild(options) {
2589
+ return options.fieldAdapter.newChild(options);
2590
+ }
2591
+ createStructure(options) {
2592
+ return options.kind === 'root'
2593
+ ? new RootFieldNodeStructure(this, options.pathNode, options.logic, options.fieldManager, options.value, options.fieldAdapter, FieldNode.newChild)
2594
+ : new ChildFieldNodeStructure(this, options.pathNode, options.logic, options.parent, options.identityInParent, options.initialKeyInParent, options.fieldAdapter, FieldNode.newChild);
2595
+ }
2596
+ }
2597
+
2598
+ /**
2599
+ * The non-validation and non-submit state associated with a `FieldNode`, such as touched and dirty
2600
+ * status, as well as derived logical state.
2601
+ */
2602
+ class FieldNodeState {
2603
+ node;
2604
+ /**
2605
+ * Indicates whether this field has been touched directly by the user (as opposed to indirectly by
2606
+ * touching a child field).
2607
+ *
2608
+ * A field is considered directly touched when a user stops editing it for the first time (i.e. on blur)
2609
+ */
2610
+ selfTouched = signal(false, ...(ngDevMode ? [{ debugName: "selfTouched" }] : []));
2611
+ /**
2612
+ * Indicates whether this field has been dirtied directly by the user (as opposed to indirectly by
2613
+ * dirtying a child field).
2614
+ *
2615
+ * A field is considered directly dirtied if a user changed the value of the field at least once.
2616
+ */
2617
+ selfDirty = signal(false, ...(ngDevMode ? [{ debugName: "selfDirty" }] : []));
2618
+ /**
2619
+ * Marks this specific field as touched.
2620
+ */
2621
+ markAsTouched() {
2622
+ // TODO: should this be noop for fields that are hidden/disabled/readonly
2623
+ this.selfTouched.set(true);
2624
+ }
2625
+ /**
2626
+ * Marks this specific field as dirty.
2627
+ */
2628
+ markAsDirty() {
2629
+ // TODO: should this be noop for fields that are hidden/disabled/readonly
2630
+ this.selfDirty.set(true);
2631
+ }
2632
+ /**
2633
+ * Marks this specific field as not dirty.
2634
+ */
2635
+ markAsPristine() {
2636
+ this.selfDirty.set(false);
2637
+ }
2638
+ /**
2639
+ * Marks this specific field as not touched.
2640
+ */
2641
+ markAsUntouched() {
2642
+ this.selfTouched.set(false);
2643
+ }
2644
+ /** The UI controls the field is currently bound to. */
2645
+ controls = signal([], ...(ngDevMode ? [{ debugName: "controls" }] : []));
2646
+ constructor(node) {
2647
+ this.node = node;
2648
+ }
2649
+ /**
2650
+ * Whether this field is considered dirty.
2651
+ *
2652
+ * A field is considered dirty if one of the following is true:
2653
+ * - It was directly dirtied
2654
+ * - One of its children is considered dirty
2655
+ */
2656
+ dirty = computed(() => {
2657
+ return reduceChildren(this.node, this.selfDirty(), (child, value) => value || child.nodeState.dirty(), shortCircuitTrue);
2658
+ }, ...(ngDevMode ? [{ debugName: "dirty" }] : []));
2659
+ /**
2660
+ * Whether this field is considered touched.
2661
+ *
2662
+ * A field is considered touched if one of the following is true:
2663
+ * - It was directly touched
2664
+ * - One of its children is considered touched
2665
+ */
2666
+ touched = computed(() => reduceChildren(this.node, this.selfTouched(), (child, value) => value || child.nodeState.touched(), shortCircuitTrue), ...(ngDevMode ? [{ debugName: "touched" }] : []));
2667
+ /**
2668
+ * The reasons for this field's disablement. This includes disabled reasons for any parent field
2669
+ * that may have been disabled, indirectly causing this field to be disabled as well.
2670
+ * The `field` property of the `DisabledReason` can be used to determine which field ultimately
2671
+ * caused the disablement.
2672
+ */
2673
+ disabledReasons = computed(() => [
2674
+ ...(this.node.structure.parent?.nodeState.disabledReasons() ?? []),
2675
+ ...this.node.logicNode.logic.disabledReasons.compute(this.node.context),
2676
+ ], ...(ngDevMode ? [{ debugName: "disabledReasons" }] : []));
2677
+ /**
2678
+ * Whether this field is considered disabled.
2679
+ *
2680
+ * A field is considered disabled if one of the following is true:
2681
+ * - The schema contains logic that directly disabled it
2682
+ * - Its parent field is considered disabled
2683
+ */
2684
+ disabled = computed(() => !!this.disabledReasons().length, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
2685
+ /**
2686
+ * Whether this field is considered readonly.
2687
+ *
2688
+ * A field is considered readonly if one of the following is true:
2689
+ * - The schema contains logic that directly made it readonly
2690
+ * - Its parent field is considered readonly
2691
+ */
2692
+ readonly = computed(() => (this.node.structure.parent?.nodeState.readonly() ||
2693
+ this.node.logicNode.logic.readonly.compute(this.node.context)) ??
2694
+ false, ...(ngDevMode ? [{ debugName: "readonly" }] : []));
2695
+ /**
2696
+ * Whether this field is considered hidden.
2697
+ *
2698
+ * A field is considered hidden if one of the following is true:
2699
+ * - The schema contains logic that directly hides it
2700
+ * - Its parent field is considered hidden
2701
+ */
2702
+ hidden = computed(() => (this.node.structure.parent?.nodeState.hidden() ||
2703
+ this.node.logicNode.logic.hidden.compute(this.node.context)) ??
2704
+ false, ...(ngDevMode ? [{ debugName: "hidden" }] : []));
2705
+ name = computed(() => {
2706
+ const parent = this.node.structure.parent;
2707
+ if (!parent) {
2708
+ return this.node.structure.fieldManager.rootName;
2709
+ }
2710
+ return `${parent.name()}.${this.node.structure.keyInParent()}`;
2711
+ }, ...(ngDevMode ? [{ debugName: "name" }] : []));
2712
+ }
2713
+
2714
+ /**
2715
+ * Basic adapter supporting standard form behavior.
2716
+ */
2717
+ class BasicFieldAdapter {
2718
+ /**
2719
+ * Creates a new Root field node.
2720
+ * @param fieldManager
2721
+ * @param value
2722
+ * @param pathNode
2723
+ * @param adapter
2724
+ */
2725
+ newRoot(fieldManager, value, pathNode, adapter) {
2726
+ return new FieldNode({
2727
+ kind: 'root',
2728
+ fieldManager,
2729
+ value,
2730
+ pathNode,
2731
+ logic: pathNode.logic.build(),
2732
+ fieldAdapter: adapter,
2733
+ });
2734
+ }
2735
+ /**
2736
+ * Creates a new child field node.
2737
+ * @param options
2738
+ */
2739
+ newChild(options) {
2740
+ return new FieldNode(options);
2741
+ }
2742
+ /**
2743
+ * Creates a node state.
2744
+ * @param node
2745
+ */
2746
+ createNodeState(node) {
2747
+ return new FieldNodeState(node);
2748
+ }
2749
+ /**
2750
+ * Creates a validation state.
2751
+ * @param node
2752
+ */
2753
+ createValidationState(node) {
2754
+ return new FieldValidationState(node);
2755
+ }
2756
+ /**
2757
+ * Creates a node structure.
2758
+ * @param node
2759
+ * @param options
2760
+ */
2761
+ createStructure(node, options) {
2762
+ return node.createStructure(options);
2763
+ }
2764
+ }
2765
+
2766
+ /**
2767
+ * Manages the collection of fields associated with a given `form`.
2768
+ *
2769
+ * Fields are created implicitly, through reactivity, and may create "owned" entities like effects
2770
+ * or resources. When a field is no longer connected to the form, these owned entities should be
2771
+ * destroyed, which is the job of the `FormFieldManager`.
2772
+ */
2773
+ class FormFieldManager {
2774
+ injector;
2775
+ rootName;
2776
+ constructor(injector, rootName) {
2777
+ this.injector = injector;
2778
+ this.rootName = rootName ?? `${this.injector.get(APP_ID)}.form${nextFormId++}`;
2779
+ }
2780
+ /**
2781
+ * Contains all child field structures that have been created as part of the current form.
2782
+ * New child structures are automatically added when they are created.
2783
+ * Structures are destroyed and removed when they are no longer reachable from the root.
2784
+ */
2785
+ structures = new Set();
2786
+ /**
2787
+ * Creates an effect that runs when the form's structure changes and checks for structures that
2788
+ * have become unreachable to clean up.
2789
+ *
2790
+ * For example, consider a form wrapped around the following model: `signal([0, 1, 2])`.
2791
+ * This form would have 4 nodes as part of its structure tree.
2792
+ * One structure for the root array, and one structure for each element of the array.
2793
+ * Now imagine the data is updated: `model.set([0])`. In this case the structure for the first
2794
+ * element can still be reached from the root, but the structures for the second and third
2795
+ * elements are now orphaned and not connected to the root. Thus they will be destroyed.
2796
+ *
2797
+ * @param root The root field structure.
2798
+ */
2799
+ createFieldManagementEffect(root) {
2800
+ effect(() => {
2801
+ const liveStructures = new Set();
2802
+ this.markStructuresLive(root, liveStructures);
2803
+ // Destroy all nodes that are no longer live.
2804
+ for (const structure of this.structures) {
2805
+ if (!liveStructures.has(structure)) {
2806
+ this.structures.delete(structure);
2807
+ untracked(() => structure.destroy());
2808
+ }
2809
+ }
2810
+ }, { injector: this.injector });
2811
+ }
2812
+ /**
2813
+ * Collects all structures reachable from the given structure into the given set.
2814
+ *
2815
+ * @param structure The root structure
2816
+ * @param liveStructures The set of reachable structures to populate
2817
+ */
2818
+ markStructuresLive(structure, liveStructures) {
2819
+ liveStructures.add(structure);
2820
+ for (const child of structure.children()) {
2821
+ this.markStructuresLive(child.structure, liveStructures);
2822
+ }
2823
+ }
2824
+ }
2825
+ let nextFormId = 0;
2826
+
2827
+ /** Extracts the model, schema, and options from the arguments passed to `form()`. */
2828
+ function normalizeFormArgs(args) {
2829
+ let model;
2830
+ let schema;
2831
+ let options;
2832
+ if (args.length === 3) {
2833
+ [model, schema, options] = args;
2834
+ }
2835
+ else if (args.length === 2) {
2836
+ if (isSchemaOrSchemaFn(args[1])) {
2837
+ [model, schema] = args;
2838
+ }
2839
+ else {
2840
+ [model, options] = args;
2841
+ }
2842
+ }
2843
+ else {
2844
+ [model] = args;
2845
+ }
2846
+ return [model, schema, options];
2847
+ }
2848
+ function form(...args) {
2849
+ const [model, schema, options] = normalizeFormArgs(args);
2850
+ const injector = options?.injector ?? inject(Injector);
2851
+ const pathNode = runInInjectionContext(injector, () => SchemaImpl.rootCompile(schema));
2852
+ const fieldManager = new FormFieldManager(injector, options?.name);
2853
+ const adapter = options?.adapter ?? new BasicFieldAdapter();
2854
+ const fieldRoot = FieldNode.newRoot(fieldManager, model, pathNode, adapter);
2855
+ fieldManager.createFieldManagementEffect(fieldRoot.structure);
2856
+ return fieldRoot.fieldProxy;
2857
+ }
2858
+ /**
2859
+ * Applies a schema to each item of an array.
2860
+ *
2861
+ * @example
2862
+ * ```
2863
+ * const nameSchema = schema<{first: string, last: string}>((name) => {
2864
+ * required(name.first);
2865
+ * required(name.last);
2866
+ * });
2867
+ * const namesForm = form(signal([{first: '', last: ''}]), (names) => {
2868
+ * applyEach(names, nameSchema);
2869
+ * });
2870
+ * ```
2871
+ *
2872
+ * When binding logic to the array items, the `Field` for the array item is passed as an additional
2873
+ * argument. This can be used to reference other properties on the item.
2874
+ *
2875
+ * @example
2876
+ * ```
2877
+ * const namesForm = form(signal([{first: '', last: ''}]), (names) => {
2878
+ * applyEach(names, (name) => {
2879
+ * error(
2880
+ * name.last,
2881
+ * (value, nameField) => value === nameField.first().value(),
2882
+ * 'Last name must be different than first name',
2883
+ * );
2884
+ * });
2885
+ * });
2886
+ * ```
2887
+ *
2888
+ * @param path The target path for an array field whose items the schema will be applied to.
2889
+ * @param schema A schema for an element of the array, or function that binds logic to an
2890
+ * element of the array.
2891
+ * @template TValue The data type of the item field to apply the schema to.
2892
+ *
2893
+ * @experimental 21.0.0
2894
+ */
2895
+ function applyEach(path, schema) {
2896
+ assertPathIsCurrent(path);
2897
+ const elementPath = FieldPathNode.unwrapFieldPath(path).element.fieldPathProxy;
2898
+ apply(elementPath, schema);
2899
+ }
2900
+ /**
2901
+ * Applies a predefined schema to a given `FieldPath`.
2902
+ *
2903
+ * @example
2904
+ * ```
2905
+ * const nameSchema = schema<{first: string, last: string}>((name) => {
2906
+ * required(name.first);
2907
+ * required(name.last);
2908
+ * });
2909
+ * const profileForm = form(signal({name: {first: '', last: ''}, age: 0}), (profile) => {
2910
+ * apply(profile.name, nameSchema);
2911
+ * });
2912
+ * ```
2913
+ *
2914
+ * @param path The target path to apply the schema to.
2915
+ * @param schema The schema to apply to the property
2916
+ * @template TValue The data type of the field to apply the schema to.
2917
+ *
2918
+ * @experimental 21.0.0
2919
+ */
2920
+ function apply(path, schema) {
2921
+ assertPathIsCurrent(path);
2922
+ const pathNode = FieldPathNode.unwrapFieldPath(path);
2923
+ pathNode.mergeIn(SchemaImpl.create(schema));
2924
+ }
2925
+ /**
2926
+ * Conditionally applies a predefined schema to a given `FieldPath`.
2927
+ *
2928
+ * @param path The target path to apply the schema to.
2929
+ * @param logic A `LogicFn<T, boolean>` that returns `true` when the schema should be applied.
2930
+ * @param schema The schema to apply to the field when the `logic` function returns `true`.
2931
+ * @template TValue The data type of the field to apply the schema to.
2932
+ *
2933
+ * @experimental 21.0.0
2934
+ */
2935
+ function applyWhen(path, logic, schema) {
2936
+ assertPathIsCurrent(path);
2937
+ const pathNode = FieldPathNode.unwrapFieldPath(path);
2938
+ pathNode.mergeIn(SchemaImpl.create(schema), { fn: logic, path });
2939
+ }
2940
+ function applyWhenValue(path, predicate, schema) {
2941
+ applyWhen(path, ({ value }) => predicate(value()), schema);
2942
+ }
2943
+ /**
2944
+ * Submits a given `Field` using the given action function and applies any server errors resulting
2945
+ * from the action to the field. Server errors returned by the `action` will be integrated into the
2946
+ * field as a `ValidationError` on the sub-field indicated by the `field` property of the server
2947
+ * error.
2948
+ *
2949
+ * @example
2950
+ * ```
2951
+ * async function registerNewUser(registrationForm: Field<{username: string, password: string}>) {
2952
+ * const result = await myClient.registerNewUser(registrationForm().value());
2953
+ * if (result.errorCode === myClient.ErrorCode.USERNAME_TAKEN) {
2954
+ * return [{
2955
+ * field: registrationForm.username,
2956
+ * error: {kind: 'server', message: 'Username already taken'}
2957
+ * }];
2958
+ * }
2959
+ * return undefined;
2960
+ * }
2961
+ *
2962
+ * const registrationForm = form(signal({username: 'god', password: ''}));
2963
+ * submit(registrationForm, async (f) => {
2964
+ * return registerNewUser(registrationForm);
2965
+ * });
2966
+ * registrationForm.username().errors(); // [{kind: 'server', message: 'Username already taken'}]
2967
+ * ```
2968
+ *
2969
+ * @param form The field to submit.
2970
+ * @param action An asynchronous action used to submit the field. The action may return server
2971
+ * errors.
2972
+ * @template TValue The data type of the field being submitted.
2973
+ *
2974
+ * @experimental 21.0.0
2975
+ */
2976
+ async function submit(form, action) {
2977
+ const node = form();
2978
+ markAllAsTouched(node);
2979
+ // Fail fast if the form is already invalid.
2980
+ if (node.invalid()) {
2981
+ return;
2982
+ }
2983
+ node.submitState.selfSubmitting.set(true);
2984
+ try {
2985
+ const errors = await action(form);
2986
+ errors && setServerErrors(node, errors);
2987
+ }
2988
+ finally {
2989
+ node.submitState.selfSubmitting.set(false);
2990
+ }
2991
+ }
2992
+ /**
2993
+ * Sets a list of server errors to their individual fields.
2994
+ *
2995
+ * @param submittedField The field that was submitted, resulting in the errors.
2996
+ * @param errors The errors to set.
2997
+ */
2998
+ function setServerErrors(submittedField, errors) {
2999
+ if (!isArray(errors)) {
3000
+ errors = [errors];
3001
+ }
3002
+ const errorsByField = new Map();
3003
+ for (const error of errors) {
3004
+ const errorWithField = addDefaultField(error, submittedField.fieldProxy);
3005
+ const field = errorWithField.field();
3006
+ let fieldErrors = errorsByField.get(field);
3007
+ if (!fieldErrors) {
3008
+ fieldErrors = [];
3009
+ errorsByField.set(field, fieldErrors);
3010
+ }
3011
+ fieldErrors.push(errorWithField);
3012
+ }
3013
+ for (const [field, fieldErrors] of errorsByField) {
3014
+ field.submitState.serverErrors.set(fieldErrors);
3015
+ }
3016
+ }
3017
+ /**
3018
+ * Creates a `Schema` that adds logic rules to a form.
3019
+ * @param fn A **non-reactive** function that sets up reactive logic rules for the form.
3020
+ * @returns A schema object that implements the given logic.
3021
+ * @template TValue The value type of a `Field` that this schema binds to.
3022
+ *
3023
+ * @experimental 21.0.0
3024
+ */
3025
+ function schema(fn) {
3026
+ return SchemaImpl.create(fn);
3027
+ }
3028
+ /** Marks a {@link node} and its descendants as touched. */
3029
+ function markAllAsTouched(node) {
3030
+ node.markAsTouched();
3031
+ for (const child of node.structure.children()) {
3032
+ markAllAsTouched(child);
3033
+ }
3034
+ }
3035
+
3036
+ function requiredError(options) {
3037
+ return new RequiredValidationError(options);
3038
+ }
3039
+ function minError(min, options) {
3040
+ return new MinValidationError(min, options);
3041
+ }
3042
+ function maxError(max, options) {
3043
+ return new MaxValidationError(max, options);
3044
+ }
3045
+ function minLengthError(minLength, options) {
3046
+ return new MinLengthValidationError(minLength, options);
3047
+ }
3048
+ function maxLengthError(maxLength, options) {
3049
+ return new MaxLengthValidationError(maxLength, options);
3050
+ }
3051
+ function patternError(pattern, options) {
3052
+ return new PatternValidationError(pattern, options);
3053
+ }
3054
+ function emailError(options) {
3055
+ return new EmailValidationError(options);
3056
+ }
3057
+ function standardSchemaError(issue, options) {
3058
+ return new StandardSchemaValidationError(issue, options);
3059
+ }
3060
+ function customError(obj) {
3061
+ return new CustomValidationError(obj);
3062
+ }
3063
+ /**
3064
+ * A custom error that may contain additional properties
3065
+ *
3066
+ * @experimental 21.0.0
3067
+ */
3068
+ class CustomValidationError {
3069
+ /** Brand the class to avoid Typescript structural matching */
3070
+ __brand = undefined;
3071
+ /** Identifies the kind of error. */
3072
+ kind = '';
3073
+ /** The field associated with this error. */
3074
+ field;
3075
+ /** Human readable error message. */
3076
+ message;
3077
+ constructor(options) {
3078
+ if (options) {
3079
+ Object.assign(this, options);
3080
+ }
3081
+ }
3082
+ }
3083
+ /**
3084
+ * Internal version of `NgValidationError`, we create this separately so we can change its type on
3085
+ * the exported version to a type union of the possible sub-classes.
3086
+ *
3087
+ * @experimental 21.0.0
3088
+ */
3089
+ class _NgValidationError {
3090
+ /** Brand the class to avoid Typescript structural matching */
3091
+ __brand = undefined;
3092
+ /** Identifies the kind of error. */
3093
+ kind = '';
3094
+ /** The field associated with this error. */
3095
+ field;
3096
+ /** Human readable error message. */
3097
+ message;
3098
+ constructor(options) {
3099
+ if (options) {
3100
+ Object.assign(this, options);
3101
+ }
3102
+ }
3103
+ }
3104
+ /**
3105
+ * An error used to indicate that a required field is empty.
3106
+ *
3107
+ * @experimental 21.0.0
3108
+ */
3109
+ class RequiredValidationError extends _NgValidationError {
3110
+ kind = 'required';
3111
+ }
3112
+ /**
3113
+ * An error used to indicate that a value is lower than the minimum allowed.
3114
+ *
3115
+ * @experimental 21.0.0
3116
+ */
3117
+ class MinValidationError extends _NgValidationError {
3118
+ min;
3119
+ kind = 'min';
3120
+ constructor(min, options) {
3121
+ super(options);
3122
+ this.min = min;
3123
+ }
3124
+ }
3125
+ /**
3126
+ * An error used to indicate that a value is higher than the maximum allowed.
3127
+ *
3128
+ * @experimental 21.0.0
3129
+ */
3130
+ class MaxValidationError extends _NgValidationError {
3131
+ max;
3132
+ kind = 'max';
3133
+ constructor(max, options) {
3134
+ super(options);
3135
+ this.max = max;
3136
+ }
3137
+ }
3138
+ /**
3139
+ * An error used to indicate that a value is shorter than the minimum allowed length.
3140
+ *
3141
+ * @experimental 21.0.0
3142
+ */
3143
+ class MinLengthValidationError extends _NgValidationError {
3144
+ minLength;
3145
+ kind = 'minLength';
3146
+ constructor(minLength, options) {
3147
+ super(options);
3148
+ this.minLength = minLength;
3149
+ }
3150
+ }
3151
+ /**
3152
+ * An error used to indicate that a value is longer than the maximum allowed length.
3153
+ *
3154
+ * @experimental 21.0.0
3155
+ */
3156
+ class MaxLengthValidationError extends _NgValidationError {
3157
+ maxLength;
3158
+ kind = 'maxLength';
3159
+ constructor(maxLength, options) {
3160
+ super(options);
3161
+ this.maxLength = maxLength;
3162
+ }
3163
+ }
3164
+ /**
3165
+ * An error used to indicate that a value does not match the required pattern.
3166
+ *
3167
+ * @experimental 21.0.0
3168
+ */
3169
+ class PatternValidationError extends _NgValidationError {
3170
+ pattern;
3171
+ kind = 'pattern';
3172
+ constructor(pattern, options) {
3173
+ super(options);
3174
+ this.pattern = pattern;
3175
+ }
3176
+ }
3177
+ /**
3178
+ * An error used to indicate that a value is not a valid email.
3179
+ *
3180
+ * @experimental 21.0.0
3181
+ */
3182
+ class EmailValidationError extends _NgValidationError {
3183
+ kind = 'email';
3184
+ }
3185
+ /**
3186
+ * An error used to indicate an issue validating against a standard schema.
3187
+ *
3188
+ * @experimental 21.0.0
3189
+ */
3190
+ class StandardSchemaValidationError extends _NgValidationError {
3191
+ issue;
3192
+ kind = 'standardSchema';
3193
+ constructor(issue, options) {
3194
+ super(options);
3195
+ this.issue = issue;
3196
+ }
3197
+ }
3198
+ /**
3199
+ * The base class for all built-in, non-custom errors. This class can be used to check if an error
3200
+ * is one of the standard kinds, allowing you to switch on the kind to further narrow the type.
3201
+ *
3202
+ * @example
3203
+ * ```
3204
+ * const f = form(...);
3205
+ * for (const e of form().errors()) {
3206
+ * if (e instanceof NgValidationError) {
3207
+ * switch(e.kind) {
3208
+ * case 'required':
3209
+ * console.log('This is required!');
3210
+ * break;
3211
+ * case 'min':
3212
+ * console.log(`Must be at least ${e.min}`);
3213
+ * break;
3214
+ * ...
3215
+ * }
3216
+ * }
3217
+ * }
3218
+ * ```
3219
+ *
3220
+ * @experimental 21.0.0
3221
+ */
3222
+ const NgValidationError = _NgValidationError;
3223
+
3224
+ /** Gets the length or size of the given value. */
3225
+ function getLengthOrSize(value) {
3226
+ const v = value;
3227
+ return typeof v.length === 'number' ? v.length : v.size;
3228
+ }
3229
+ /**
3230
+ * Gets the value for an option that may be either a static value or a logic function that produces
3231
+ * the option value.
3232
+ *
3233
+ * @param opt The option from BaseValidatorConfig.
3234
+ * @param ctx The current FieldContext.
3235
+ * @returns The value for the option.
3236
+ */
3237
+ function getOption(opt, ctx) {
3238
+ return opt instanceof Function ? opt(ctx) : opt;
3239
+ }
3240
+ /**
3241
+ * Checks if the given value is considered empty. Empty values are: null, undefined, '', false, NaN.
3242
+ */
3243
+ function isEmpty(value) {
3244
+ if (typeof value === 'number') {
3245
+ return isNaN(value);
3246
+ }
3247
+ return value === '' || value === false || value == null;
3248
+ }
3249
+
3250
+ /**
3251
+ * A regular expression that matches valid e-mail addresses.
3252
+ *
3253
+ * At a high level, this regexp matches e-mail addresses of the format `local-part@tld`, where:
3254
+ * - `local-part` consists of one or more of the allowed characters (alphanumeric and some
3255
+ * punctuation symbols).
3256
+ * - `local-part` cannot begin or end with a period (`.`).
3257
+ * - `local-part` cannot be longer than 64 characters.
3258
+ * - `tld` consists of one or more `labels` separated by periods (`.`). For example `localhost` or
3259
+ * `foo.com`.
3260
+ * - A `label` consists of one or more of the allowed characters (alphanumeric, dashes (`-`) and
3261
+ * periods (`.`)).
3262
+ * - A `label` cannot begin or end with a dash (`-`) or a period (`.`).
3263
+ * - A `label` cannot be longer than 63 characters.
3264
+ * - The whole address cannot be longer than 254 characters.
3265
+ *
3266
+ * ## Implementation background
3267
+ *
3268
+ * This regexp was ported over from AngularJS (see there for git history):
3269
+ * https://github.com/angular/angular.js/blob/c133ef836/src/ng/directive/input.js#L27
3270
+ * It is based on the
3271
+ * [WHATWG version](https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address) with
3272
+ * some enhancements to incorporate more RFC rules (such as rules related to domain names and the
3273
+ * lengths of different parts of the address). The main differences from the WHATWG version are:
3274
+ * - Disallow `local-part` to begin or end with a period (`.`).
3275
+ * - Disallow `local-part` length to exceed 64 characters.
3276
+ * - Disallow total address length to exceed 254 characters.
3277
+ *
3278
+ * See [this commit](https://github.com/angular/angular.js/commit/f3f5cf72e) for more details.
3279
+ */
3280
+ const EMAIL_REGEXP = /^(?=.{1,254}$)(?=.{1,64}@)[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
3281
+ /**
3282
+ * Binds a validator to the given path that requires the value to match the standard email format.
3283
+ * This function can only be called on string paths.
3284
+ *
3285
+ * @param path Path of the field to validate
3286
+ * @param config Optional, allows providing any of the following options:
3287
+ * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.email()`
3288
+ * or a function that receives the `FieldContext` and returns custom validation error(s).
3289
+ * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
3290
+ *
3291
+ * @experimental 21.0.0
3292
+ */
3293
+ function email(path, config) {
3294
+ validate(path, (ctx) => {
3295
+ if (isEmpty(ctx.value())) {
3296
+ return undefined;
3297
+ }
3298
+ if (!EMAIL_REGEXP.test(ctx.value())) {
3299
+ if (config?.error) {
3300
+ return getOption(config.error, ctx);
3301
+ }
3302
+ else {
3303
+ return emailError({ message: getOption(config?.message, ctx) });
3304
+ }
3305
+ }
3306
+ return undefined;
3307
+ });
3308
+ }
3309
+
3310
+ /**
3311
+ * Binds a validator to the given path that requires the value to be less than or equal to the
3312
+ * given `maxValue`.
3313
+ * This function can only be called on number paths.
3314
+ * In addition to binding a validator, this function adds `MAX` property to the field.
3315
+ *
3316
+ * @param path Path of the field to validate
3317
+ * @param maxValue The maximum value, or a LogicFn that returns the maximum value.
3318
+ * @param config Optional, allows providing any of the following options:
3319
+ * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.max(maxValue)`
3320
+ * or a function that receives the `FieldContext` and returns custom validation error(s).
3321
+ * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
3322
+ *
3323
+ * @experimental 21.0.0
3324
+ */
3325
+ function max(path, maxValue, config) {
3326
+ const MAX_MEMO = property(path, (ctx) => computed(() => (typeof maxValue === 'number' ? maxValue : maxValue(ctx))));
3327
+ aggregateProperty(path, MAX, ({ state }) => state.property(MAX_MEMO)());
3328
+ validate(path, (ctx) => {
3329
+ if (isEmpty(ctx.value())) {
3330
+ return undefined;
3331
+ }
3332
+ const max = ctx.state.property(MAX_MEMO)();
3333
+ if (max === undefined || Number.isNaN(max)) {
3334
+ return undefined;
3335
+ }
3336
+ if (ctx.value() > max) {
3337
+ if (config?.error) {
3338
+ return getOption(config.error, ctx);
3339
+ }
3340
+ else {
3341
+ return maxError(max, { message: getOption(config?.message, ctx) });
3342
+ }
3343
+ }
3344
+ return undefined;
3345
+ });
3346
+ }
3347
+
3348
+ /**
3349
+ * Binds a validator to the given path that requires the length of the value to be less than or
3350
+ * equal to the given `maxLength`.
3351
+ * This function can only be called on string or array paths.
3352
+ * In addition to binding a validator, this function adds `MAX_LENGTH` property to the field.
3353
+ *
3354
+ * @param path Path of the field to validate
3355
+ * @param maxLength The maximum length, or a LogicFn that returns the maximum length.
3356
+ * @param config Optional, allows providing any of the following options:
3357
+ * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.maxLength(maxLength)`
3358
+ * or a function that receives the `FieldContext` and returns custom validation error(s).
3359
+ * @template TValue The type of value stored in the field the logic is bound to.
3360
+ * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
3361
+ *
3362
+ * @experimental 21.0.0
3363
+ */
3364
+ function maxLength(path, maxLength, config) {
3365
+ const MAX_LENGTH_MEMO = property(path, (ctx) => computed(() => (typeof maxLength === 'number' ? maxLength : maxLength(ctx))));
3366
+ aggregateProperty(path, MAX_LENGTH, ({ state }) => state.property(MAX_LENGTH_MEMO)());
3367
+ validate(path, (ctx) => {
3368
+ if (isEmpty(ctx.value())) {
3369
+ return undefined;
3370
+ }
3371
+ const maxLength = ctx.state.property(MAX_LENGTH_MEMO)();
3372
+ if (maxLength === undefined) {
3373
+ return undefined;
3374
+ }
3375
+ if (getLengthOrSize(ctx.value()) > maxLength) {
3376
+ if (config?.error) {
3377
+ return getOption(config.error, ctx);
3378
+ }
3379
+ else {
3380
+ return maxLengthError(maxLength, { message: getOption(config?.message, ctx) });
3381
+ }
3382
+ }
3383
+ return undefined;
3384
+ });
3385
+ }
3386
+
3387
+ /**
3388
+ * Binds a validator to the given path that requires the value to be greater than or equal to
3389
+ * the given `minValue`.
3390
+ * This function can only be called on number paths.
3391
+ * In addition to binding a validator, this function adds `MIN` property to the field.
3392
+ *
3393
+ * @param path Path of the field to validate
3394
+ * @param minValue The minimum value, or a LogicFn that returns the minimum value.
3395
+ * @param config Optional, allows providing any of the following options:
3396
+ * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.min(minValue)`
3397
+ * or a function that receives the `FieldContext` and returns custom validation error(s).
3398
+ * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
3399
+ *
3400
+ * @experimental 21.0.0
3401
+ */
3402
+ function min(path, minValue, config) {
3403
+ const MIN_MEMO = property(path, (ctx) => computed(() => (typeof minValue === 'number' ? minValue : minValue(ctx))));
3404
+ aggregateProperty(path, MIN, ({ state }) => state.property(MIN_MEMO)());
3405
+ validate(path, (ctx) => {
3406
+ if (isEmpty(ctx.value())) {
3407
+ return undefined;
3408
+ }
3409
+ const min = ctx.state.property(MIN_MEMO)();
3410
+ if (min === undefined || Number.isNaN(min)) {
3411
+ return undefined;
3412
+ }
3413
+ if (ctx.value() < min) {
3414
+ if (config?.error) {
3415
+ return getOption(config.error, ctx);
3416
+ }
3417
+ else {
3418
+ return minError(min, { message: getOption(config?.message, ctx) });
3419
+ }
3420
+ }
3421
+ return undefined;
3422
+ });
3423
+ }
3424
+
3425
+ /**
3426
+ * Binds a validator to the given path that requires the length of the value to be greater than or
3427
+ * equal to the given `minLength`.
3428
+ * This function can only be called on string or array paths.
3429
+ * In addition to binding a validator, this function adds `MIN_LENGTH` property to the field.
3430
+ *
3431
+ * @param path Path of the field to validate
3432
+ * @param minLength The minimum length, or a LogicFn that returns the minimum length.
3433
+ * @param config Optional, allows providing any of the following options:
3434
+ * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.minLength(minLength)`
3435
+ * or a function that receives the `FieldContext` and returns custom validation error(s).
3436
+ * @template TValue The type of value stored in the field the logic is bound to.
3437
+ * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
3438
+ *
3439
+ * @experimental 21.0.0
3440
+ */
3441
+ function minLength(path, minLength, config) {
3442
+ const MIN_LENGTH_MEMO = property(path, (ctx) => computed(() => (typeof minLength === 'number' ? minLength : minLength(ctx))));
3443
+ aggregateProperty(path, MIN_LENGTH, ({ state }) => state.property(MIN_LENGTH_MEMO)());
3444
+ validate(path, (ctx) => {
3445
+ if (isEmpty(ctx.value())) {
3446
+ return undefined;
3447
+ }
3448
+ const minLength = ctx.state.property(MIN_LENGTH_MEMO)();
3449
+ if (minLength === undefined) {
3450
+ return undefined;
3451
+ }
3452
+ if (getLengthOrSize(ctx.value()) < minLength) {
3453
+ if (config?.error) {
3454
+ return getOption(config.error, ctx);
3455
+ }
3456
+ else {
3457
+ return minLengthError(minLength, { message: getOption(config?.message, ctx) });
3458
+ }
3459
+ }
3460
+ return undefined;
3461
+ });
3462
+ }
3463
+
3464
+ /**
3465
+ * Binds a validator to the given path that requires the value to match a specific regex pattern.
3466
+ * This function can only be called on string paths.
3467
+ * In addition to binding a validator, this function adds `PATTERN` property to the field.
3468
+ *
3469
+ * @param path Path of the field to validate
3470
+ * @param pattern The RegExp pattern to match, or a LogicFn that returns the RegExp pattern.
3471
+ * @param config Optional, allows providing any of the following options:
3472
+ * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.pattern(pattern)`
3473
+ * or a function that receives the `FieldContext` and returns custom validation error(s).
3474
+ * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
3475
+ *
3476
+ * @experimental 21.0.0
3477
+ */
3478
+ function pattern(path, pattern, config) {
3479
+ const PATTERN_MEMO = property(path, (ctx) => computed(() => (pattern instanceof RegExp ? pattern : pattern(ctx))));
3480
+ aggregateProperty(path, PATTERN, ({ state }) => state.property(PATTERN_MEMO)());
3481
+ validate(path, (ctx) => {
3482
+ if (isEmpty(ctx.value())) {
3483
+ return undefined;
3484
+ }
3485
+ const pattern = ctx.state.property(PATTERN_MEMO)();
3486
+ if (pattern === undefined) {
3487
+ return undefined;
3488
+ }
3489
+ if (!pattern.test(ctx.value())) {
3490
+ if (config?.error) {
3491
+ return getOption(config.error, ctx);
3492
+ }
3493
+ else {
3494
+ return patternError(pattern, { message: getOption(config?.message, ctx) });
3495
+ }
3496
+ }
3497
+ return undefined;
3498
+ });
3499
+ }
3500
+
3501
+ /**
3502
+ * Binds a validator to the given path that requires the value to be non-empty.
3503
+ * This function can only be called on any type of path.
3504
+ * In addition to binding a validator, this function adds `REQUIRED` property to the field.
3505
+ *
3506
+ * @param path Path of the field to validate
3507
+ * @param config Optional, allows providing any of the following options:
3508
+ * - `message`: A user-facing message for the error.
3509
+ * - `error`: Custom validation error(s) to be used instead of the default `ValidationError.required()`
3510
+ * or a function that receives the `FieldContext` and returns custom validation error(s).
3511
+ * - `when`: A function that receives the `FieldContext` and returns true if the field is required
3512
+ * @template TValue The type of value stored in the field the logic is bound to.
3513
+ * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)
3514
+ *
3515
+ * @experimental 21.0.0
3516
+ */
3517
+ function required(path, config) {
3518
+ const REQUIRED_MEMO = property(path, (ctx) => computed(() => (config?.when ? config.when(ctx) : true)));
3519
+ aggregateProperty(path, REQUIRED, ({ state }) => state.property(REQUIRED_MEMO)());
3520
+ validate(path, (ctx) => {
3521
+ if (ctx.state.property(REQUIRED_MEMO)() && isEmpty(ctx.value())) {
3522
+ if (config?.error) {
3523
+ return getOption(config.error, ctx);
3524
+ }
3525
+ else {
3526
+ return requiredError({ message: getOption(config?.message, ctx) });
3527
+ }
3528
+ }
3529
+ return undefined;
3530
+ });
3531
+ }
3532
+
3533
+ /**
3534
+ * Validates a field using a `StandardSchemaV1` compatible validator (e.g. a Zod validator).
3535
+ *
3536
+ * See https://github.com/standard-schema/standard-schema for more about standard schema.
3537
+ *
3538
+ * @param path The `FieldPath` to the field to validate.
3539
+ * @param schema The standard schema compatible validator to use for validation.
3540
+ * @template TSchema The type validated by the schema. This may be either the full `TValue` type,
3541
+ * or a partial of it.
3542
+ * @template TValue The type of value stored in the field being validated.
3543
+ *
3544
+ * @experimental 21.0.0
3545
+ */
3546
+ function validateStandardSchema(path, schema) {
3547
+ // We create both a sync and async validator because the standard schema validator can return
3548
+ // either a sync result or a Promise, and we need to handle both cases. The sync validator
3549
+ // handles the sync result, and the async validator handles the Promise.
3550
+ // We memoize the result of the validation function here, so that it is only run once for both
3551
+ // validators, it can then be passed through both sync & async validation.
3552
+ const VALIDATOR_MEMO = property(path, ({ value }) => {
3553
+ return computed(() => schema['~standard'].validate(value()));
3554
+ });
3555
+ validateTree(path, ({ state, fieldOf }) => {
3556
+ // Skip sync validation if the result is a Promise.
3557
+ const result = state.property(VALIDATOR_MEMO)();
3558
+ if (_isPromise(result)) {
3559
+ return [];
3560
+ }
3561
+ return result.issues?.map((issue) => standardIssueToFormTreeError(fieldOf(path), issue)) ?? [];
3562
+ });
3563
+ validateAsync(path, {
3564
+ params: ({ state }) => {
3565
+ // Skip async validation if the result is *not* a Promise.
3566
+ const result = state.property(VALIDATOR_MEMO)();
3567
+ return _isPromise(result) ? result : undefined;
3568
+ },
3569
+ factory: (params) => {
3570
+ return resource({
3571
+ params,
3572
+ loader: async ({ params }) => (await params)?.issues ?? [],
3573
+ });
3574
+ },
3575
+ errors: (issues, { fieldOf }) => {
3576
+ return issues.map((issue) => standardIssueToFormTreeError(fieldOf(path), issue));
3577
+ },
3578
+ });
3579
+ }
3580
+ /**
3581
+ * Converts a `StandardSchemaV1.Issue` to a `FormTreeError`.
3582
+ *
3583
+ * @param field The root field to which the issue's path is relative.
3584
+ * @param issue The `StandardSchemaV1.Issue` to convert.
3585
+ * @returns A `ValidationError` representing the issue.
3586
+ */
3587
+ function standardIssueToFormTreeError(field, issue) {
3588
+ let target = field;
3589
+ for (const pathPart of issue.path ?? []) {
3590
+ const pathKey = typeof pathPart === 'object' ? pathPart.key : pathPart;
3591
+ target = target[pathKey];
3592
+ }
3593
+ return addDefaultField(standardSchemaError(issue), target);
3594
+ }
3595
+
3596
+ export { AggregateProperty, Control, CustomValidationError, EmailValidationError, MAX, MAX_LENGTH, MIN, MIN_LENGTH, MaxLengthValidationError, MaxValidationError, MinLengthValidationError, MinValidationError, NgValidationError, PATTERN, PatternValidationError, Property, REQUIRED, RequiredValidationError, StandardSchemaValidationError, aggregateProperty, andProperty, apply, applyEach, applyWhen, applyWhenValue, createProperty, customError, disabled, email, emailError, form, hidden, listProperty, max, maxError, maxLength, maxLengthError, maxProperty, min, minError, minLength, minLengthError, minProperty, orProperty, pattern, patternError, property, readonly, reducedProperty, required, requiredError, schema, standardSchemaError, submit, validate, validateAsync, validateHttp, validateStandardSchema, validateTree };
3597
+ //# sourceMappingURL=signals.mjs.map