@angular/forms 21.0.0-next.5 → 21.0.0-next.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/forms.mjs +129 -129
- package/fesm2022/forms.mjs.map +1 -1
- package/fesm2022/signals.mjs +57 -509
- package/fesm2022/signals.mjs.map +1 -1
- package/package.json +7 -7
- package/{index.d.ts → types/forms.d.ts} +4 -2
- package/{signals/index.d.ts → types/signals.d.ts} +78 -134
package/fesm2022/signals.mjs
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @license Angular v21.0.0-next.
|
|
3
|
-
* (c) 2010-2025 Google LLC. https://angular.
|
|
2
|
+
* @license Angular v21.0.0-next.7
|
|
3
|
+
* (c) 2010-2025 Google LLC. https://angular.dev/
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { httpResource } from '@angular/common/http';
|
|
8
8
|
import * as i0 from '@angular/core';
|
|
9
|
-
import { computed, untracked,
|
|
10
|
-
import { Validators, NG_VALUE_ACCESSOR, NgControl } from '@angular/forms';
|
|
9
|
+
import { computed, untracked, InjectionToken, inject, Injector, input, ɵCONTROL as _CONTROL, effect, Directive, runInInjectionContext, linkedSignal, signal, APP_ID, ɵisPromise as _isPromise, resource } from '@angular/core';
|
|
11
10
|
import { SIGNAL } from '@angular/core/primitives/signals';
|
|
12
11
|
|
|
13
12
|
/**
|
|
@@ -1426,212 +1425,37 @@ function validateHttp(path, opts) {
|
|
|
1426
1425
|
}
|
|
1427
1426
|
|
|
1428
1427
|
/**
|
|
1429
|
-
*
|
|
1430
|
-
* with a wider range of components designed to work with reactive forms, in particular ones that
|
|
1431
|
-
* inject the `NgControl`. The interop control does not implement *all* properties and methods of
|
|
1432
|
-
* the real `NgControl`, but does implement some of the most commonly used ones that have a clear
|
|
1433
|
-
* equivalent in signal forms.
|
|
1428
|
+
* Lightweight DI token provided by the {@link Control} directive.
|
|
1434
1429
|
*/
|
|
1435
|
-
|
|
1436
|
-
field;
|
|
1437
|
-
constructor(field) {
|
|
1438
|
-
this.field = field;
|
|
1439
|
-
}
|
|
1440
|
-
control = this;
|
|
1441
|
-
get value() {
|
|
1442
|
-
return this.field().value();
|
|
1443
|
-
}
|
|
1444
|
-
get valid() {
|
|
1445
|
-
return this.field().valid();
|
|
1446
|
-
}
|
|
1447
|
-
get invalid() {
|
|
1448
|
-
return this.field().invalid();
|
|
1449
|
-
}
|
|
1450
|
-
get pending() {
|
|
1451
|
-
return this.field().pending();
|
|
1452
|
-
}
|
|
1453
|
-
get disabled() {
|
|
1454
|
-
return this.field().disabled();
|
|
1455
|
-
}
|
|
1456
|
-
get enabled() {
|
|
1457
|
-
return !this.field().disabled();
|
|
1458
|
-
}
|
|
1459
|
-
get errors() {
|
|
1460
|
-
const errors = this.field().errors();
|
|
1461
|
-
if (errors.length === 0) {
|
|
1462
|
-
return null;
|
|
1463
|
-
}
|
|
1464
|
-
const errObj = {};
|
|
1465
|
-
for (const error of errors) {
|
|
1466
|
-
errObj[error.kind] = error;
|
|
1467
|
-
}
|
|
1468
|
-
return errObj;
|
|
1469
|
-
}
|
|
1470
|
-
get pristine() {
|
|
1471
|
-
return !this.field().dirty();
|
|
1472
|
-
}
|
|
1473
|
-
get dirty() {
|
|
1474
|
-
return this.field().dirty();
|
|
1475
|
-
}
|
|
1476
|
-
get touched() {
|
|
1477
|
-
return this.field().touched();
|
|
1478
|
-
}
|
|
1479
|
-
get untouched() {
|
|
1480
|
-
return !this.field().touched();
|
|
1481
|
-
}
|
|
1482
|
-
get status() {
|
|
1483
|
-
if (this.field().disabled()) {
|
|
1484
|
-
return 'DISABLED';
|
|
1485
|
-
}
|
|
1486
|
-
if (this.field().valid()) {
|
|
1487
|
-
return 'VALID';
|
|
1488
|
-
}
|
|
1489
|
-
if (this.field().invalid()) {
|
|
1490
|
-
return 'INVALID';
|
|
1491
|
-
}
|
|
1492
|
-
if (this.field().pending()) {
|
|
1493
|
-
return 'PENDING';
|
|
1494
|
-
}
|
|
1495
|
-
throw Error('AssertionError: unknown form control status');
|
|
1496
|
-
}
|
|
1497
|
-
valueAccessor = null;
|
|
1498
|
-
hasValidator(validator) {
|
|
1499
|
-
// This addresses a common case where users look for the presence of `Validators.required` to
|
|
1500
|
-
// determine whether or not to show a required "*" indicator in the UI.
|
|
1501
|
-
if (validator === Validators.required) {
|
|
1502
|
-
return this.field().property(REQUIRED)();
|
|
1503
|
-
}
|
|
1504
|
-
return false;
|
|
1505
|
-
}
|
|
1506
|
-
updateValueAndValidity() {
|
|
1507
|
-
// No-op since value and validity are always up to date in signal forms.
|
|
1508
|
-
// We offer this method so that reactive forms code attempting to call it doesn't error.
|
|
1509
|
-
}
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
// TODO: These utilities to be replaced with proper integration into framework.
|
|
1513
|
-
function privateGetComponentInstance(injector) {
|
|
1514
|
-
assertIsNodeInjector(injector);
|
|
1515
|
-
if (injector._tNode.directiveStart === 0 || injector._tNode.componentOffset === -1) {
|
|
1516
|
-
return undefined;
|
|
1517
|
-
}
|
|
1518
|
-
return injector._lView[injector._tNode.directiveStart + injector._tNode.componentOffset];
|
|
1519
|
-
}
|
|
1520
|
-
function privateSetComponentInput(inputSignal, value) {
|
|
1521
|
-
inputSignal[_SIGNAL].applyValueToInputSignal(inputSignal[_SIGNAL], value);
|
|
1522
|
-
}
|
|
1523
|
-
function privateIsSignalInput(value) {
|
|
1524
|
-
return isInputSignal(value);
|
|
1525
|
-
}
|
|
1526
|
-
function privateIsModelInput(value) {
|
|
1527
|
-
return isInputSignal(value) && isObject(value) && 'subscribe' in value;
|
|
1528
|
-
}
|
|
1529
|
-
function privateRunEffect(ref) {
|
|
1530
|
-
ref[_SIGNAL].run();
|
|
1531
|
-
}
|
|
1532
|
-
function assertIsNodeInjector(injector) {
|
|
1533
|
-
if (!('_tNode' in injector)) {
|
|
1534
|
-
throw new Error('Expected a Node Injector');
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
function isInputSignal(value) {
|
|
1538
|
-
if (!isObject(value) || !(_SIGNAL in value)) {
|
|
1539
|
-
return false;
|
|
1540
|
-
}
|
|
1541
|
-
const node = value[_SIGNAL];
|
|
1542
|
-
return isObject(node) && 'applyValueToInputSignal' in node;
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1430
|
+
const CONTROL = new InjectionToken(typeof ngDevMode !== undefined && ngDevMode ? 'CONTROL' : '');
|
|
1545
1431
|
/**
|
|
1546
|
-
* Binds a form `
|
|
1432
|
+
* Binds a form `FieldTree` to a UI control that edits it. A UI control can be one of several things:
|
|
1547
1433
|
* 1. A native HTML input or textarea
|
|
1548
1434
|
* 2. A signal forms custom control that implements `FormValueControl` or `FormCheckboxControl`
|
|
1549
|
-
* 3.
|
|
1435
|
+
* 3. TODO: https://github.com/orgs/angular/projects/60/views/1?pane=issue&itemId=131712274. A
|
|
1436
|
+
* component that provides a ControlValueAccessor. This should only be used to backwards
|
|
1550
1437
|
* compatibility with reactive forms. Prefer options (1) and (2).
|
|
1551
1438
|
*
|
|
1552
1439
|
* This directive has several responsibilities:
|
|
1553
1440
|
* 1. Two-way binds the field's value with the UI control's value
|
|
1554
1441
|
* 2. Binds additional forms related state on the field to the UI control (disabled, required, etc.)
|
|
1555
1442
|
* 3. Relays relevant events on the control to the field (e.g. marks field touched on blur)
|
|
1556
|
-
* 4.
|
|
1557
|
-
*
|
|
1558
|
-
*
|
|
1443
|
+
* 4. TODO: https://github.com/orgs/angular/projects/60/views/1?pane=issue&itemId=131712274.
|
|
1444
|
+
* Provides a fake `NgControl` that implements a subset of the features available on the
|
|
1445
|
+
* reactive forms `NgControl`. This is provided to improve interoperability with controls
|
|
1446
|
+
* designed to work with reactive forms. It should not be used by controls written for signal
|
|
1447
|
+
* forms.
|
|
1559
1448
|
*
|
|
1560
1449
|
* @category control
|
|
1561
1450
|
* @experimental 21.0.0
|
|
1562
1451
|
*/
|
|
1563
1452
|
class Control {
|
|
1564
|
-
/** The injector for this component. */
|
|
1565
1453
|
injector = inject(Injector);
|
|
1566
|
-
|
|
1567
|
-
/** Whether state synchronization with the field has been setup yet. */
|
|
1568
|
-
initialized = false;
|
|
1569
|
-
/** The field that is bound to this control. */
|
|
1570
|
-
field = signal(undefined, ...(ngDevMode ? [{ debugName: "field" }] : []));
|
|
1571
|
-
// If `[control]` is applied to a custom UI control, it wants to synchronize state in the field w/
|
|
1572
|
-
// the inputs of that custom control. This is difficult to do in user-land. We use `effect`, but
|
|
1573
|
-
// effects don't run before the lifecycle hooks of the component. This is usually okay, but has
|
|
1574
|
-
// one significant issue: the UI control's required inputs won't be set in time for those
|
|
1575
|
-
// lifecycle hooks to run.
|
|
1576
|
-
//
|
|
1577
|
-
// Eventually we can build custom functionality for the `Control` directive into the framework,
|
|
1578
|
-
// but for now we work around this limitation with a hack. We use an `@Input` instead of a
|
|
1579
|
-
// signal-based `input()` for the `[control]` to hook the exact moment inputs are being set,
|
|
1580
|
-
// before the important lifecycle hooks of the UI control. We can then initialize all our effects
|
|
1581
|
-
// and force them to run immediately, ensuring all required inputs have values.
|
|
1582
|
-
set _field(value) {
|
|
1583
|
-
this.field.set(value);
|
|
1584
|
-
if (!this.initialized) {
|
|
1585
|
-
this.initialize();
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
|
-
/** The field state of the bound field. */
|
|
1454
|
+
field = input.required(...(ngDevMode ? [{ debugName: "field", alias: 'control' }] : [{ alias: 'control' }]));
|
|
1589
1455
|
state = computed(() => this.field()(), ...(ngDevMode ? [{ debugName: "state" }] : []));
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
cvaArray = inject(NG_VALUE_ACCESSOR, { optional: true });
|
|
1594
|
-
/** The Cached value for the lazily created interop NgControl. */
|
|
1595
|
-
_ngControl;
|
|
1596
|
-
/** A fake NgControl provided for better interop with reactive forms. */
|
|
1597
|
-
get ngControl() {
|
|
1598
|
-
return (this._ngControl ??= new InteropNgControl(() => this.state()));
|
|
1599
|
-
}
|
|
1600
|
-
/** The ControlValueAccessor for the host component. */
|
|
1601
|
-
get cva() {
|
|
1602
|
-
return this.cvaArray?.[0] ?? this._ngControl?.valueAccessor ?? undefined;
|
|
1603
|
-
}
|
|
1604
|
-
/** Initializes state synchronization between the field and the host UI control. */
|
|
1605
|
-
initialize() {
|
|
1606
|
-
this.initialized = true;
|
|
1607
|
-
const injector = this.injector;
|
|
1608
|
-
const cmp = privateGetComponentInstance(injector);
|
|
1609
|
-
// If component has a `control` input, we assume that it will handle binding the field to the
|
|
1610
|
-
// appropriate native/custom control in its template, so we do not attempt to bind any inputs on
|
|
1611
|
-
// this component.
|
|
1612
|
-
if (cmp && isShadowedControlComponent(cmp)) {
|
|
1613
|
-
return;
|
|
1614
|
-
}
|
|
1615
|
-
if (cmp && isFormUiControl(cmp)) {
|
|
1616
|
-
// If we're binding to a component that follows the standard form ui control contract,
|
|
1617
|
-
// set up state synchronization based on the contract.
|
|
1618
|
-
this.setupCustomUiControl(cmp);
|
|
1619
|
-
}
|
|
1620
|
-
else if (this.cva !== undefined) {
|
|
1621
|
-
// If we're binding to a component that doesn't follow the standard contract, but provides a
|
|
1622
|
-
// control value accessor, set up state synchronization based on th CVA.
|
|
1623
|
-
this.setupControlValueAccessor(this.cva);
|
|
1624
|
-
}
|
|
1625
|
-
else if (this.el.nativeElement instanceof HTMLInputElement ||
|
|
1626
|
-
this.el.nativeElement instanceof HTMLTextAreaElement ||
|
|
1627
|
-
this.el.nativeElement instanceof HTMLSelectElement) {
|
|
1628
|
-
// If we're binding to a native html input, set up state synchronization with its native
|
|
1629
|
-
// properties / attributes.
|
|
1630
|
-
this.setupNativeInput(this.el.nativeElement);
|
|
1631
|
-
}
|
|
1632
|
-
else {
|
|
1633
|
-
throw new Error(`Unhandled control?`);
|
|
1634
|
-
}
|
|
1456
|
+
[_CONTROL] = undefined;
|
|
1457
|
+
// TODO: https://github.com/orgs/angular/projects/60/views/1?pane=issue&itemId=131861631
|
|
1458
|
+
register() {
|
|
1635
1459
|
// Register this control on the field it is currently bound to. We do this at the end of
|
|
1636
1460
|
// initialization so that it only runs if we are actually syncing with this control
|
|
1637
1461
|
// (as opposed to just passing the field through to its `control` input).
|
|
@@ -1643,308 +1467,13 @@ class Control {
|
|
|
1643
1467
|
});
|
|
1644
1468
|
}, { injector: this.injector });
|
|
1645
1469
|
}
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
*/
|
|
1649
|
-
setupNativeInput(input) {
|
|
1650
|
-
const inputType = input instanceof HTMLTextAreaElement
|
|
1651
|
-
? 'text'
|
|
1652
|
-
: input instanceof HTMLSelectElement
|
|
1653
|
-
? 'select'
|
|
1654
|
-
: input.type;
|
|
1655
|
-
input.addEventListener('input', () => {
|
|
1656
|
-
switch (inputType) {
|
|
1657
|
-
case 'checkbox':
|
|
1658
|
-
this.state().value.set(input.checked);
|
|
1659
|
-
break;
|
|
1660
|
-
case 'radio':
|
|
1661
|
-
// The `input` event only fires when a radio button becomes selected, so write its `value`
|
|
1662
|
-
// into the state.
|
|
1663
|
-
this.state().value.set(input.value);
|
|
1664
|
-
break;
|
|
1665
|
-
case 'number':
|
|
1666
|
-
case 'range':
|
|
1667
|
-
case 'datetime-local':
|
|
1668
|
-
// We can read a `number` or a `string` from this input type.
|
|
1669
|
-
// Prefer whichever is consistent with the current type.
|
|
1670
|
-
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
|
-
case 'date':
|
|
1678
|
-
case 'month':
|
|
1679
|
-
case 'week':
|
|
1680
|
-
case 'time':
|
|
1681
|
-
// We can read a `Date | null` or a `number` or a `string` from this input type.
|
|
1682
|
-
// Prefer whichever is consistent with the current type.
|
|
1683
|
-
if (isDateOrNull(this.state().value())) {
|
|
1684
|
-
this.state().value.set(input.valueAsDate);
|
|
1685
|
-
}
|
|
1686
|
-
else if (typeof this.state().value() === 'number') {
|
|
1687
|
-
this.state().value.set(input.valueAsNumber);
|
|
1688
|
-
}
|
|
1689
|
-
else {
|
|
1690
|
-
this.state().value.set(input.value);
|
|
1691
|
-
}
|
|
1692
|
-
break;
|
|
1693
|
-
default:
|
|
1694
|
-
this.state().value.set(input.value);
|
|
1695
|
-
break;
|
|
1696
|
-
}
|
|
1697
|
-
this.state().markAsDirty();
|
|
1698
|
-
});
|
|
1699
|
-
input.addEventListener('blur', () => this.state().markAsTouched());
|
|
1700
|
-
this.maybeSynchronize(() => this.state().readonly(), this.withBooleanAttribute(input, 'readonly'));
|
|
1701
|
-
// TODO: consider making a global configuration option for using aria-disabled instead.
|
|
1702
|
-
this.maybeSynchronize(() => this.state().disabled(), this.withBooleanAttribute(input, 'disabled'));
|
|
1703
|
-
this.maybeSynchronize(() => this.state().name(), this.withAttribute(input, 'name'));
|
|
1704
|
-
this.maybeSynchronize(this.propertySource(REQUIRED), this.withBooleanAttribute(input, 'required'));
|
|
1705
|
-
this.maybeSynchronize(this.propertySource(MIN), this.withAttribute(input, 'min'));
|
|
1706
|
-
this.maybeSynchronize(this.propertySource(MIN_LENGTH), this.withAttribute(input, 'minLength'));
|
|
1707
|
-
this.maybeSynchronize(this.propertySource(MAX), this.withAttribute(input, 'max'));
|
|
1708
|
-
this.maybeSynchronize(this.propertySource(MAX_LENGTH), this.withAttribute(input, 'maxLength'));
|
|
1709
|
-
switch (inputType) {
|
|
1710
|
-
case 'checkbox':
|
|
1711
|
-
this.maybeSynchronize(() => this.state().value(), (value) => (input.checked = value));
|
|
1712
|
-
break;
|
|
1713
|
-
case 'radio':
|
|
1714
|
-
this.maybeSynchronize(() => this.state().value(), (value) => {
|
|
1715
|
-
// Although HTML behavior is to clear the input already, we do this just in case.
|
|
1716
|
-
// It seems like it might be necessary in certain environments (e.g. Domino).
|
|
1717
|
-
input.checked = input.value === value;
|
|
1718
|
-
});
|
|
1719
|
-
break;
|
|
1720
|
-
case 'select':
|
|
1721
|
-
this.maybeSynchronize(() => this.state().value(), (value) => {
|
|
1722
|
-
// A select will not take a value unil the value's option has rendered.
|
|
1723
|
-
afterNextRender(() => (input.value = value), { injector: this.injector });
|
|
1724
|
-
});
|
|
1725
|
-
break;
|
|
1726
|
-
case 'number':
|
|
1727
|
-
case 'range':
|
|
1728
|
-
case 'datetime-local':
|
|
1729
|
-
// This input type can receive a `number` or a `string`.
|
|
1730
|
-
this.maybeSynchronize(() => this.state().value(), (value) => {
|
|
1731
|
-
if (typeof value === 'number') {
|
|
1732
|
-
input.valueAsNumber = value;
|
|
1733
|
-
}
|
|
1734
|
-
else {
|
|
1735
|
-
input.value = value;
|
|
1736
|
-
}
|
|
1737
|
-
});
|
|
1738
|
-
break;
|
|
1739
|
-
case 'date':
|
|
1740
|
-
case 'month':
|
|
1741
|
-
case 'week':
|
|
1742
|
-
case 'time':
|
|
1743
|
-
// This input type can receive a `Date | null` or a `number` or a `string`.
|
|
1744
|
-
this.maybeSynchronize(() => this.state().value(), (value) => {
|
|
1745
|
-
if (isDateOrNull(value)) {
|
|
1746
|
-
input.valueAsDate = value;
|
|
1747
|
-
}
|
|
1748
|
-
else if (typeof value === 'number') {
|
|
1749
|
-
input.valueAsNumber = value;
|
|
1750
|
-
}
|
|
1751
|
-
else {
|
|
1752
|
-
input.value = value;
|
|
1753
|
-
}
|
|
1754
|
-
});
|
|
1755
|
-
break;
|
|
1756
|
-
default:
|
|
1757
|
-
this.maybeSynchronize(() => this.state().value(), (value) => {
|
|
1758
|
-
input.value = value;
|
|
1759
|
-
});
|
|
1760
|
-
break;
|
|
1761
|
-
}
|
|
1762
|
-
}
|
|
1763
|
-
/** Set up state synchronization between the field and a ControlValueAccessor. */
|
|
1764
|
-
setupControlValueAccessor(cva) {
|
|
1765
|
-
cva.registerOnChange((value) => this.state().value.set(value));
|
|
1766
|
-
cva.registerOnTouched(() => this.state().markAsTouched());
|
|
1767
|
-
this.maybeSynchronize(() => this.state().value(), (value) => cva.writeValue(value));
|
|
1768
|
-
if (cva.setDisabledState) {
|
|
1769
|
-
this.maybeSynchronize(() => this.state().disabled(), (value) => cva.setDisabledState(value));
|
|
1770
|
-
}
|
|
1771
|
-
cva.writeValue(this.state().value());
|
|
1772
|
-
cva.setDisabledState?.(this.state().disabled());
|
|
1773
|
-
}
|
|
1774
|
-
/** Set up state synchronization between the field and a FormUiControl. */
|
|
1775
|
-
setupCustomUiControl(cmp) {
|
|
1776
|
-
// Handle the property side of the model binding. How we do this depends on the shape of the
|
|
1777
|
-
// component. There are 2 options:
|
|
1778
|
-
// * it provides a `value` model (most controls that edit a single value)
|
|
1779
|
-
// * it provides a `checked` model with no `value` signal (custom checkbox)
|
|
1780
|
-
let cleanupValue;
|
|
1781
|
-
if (isFormValueControl(cmp)) {
|
|
1782
|
-
// <custom-input [(value)]="state().value">
|
|
1783
|
-
this.maybeSynchronize(() => this.state().value(), withInput(cmp.value));
|
|
1784
|
-
cleanupValue = cmp.value.subscribe((newValue) => this.state().value.set(newValue));
|
|
1785
|
-
}
|
|
1786
|
-
else if (isFormCheckboxControl(cmp)) {
|
|
1787
|
-
// <custom-checkbox [(checked)]="state().value" />
|
|
1788
|
-
this.maybeSynchronize(() => this.state().value(), withInput(cmp.checked));
|
|
1789
|
-
cleanupValue = cmp.checked.subscribe((newValue) => this.state().value.set(newValue));
|
|
1790
|
-
}
|
|
1791
|
-
else {
|
|
1792
|
-
throw new Error(`Unknown custom control subtype`);
|
|
1793
|
-
}
|
|
1794
|
-
this.maybeSynchronize(() => this.state().name(), withInput(cmp.name));
|
|
1795
|
-
this.maybeSynchronize(() => this.state().disabled(), withInput(cmp.disabled));
|
|
1796
|
-
this.maybeSynchronize(() => this.state().disabledReasons(), withInput(cmp.disabledReasons));
|
|
1797
|
-
this.maybeSynchronize(() => this.state().readonly(), withInput(cmp.readonly));
|
|
1798
|
-
this.maybeSynchronize(() => this.state().hidden(), withInput(cmp.hidden));
|
|
1799
|
-
this.maybeSynchronize(() => this.state().errors(), withInput(cmp.errors));
|
|
1800
|
-
if (privateIsModelInput(cmp.touched) || privateIsSignalInput(cmp.touched)) {
|
|
1801
|
-
this.maybeSynchronize(() => this.state().touched(), withInput(cmp.touched));
|
|
1802
|
-
}
|
|
1803
|
-
this.maybeSynchronize(() => this.state().dirty(), withInput(cmp.dirty));
|
|
1804
|
-
this.maybeSynchronize(() => this.state().invalid(), withInput(cmp.invalid));
|
|
1805
|
-
this.maybeSynchronize(() => this.state().pending(), withInput(cmp.pending));
|
|
1806
|
-
this.maybeSynchronize(this.propertySource(REQUIRED), withInput(cmp.required));
|
|
1807
|
-
this.maybeSynchronize(this.propertySource(MIN), withInput(cmp.min));
|
|
1808
|
-
this.maybeSynchronize(this.propertySource(MIN_LENGTH), withInput(cmp.minLength));
|
|
1809
|
-
this.maybeSynchronize(this.propertySource(MAX), withInput(cmp.max));
|
|
1810
|
-
this.maybeSynchronize(this.propertySource(MAX_LENGTH), withInput(cmp.maxLength));
|
|
1811
|
-
this.maybeSynchronize(this.propertySource(PATTERN), withInput(cmp.pattern));
|
|
1812
|
-
let cleanupTouch;
|
|
1813
|
-
let cleanupDefaultTouch;
|
|
1814
|
-
if (privateIsModelInput(cmp.touched) || isOutputRef(cmp.touched)) {
|
|
1815
|
-
cleanupTouch = cmp.touched.subscribe(() => this.state().markAsTouched());
|
|
1816
|
-
}
|
|
1817
|
-
else {
|
|
1818
|
-
// If the component did not give us a touch event stream, use the standard touch logic,
|
|
1819
|
-
// marking it touched when the focus moves from inside the host element to outside.
|
|
1820
|
-
const listener = (event) => {
|
|
1821
|
-
const newActiveEl = event.relatedTarget;
|
|
1822
|
-
if (!this.el.nativeElement.contains(newActiveEl)) {
|
|
1823
|
-
this.state().markAsTouched();
|
|
1824
|
-
}
|
|
1825
|
-
};
|
|
1826
|
-
this.el.nativeElement.addEventListener('focusout', listener);
|
|
1827
|
-
cleanupDefaultTouch = () => this.el.nativeElement.removeEventListener('focusout', listener);
|
|
1828
|
-
}
|
|
1829
|
-
// Cleanup for output binding subscriptions:
|
|
1830
|
-
this.injector.get(DestroyRef).onDestroy(() => {
|
|
1831
|
-
cleanupValue?.unsubscribe();
|
|
1832
|
-
cleanupTouch?.unsubscribe();
|
|
1833
|
-
cleanupDefaultTouch?.();
|
|
1834
|
-
});
|
|
1835
|
-
}
|
|
1836
|
-
/** Synchronize a value from a reactive source to a given sink. */
|
|
1837
|
-
maybeSynchronize(source, sink) {
|
|
1838
|
-
if (!sink) {
|
|
1839
|
-
return undefined;
|
|
1840
|
-
}
|
|
1841
|
-
const ref = effect(() => {
|
|
1842
|
-
const value = source();
|
|
1843
|
-
untracked(() => sink(value));
|
|
1844
|
-
}, ...(ngDevMode ? [{ debugName: "ref", injector: this.injector }] : [{ injector: this.injector }]));
|
|
1845
|
-
// Run the effect immediately to ensure sinks which are required inputs are set before they can
|
|
1846
|
-
// be observed. See the note on `_field` for more details.
|
|
1847
|
-
privateRunEffect(ref);
|
|
1848
|
-
}
|
|
1849
|
-
/** Creates a reactive value source by reading the given AggregateProperty from the field. */
|
|
1850
|
-
propertySource(key) {
|
|
1851
|
-
const metaSource = computed(() => this.state().hasProperty(key) ? this.state().property(key) : key.getInitial, ...(ngDevMode ? [{ debugName: "metaSource" }] : []));
|
|
1852
|
-
return () => metaSource()?.();
|
|
1853
|
-
}
|
|
1854
|
-
/** Creates a (non-boolean) value sync that writes the given attribute of the given element. */
|
|
1855
|
-
withAttribute(element, attribute) {
|
|
1856
|
-
return (value) => {
|
|
1857
|
-
if (value !== undefined) {
|
|
1858
|
-
this.renderer.setAttribute(element, attribute, value.toString());
|
|
1859
|
-
}
|
|
1860
|
-
else {
|
|
1861
|
-
this.renderer.removeAttribute(element, attribute);
|
|
1862
|
-
}
|
|
1863
|
-
};
|
|
1864
|
-
}
|
|
1865
|
-
/** Creates a boolean value sync that writes the given attribute of the given element. */
|
|
1866
|
-
withBooleanAttribute(element, attribute) {
|
|
1867
|
-
return (value) => {
|
|
1868
|
-
if (value) {
|
|
1869
|
-
this.renderer.setAttribute(element, attribute, '');
|
|
1870
|
-
}
|
|
1871
|
-
else {
|
|
1872
|
-
this.renderer.removeAttribute(element, attribute);
|
|
1873
|
-
}
|
|
1874
|
-
};
|
|
1875
|
-
}
|
|
1876
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0-next.5", ngImport: i0, type: Control, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1877
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.0-next.5", type: Control, isStandalone: true, selector: "[control]", inputs: { _field: ["control", "_field"] }, providers: [
|
|
1878
|
-
{
|
|
1879
|
-
provide: NgControl,
|
|
1880
|
-
useFactory: () => inject(Control).ngControl,
|
|
1881
|
-
},
|
|
1882
|
-
], ngImport: i0 });
|
|
1470
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0-next.7", ngImport: i0, type: Control, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1471
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.0-next.7", type: Control, isStandalone: true, selector: "[control]", inputs: { field: { classPropertyName: "field", publicName: "control", isSignal: true, isRequired: true, transformFunction: null } }, providers: [{ provide: CONTROL, useExisting: Control }], ngImport: i0 });
|
|
1883
1472
|
}
|
|
1884
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.0-next.
|
|
1473
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.0-next.7", ngImport: i0, type: Control, decorators: [{
|
|
1885
1474
|
type: Directive,
|
|
1886
|
-
args: [{
|
|
1887
|
-
|
|
1888
|
-
providers: [
|
|
1889
|
-
{
|
|
1890
|
-
provide: NgControl,
|
|
1891
|
-
useFactory: () => inject(Control).ngControl,
|
|
1892
|
-
},
|
|
1893
|
-
],
|
|
1894
|
-
}]
|
|
1895
|
-
}], propDecorators: { _field: [{
|
|
1896
|
-
type: Input,
|
|
1897
|
-
args: [{ required: true, alias: 'control' }]
|
|
1898
|
-
}] } });
|
|
1899
|
-
/** Creates a value sync from an input signal. */
|
|
1900
|
-
function withInput(input) {
|
|
1901
|
-
return input ? (value) => privateSetComponentInput(input, value) : undefined;
|
|
1902
|
-
}
|
|
1903
|
-
/**
|
|
1904
|
-
* Checks whether the given component matches the contract for either FormValueControl or
|
|
1905
|
-
* FormCheckboxControl.
|
|
1906
|
-
*/
|
|
1907
|
-
function isFormUiControl(cmp) {
|
|
1908
|
-
const castCmp = cmp;
|
|
1909
|
-
return ((isFormValueControl(castCmp) || isFormCheckboxControl(castCmp)) &&
|
|
1910
|
-
(castCmp.readonly === undefined || privateIsSignalInput(castCmp.readonly)) &&
|
|
1911
|
-
(castCmp.disabled === undefined || privateIsSignalInput(castCmp.disabled)) &&
|
|
1912
|
-
(castCmp.disabledReasons === undefined || privateIsSignalInput(castCmp.disabledReasons)) &&
|
|
1913
|
-
(castCmp.errors === undefined || privateIsSignalInput(castCmp.errors)) &&
|
|
1914
|
-
(castCmp.invalid === undefined || privateIsSignalInput(castCmp.invalid)) &&
|
|
1915
|
-
(castCmp.pending === undefined || privateIsSignalInput(castCmp.pending)) &&
|
|
1916
|
-
(castCmp.touched === undefined ||
|
|
1917
|
-
privateIsModelInput(castCmp.touched) ||
|
|
1918
|
-
privateIsSignalInput(castCmp.touched) ||
|
|
1919
|
-
isOutputRef(castCmp.touched)) &&
|
|
1920
|
-
(castCmp.dirty === undefined || privateIsSignalInput(castCmp.dirty)) &&
|
|
1921
|
-
(castCmp.min === undefined || privateIsSignalInput(castCmp.min)) &&
|
|
1922
|
-
(castCmp.minLength === undefined || privateIsSignalInput(castCmp.minLength)) &&
|
|
1923
|
-
(castCmp.max === undefined || privateIsSignalInput(castCmp.max)) &&
|
|
1924
|
-
(castCmp.maxLength === undefined || privateIsSignalInput(castCmp.maxLength)));
|
|
1925
|
-
}
|
|
1926
|
-
/** Checks whether the given FormUiControl is a FormValueControl. */
|
|
1927
|
-
function isFormValueControl(cmp) {
|
|
1928
|
-
return privateIsModelInput(cmp.value);
|
|
1929
|
-
}
|
|
1930
|
-
/** Checks whether the given FormUiControl is a FormCheckboxControl. */
|
|
1931
|
-
function isFormCheckboxControl(cmp) {
|
|
1932
|
-
return (privateIsModelInput(cmp.checked) &&
|
|
1933
|
-
cmp.value === undefined);
|
|
1934
|
-
}
|
|
1935
|
-
/** Checks whether the given component has an input called `control`. */
|
|
1936
|
-
function isShadowedControlComponent(cmp) {
|
|
1937
|
-
const mirror = reflectComponentType(cmp.constructor);
|
|
1938
|
-
return mirror?.inputs.some((input) => input.templateName === 'control') ?? false;
|
|
1939
|
-
}
|
|
1940
|
-
/** Checks whether the given object is an output ref. */
|
|
1941
|
-
function isOutputRef(value) {
|
|
1942
|
-
return value instanceof OutputEmitterRef || value instanceof EventEmitter;
|
|
1943
|
-
}
|
|
1944
|
-
/** Checks if a given value is a Date or null */
|
|
1945
|
-
function isDateOrNull(value) {
|
|
1946
|
-
return value === null || value instanceof Date;
|
|
1947
|
-
}
|
|
1475
|
+
args: [{ selector: '[control]', providers: [{ provide: CONTROL, useExisting: Control }] }]
|
|
1476
|
+
}] });
|
|
1948
1477
|
|
|
1949
1478
|
/**
|
|
1950
1479
|
* `FieldContext` implementation, backed by a `FieldNode`.
|
|
@@ -2087,7 +1616,7 @@ class FieldPropertyState {
|
|
|
2087
1616
|
}
|
|
2088
1617
|
|
|
2089
1618
|
/**
|
|
2090
|
-
* Proxy handler which implements `
|
|
1619
|
+
* Proxy handler which implements `FieldTree<T>` on top of `FieldNode`.
|
|
2091
1620
|
*/
|
|
2092
1621
|
const FIELD_PROXY_HANDLER = {
|
|
2093
1622
|
get(getTgt, p) {
|
|
@@ -2095,8 +1624,8 @@ const FIELD_PROXY_HANDLER = {
|
|
|
2095
1624
|
// First, check whether the requested property is a defined child node of this node.
|
|
2096
1625
|
const child = tgt.structure.getChild(p);
|
|
2097
1626
|
if (child !== undefined) {
|
|
2098
|
-
// If so, return the child node's `
|
|
2099
|
-
// the form structure.
|
|
1627
|
+
// If so, return the child node's `FieldTree` proxy, allowing the developer to continue
|
|
1628
|
+
// navigating the form structure.
|
|
2100
1629
|
return child.fieldProxy;
|
|
2101
1630
|
}
|
|
2102
1631
|
// Otherwise, we need to consider whether the properties they're accessing are related to array
|
|
@@ -2461,10 +1990,11 @@ class FieldSubmitState {
|
|
|
2461
1990
|
serverErrors;
|
|
2462
1991
|
constructor(node) {
|
|
2463
1992
|
this.node = node;
|
|
2464
|
-
this.serverErrors = linkedSignal({
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
1993
|
+
this.serverErrors = linkedSignal(...(ngDevMode ? [{ debugName: "serverErrors", source: this.node.structure.value,
|
|
1994
|
+
computation: () => [] }] : [{
|
|
1995
|
+
source: this.node.structure.value,
|
|
1996
|
+
computation: () => [],
|
|
1997
|
+
}]));
|
|
2468
1998
|
}
|
|
2469
1999
|
/**
|
|
2470
2000
|
* Whether this form is currently in the process of being submitted.
|
|
@@ -2562,6 +2092,24 @@ class FieldNode {
|
|
|
2562
2092
|
get name() {
|
|
2563
2093
|
return this.nodeState.name;
|
|
2564
2094
|
}
|
|
2095
|
+
get max() {
|
|
2096
|
+
return this.property(MAX);
|
|
2097
|
+
}
|
|
2098
|
+
get maxLength() {
|
|
2099
|
+
return this.property(MAX_LENGTH);
|
|
2100
|
+
}
|
|
2101
|
+
get min() {
|
|
2102
|
+
return this.property(MIN);
|
|
2103
|
+
}
|
|
2104
|
+
get minLength() {
|
|
2105
|
+
return this.property(MIN_LENGTH);
|
|
2106
|
+
}
|
|
2107
|
+
get pattern() {
|
|
2108
|
+
return this.property(PATTERN);
|
|
2109
|
+
}
|
|
2110
|
+
get required() {
|
|
2111
|
+
return this.property(REQUIRED);
|
|
2112
|
+
}
|
|
2565
2113
|
property(prop) {
|
|
2566
2114
|
return this.propertyState.get(prop);
|
|
2567
2115
|
}
|
|
@@ -2895,8 +2443,8 @@ function form(...args) {
|
|
|
2895
2443
|
* });
|
|
2896
2444
|
* ```
|
|
2897
2445
|
*
|
|
2898
|
-
* When binding logic to the array items, the `
|
|
2899
|
-
* argument. This can be used to reference other properties on the item.
|
|
2446
|
+
* When binding logic to the array items, the `FieldTree` for the array item is passed as an
|
|
2447
|
+
* additional argument. This can be used to reference other properties on the item.
|
|
2900
2448
|
*
|
|
2901
2449
|
* @example
|
|
2902
2450
|
* ```
|
|
@@ -2970,14 +2518,14 @@ function applyWhenValue(path, predicate, schema) {
|
|
|
2970
2518
|
applyWhen(path, ({ value }) => predicate(value()), schema);
|
|
2971
2519
|
}
|
|
2972
2520
|
/**
|
|
2973
|
-
* Submits a given `
|
|
2974
|
-
* from the action to the field. Server errors returned by the `action` will be integrated
|
|
2975
|
-
* field as a `ValidationError` on the sub-field indicated by the `field` property of the
|
|
2976
|
-
* error.
|
|
2521
|
+
* Submits a given `FieldTree` using the given action function and applies any server errors
|
|
2522
|
+
* resulting from the action to the field. Server errors returned by the `action` will be integrated
|
|
2523
|
+
* into the field as a `ValidationError` on the sub-field indicated by the `field` property of the
|
|
2524
|
+
* server error.
|
|
2977
2525
|
*
|
|
2978
2526
|
* @example
|
|
2979
2527
|
* ```
|
|
2980
|
-
* async function registerNewUser(registrationForm:
|
|
2528
|
+
* async function registerNewUser(registrationForm: FieldTree<{username: string, password: string}>) {
|
|
2981
2529
|
* const result = await myClient.registerNewUser(registrationForm().value());
|
|
2982
2530
|
* if (result.errorCode === myClient.ErrorCode.USERNAME_TAKEN) {
|
|
2983
2531
|
* return [{
|
|
@@ -3048,7 +2596,7 @@ function setServerErrors(submittedField, errors) {
|
|
|
3048
2596
|
* Creates a `Schema` that adds logic rules to a form.
|
|
3049
2597
|
* @param fn A **non-reactive** function that sets up reactive logic rules for the form.
|
|
3050
2598
|
* @returns A schema object that implements the given logic.
|
|
3051
|
-
* @template TValue The value type of a `
|
|
2599
|
+
* @template TValue The value type of a `FieldTree` that this schema binds to.
|
|
3052
2600
|
*
|
|
3053
2601
|
* @category structure
|
|
3054
2602
|
* @experimental 21.0.0
|
|
@@ -3642,5 +3190,5 @@ function standardIssueToFormTreeError(field, issue) {
|
|
|
3642
3190
|
return addDefaultField(standardSchemaError(issue), target);
|
|
3643
3191
|
}
|
|
3644
3192
|
|
|
3645
|
-
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 };
|
|
3193
|
+
export { AggregateProperty, CONTROL, 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 };
|
|
3646
3194
|
//# sourceMappingURL=signals.mjs.map
|