ngxsmk-datepicker 2.0.2 → 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/docs/API.md +2 -2
- package/docs/signal-forms.md +10 -1
- package/fesm2022/ngxsmk-datepicker.mjs +20 -145
- package/package.json +1 -1
- package/types/ngxsmk-datepicker.d.ts +9 -34
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
**npm i ngxsmk-datepicker**
|
|
9
9
|
|
|
10
|
-
> **Stable Version**: `2.0.
|
|
10
|
+
> **Stable Version**: `2.0.3` is the current stable release. For production use, install the latest version from npm.
|
|
11
11
|
>
|
|
12
12
|
> ⚠️ **Warning**: Version `1.9.26` contains broken styles. If you are using `1.9.26`, please upgrade to `1.9.28` or downgrade to `1.9.25` immediately.
|
|
13
13
|
|
|
@@ -127,7 +127,7 @@ For details, see [CONTRIBUTING.md](https://github.com/NGXSMK/ngxsmk-datepicker/b
|
|
|
127
127
|
|
|
128
128
|
Install the package using npm:
|
|
129
129
|
|
|
130
|
-
npm install ngxsmk-datepicker
|
|
130
|
+
npm install ngxsmk-datepicker@2.0.3
|
|
131
131
|
|
|
132
132
|
## **Usage**
|
|
133
133
|
|
package/docs/API.md
CHANGED
|
@@ -2180,7 +2180,7 @@ type DateInput =
|
|
|
2180
2180
|
```
|
|
2181
2181
|
|
|
2182
2182
|
|
|
2183
|
-
### SignalFormField (v2.0.
|
|
2183
|
+
### SignalFormField (v2.0.3+)
|
|
2184
2184
|
|
|
2185
2185
|
**Status**: Stable
|
|
2186
2186
|
|
|
@@ -2190,7 +2190,7 @@ Type representing a signal-based form field.
|
|
|
2190
2190
|
type SignalFormField = any; // Compatible with Angular 21 FieldTree
|
|
2191
2191
|
```
|
|
2192
2192
|
|
|
2193
|
-
### SignalFormFieldConfig (v2.0.
|
|
2193
|
+
### SignalFormFieldConfig (v2.0.3+)
|
|
2194
2194
|
|
|
2195
2195
|
**Status**: Stable
|
|
2196
2196
|
|
package/docs/signal-forms.md
CHANGED
|
@@ -113,7 +113,7 @@ export class TwoWayComponent {
|
|
|
113
113
|
}
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
### Signal Field Resolution (v2.0.
|
|
116
|
+
### Signal Field Resolution (v2.0.3+)
|
|
117
117
|
|
|
118
118
|
The datepicker includes a robust resolution mechanism for signal-based fields. It can handle:
|
|
119
119
|
- **Direct Signals**: A signal that contains the field configuration.
|
|
@@ -136,6 +136,15 @@ const config: SignalFormFieldConfig = {
|
|
|
136
136
|
};
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
+
**TypeScript Compatibility (v2.0.3+):**
|
|
140
|
+
|
|
141
|
+
The datepicker is fully compatible with Angular 21+ `FieldTree<string | Date | null, string>` structure. The types accept:
|
|
142
|
+
- `WritableSignal<Date | null>` for date values
|
|
143
|
+
- `WritableSignal<string | null>` for string date values (automatically converted to Date)
|
|
144
|
+
- Any Angular Signal Forms field configuration
|
|
145
|
+
|
|
146
|
+
String values from Signal Forms are automatically normalized to Date objects internally, ensuring seamless integration with Angular 21+ forms.
|
|
147
|
+
|
|
139
148
|
## Dirty State Tracking
|
|
140
149
|
|
|
141
150
|
When using the `[field]` binding, the datepicker automatically tracks the form's dirty state. The form will be marked as dirty when a user selects a date:
|
|
@@ -1659,48 +1659,26 @@ function provideDatepickerConfig(config) {
|
|
|
1659
1659
|
};
|
|
1660
1660
|
}
|
|
1661
1661
|
|
|
1662
|
-
// Safe isSignal detection - works with all Angular 17+ versions including 17.0.0
|
|
1663
|
-
// In Angular 17.0.0, isSignal might not be available, so we use function-based detection
|
|
1664
|
-
// We avoid importing isSignal directly to prevent build errors in Angular 17.0.0
|
|
1665
|
-
// This function provides compatibility across all Angular 17 sub-versions
|
|
1666
1662
|
function safeIsSignal(value) {
|
|
1667
1663
|
if (value === null || value === undefined) {
|
|
1668
1664
|
return false;
|
|
1669
1665
|
}
|
|
1670
|
-
// For Angular 17.1.0+, try to use isSignal if available via dynamic access
|
|
1671
|
-
// We check at runtime to avoid build-time import resolution issues
|
|
1672
1666
|
try {
|
|
1673
|
-
// Access Angular core module dynamically to check for isSignal
|
|
1674
|
-
// This avoids the @angular/core/primitives/signals resolution issue in Angular 17.0.0
|
|
1675
1667
|
const ngCore = globalThis.ng?.core;
|
|
1676
1668
|
if (ngCore?.isSignal && typeof ngCore.isSignal === 'function') {
|
|
1677
1669
|
return ngCore.isSignal(value);
|
|
1678
1670
|
}
|
|
1679
1671
|
}
|
|
1680
1672
|
catch {
|
|
1681
|
-
// isSignal not available - use fallback detection
|
|
1682
1673
|
}
|
|
1683
|
-
// Fallback detection for Angular 17.0.0 and all versions
|
|
1684
|
-
// This works by detecting signal-like function characteristics
|
|
1685
|
-
// Signals in Angular are functions that can be called without arguments
|
|
1686
1674
|
if (typeof value === 'function') {
|
|
1687
1675
|
const fn = value;
|
|
1688
|
-
// Signals are typically:
|
|
1689
|
-
// 1. Functions without a prototype (or minimal prototype)
|
|
1690
|
-
// 2. Callable without arguments
|
|
1691
|
-
// 3. Return values when called
|
|
1692
|
-
// In Angular 17.0.0, we treat callable functions as potential signals
|
|
1693
|
-
// This is safe because we're in a reactive context (effect) where signals can be read
|
|
1694
1676
|
try {
|
|
1695
|
-
// Check if it's callable - if it is, treat it as a potential signal
|
|
1696
|
-
// This works for Angular 17.0.0 where isSignal might not be available
|
|
1697
1677
|
if (fn.length === 0 || fn.length === undefined) {
|
|
1698
|
-
// No required arguments - likely a signal getter
|
|
1699
1678
|
return true;
|
|
1700
1679
|
}
|
|
1701
1680
|
}
|
|
1702
1681
|
catch {
|
|
1703
|
-
// Not callable - not a signal
|
|
1704
1682
|
return false;
|
|
1705
1683
|
}
|
|
1706
1684
|
}
|
|
@@ -1713,36 +1691,25 @@ class FieldSyncService {
|
|
|
1713
1691
|
this._isUpdatingFromInternal = false;
|
|
1714
1692
|
this.injector = inject(Injector);
|
|
1715
1693
|
}
|
|
1716
|
-
/**
|
|
1717
|
-
* Helper function to safely read a field value, handling both functions and signals
|
|
1718
|
-
* This handles Angular 21 Signal Forms where field.value can be a signal directly
|
|
1719
|
-
* Note: Angular signals (including computed) are detected as 'function' type
|
|
1720
|
-
*/
|
|
1721
1694
|
readFieldValue(field) {
|
|
1722
1695
|
if (!field || typeof field !== 'object' || field.value === undefined) {
|
|
1723
1696
|
return null;
|
|
1724
1697
|
}
|
|
1725
1698
|
try {
|
|
1726
1699
|
const fieldValue = field.value;
|
|
1727
|
-
// For Angular 21 Signal Forms, field.value might be a function that returns the signal
|
|
1728
|
-
// Try calling it first if it's a function (this handles FieldTree<Date, string> structure)
|
|
1729
1700
|
if (typeof fieldValue === 'function') {
|
|
1730
1701
|
try {
|
|
1731
1702
|
const result = fieldValue();
|
|
1732
|
-
// If result is a signal, call it again to get the actual value
|
|
1733
1703
|
if (safeIsSignal(result)) {
|
|
1734
1704
|
const signalResult = result();
|
|
1735
1705
|
return signalResult !== undefined && signalResult !== null ? signalResult : null;
|
|
1736
1706
|
}
|
|
1737
|
-
// If result is a value (Date, object, etc.), return it
|
|
1738
1707
|
if (result !== undefined && result !== null) {
|
|
1739
1708
|
return result;
|
|
1740
1709
|
}
|
|
1741
1710
|
return null;
|
|
1742
1711
|
}
|
|
1743
1712
|
catch {
|
|
1744
|
-
// If calling fails, it might be a signal itself that needs to be called
|
|
1745
|
-
// Try checking if it's a signal
|
|
1746
1713
|
if (safeIsSignal(fieldValue)) {
|
|
1747
1714
|
try {
|
|
1748
1715
|
const signalResult = fieldValue();
|
|
@@ -1755,17 +1722,14 @@ class FieldSyncService {
|
|
|
1755
1722
|
return null;
|
|
1756
1723
|
}
|
|
1757
1724
|
}
|
|
1758
|
-
// Explicitly check for Angular Signal (compatible with all Angular 17+ versions)
|
|
1759
1725
|
if (safeIsSignal(fieldValue)) {
|
|
1760
1726
|
const result = fieldValue();
|
|
1761
1727
|
return result !== undefined && result !== null ? result : null;
|
|
1762
1728
|
}
|
|
1763
|
-
// If it's an object, it might be a Date, range object, or a signal that isSignal didn't catch
|
|
1764
1729
|
if (fieldValue !== null && typeof fieldValue === 'object') {
|
|
1765
1730
|
if (fieldValue instanceof Date) {
|
|
1766
1731
|
return fieldValue;
|
|
1767
1732
|
}
|
|
1768
|
-
// Try calling it as a last resort (for computed signals that isSignal missed)
|
|
1769
1733
|
try {
|
|
1770
1734
|
const result = fieldValue();
|
|
1771
1735
|
if (result !== undefined && result !== null) {
|
|
@@ -1773,7 +1737,6 @@ class FieldSyncService {
|
|
|
1773
1737
|
}
|
|
1774
1738
|
}
|
|
1775
1739
|
catch {
|
|
1776
|
-
// Not callable, treat as value (could be range object {start, end} or array)
|
|
1777
1740
|
return fieldValue;
|
|
1778
1741
|
}
|
|
1779
1742
|
}
|
|
@@ -1787,26 +1750,19 @@ class FieldSyncService {
|
|
|
1787
1750
|
return null;
|
|
1788
1751
|
}
|
|
1789
1752
|
}
|
|
1790
|
-
/**
|
|
1791
|
-
* Helper function to safely read disabled state
|
|
1792
|
-
* Handles both direct values, functions, and signals (Angular 21 Signal Forms)
|
|
1793
|
-
*/
|
|
1794
1753
|
readDisabledState(field) {
|
|
1795
1754
|
if (!field || (typeof field !== 'object' && typeof field !== 'function')) {
|
|
1796
1755
|
return false;
|
|
1797
1756
|
}
|
|
1798
1757
|
try {
|
|
1799
|
-
// Check disabled property first
|
|
1800
1758
|
if ('disabled' in field && field.disabled !== undefined) {
|
|
1801
1759
|
const disabledVal = field.disabled;
|
|
1802
|
-
// Explicitly check for Angular Signal (compatible with all Angular 17+ versions)
|
|
1803
1760
|
if (safeIsSignal(disabledVal)) {
|
|
1804
1761
|
return !!disabledVal();
|
|
1805
1762
|
}
|
|
1806
1763
|
if (typeof disabledVal === 'function') {
|
|
1807
1764
|
return !!disabledVal();
|
|
1808
1765
|
}
|
|
1809
|
-
// Handle signals detected as objects
|
|
1810
1766
|
if (typeof disabledVal === 'object' && disabledVal !== null) {
|
|
1811
1767
|
try {
|
|
1812
1768
|
return !!disabledVal();
|
|
@@ -1824,10 +1780,6 @@ class FieldSyncService {
|
|
|
1824
1780
|
return false;
|
|
1825
1781
|
}
|
|
1826
1782
|
}
|
|
1827
|
-
/**
|
|
1828
|
-
* Helper function to safely read validation errors from Angular Signal Forms
|
|
1829
|
-
* Returns the errors array from the field's errors signal
|
|
1830
|
-
*/
|
|
1831
1783
|
readFieldErrors(field) {
|
|
1832
1784
|
if (!field || (typeof field !== 'object' && typeof field !== 'function')) {
|
|
1833
1785
|
return [];
|
|
@@ -1835,7 +1787,6 @@ class FieldSyncService {
|
|
|
1835
1787
|
try {
|
|
1836
1788
|
if ('errors' in field && field.errors !== undefined) {
|
|
1837
1789
|
const errorsVal = field.errors;
|
|
1838
|
-
// Check for Angular Signal
|
|
1839
1790
|
if (safeIsSignal(errorsVal)) {
|
|
1840
1791
|
const result = errorsVal();
|
|
1841
1792
|
return Array.isArray(result) ? result : [];
|
|
@@ -1844,7 +1795,6 @@ class FieldSyncService {
|
|
|
1844
1795
|
const result = errorsVal();
|
|
1845
1796
|
return Array.isArray(result) ? result : [];
|
|
1846
1797
|
}
|
|
1847
|
-
// Handle signals detected as objects
|
|
1848
1798
|
if (typeof errorsVal === 'object' && errorsVal !== null) {
|
|
1849
1799
|
try {
|
|
1850
1800
|
const result = errorsVal();
|
|
@@ -1862,34 +1812,24 @@ class FieldSyncService {
|
|
|
1862
1812
|
return [];
|
|
1863
1813
|
}
|
|
1864
1814
|
}
|
|
1865
|
-
/**
|
|
1866
|
-
* Helper function to safely read required state
|
|
1867
|
-
* Handles both direct values, functions, and signals (Angular 21 Signal Forms)
|
|
1868
|
-
* Also checks for 'required' validation errors in Angular Signal Forms
|
|
1869
|
-
*/
|
|
1870
1815
|
readRequiredState(field) {
|
|
1871
1816
|
if (!field || (typeof field !== 'object' && typeof field !== 'function')) {
|
|
1872
1817
|
return false;
|
|
1873
1818
|
}
|
|
1874
1819
|
try {
|
|
1875
|
-
// First, check for 'required' validation error in Angular Signal Forms
|
|
1876
|
-
// This handles schema-based validation like required(p.dateDue)
|
|
1877
1820
|
const errors = this.readFieldErrors(field);
|
|
1878
1821
|
const hasRequiredError = errors.some(error => error.kind === 'required');
|
|
1879
1822
|
if (hasRequiredError) {
|
|
1880
1823
|
return true;
|
|
1881
1824
|
}
|
|
1882
|
-
// Check required property (for direct required attribute)
|
|
1883
1825
|
if ('required' in field && field.required !== undefined) {
|
|
1884
1826
|
const requiredVal = field.required;
|
|
1885
|
-
// Explicitly check for Angular Signal (compatible with all Angular 17+ versions)
|
|
1886
1827
|
if (safeIsSignal(requiredVal)) {
|
|
1887
1828
|
return !!requiredVal();
|
|
1888
1829
|
}
|
|
1889
1830
|
if (typeof requiredVal === 'function') {
|
|
1890
1831
|
return !!requiredVal();
|
|
1891
1832
|
}
|
|
1892
|
-
// Handle signals detected as objects
|
|
1893
1833
|
if (typeof requiredVal === 'object' && requiredVal !== null) {
|
|
1894
1834
|
try {
|
|
1895
1835
|
return !!requiredVal();
|
|
@@ -1906,16 +1846,11 @@ class FieldSyncService {
|
|
|
1906
1846
|
return false;
|
|
1907
1847
|
}
|
|
1908
1848
|
}
|
|
1909
|
-
/**
|
|
1910
|
-
* Helper function to determine if the field has validation errors
|
|
1911
|
-
* Used to set the error state on the datepicker component
|
|
1912
|
-
*/
|
|
1913
1849
|
hasValidationErrors(field) {
|
|
1914
1850
|
if (!field || (typeof field !== 'object' && typeof field !== 'function')) {
|
|
1915
1851
|
return false;
|
|
1916
1852
|
}
|
|
1917
1853
|
try {
|
|
1918
|
-
// Check invalid signal first (Angular Signal Forms)
|
|
1919
1854
|
if ('invalid' in field && field.invalid !== undefined) {
|
|
1920
1855
|
const invalidVal = field.invalid;
|
|
1921
1856
|
if (safeIsSignal(invalidVal)) {
|
|
@@ -1942,38 +1877,27 @@ class FieldSyncService {
|
|
|
1942
1877
|
return false;
|
|
1943
1878
|
}
|
|
1944
1879
|
}
|
|
1945
|
-
/**
|
|
1946
|
-
* Helper to resolve the actual field object from a potential signal or function
|
|
1947
|
-
*/
|
|
1948
1880
|
resolveField(field) {
|
|
1949
1881
|
if (!field)
|
|
1950
1882
|
return null;
|
|
1951
|
-
// Handle objects directly
|
|
1952
1883
|
if (typeof field === 'object') {
|
|
1953
1884
|
return field;
|
|
1954
1885
|
}
|
|
1955
|
-
// Handle functions (Signals or getters)
|
|
1956
1886
|
if (typeof field === 'function') {
|
|
1957
1887
|
const fieldFn = field;
|
|
1958
|
-
// If the function itself has field properties (like value, disabled, setValue),
|
|
1959
|
-
// it's likely a Signal that IS the field (Angular Signal Forms pattern).
|
|
1960
|
-
// We check for some common properties to identify it as a field config.
|
|
1961
1888
|
const hasFieldProps = 'value' in fieldFn || 'disabled' in fieldFn || 'required' in fieldFn ||
|
|
1962
1889
|
'setValue' in fieldFn || 'markAsDirty' in fieldFn || 'errors' in fieldFn;
|
|
1963
1890
|
if (hasFieldProps) {
|
|
1964
1891
|
return fieldFn;
|
|
1965
1892
|
}
|
|
1966
|
-
// If it doesn't have properties, it might be a getter or a Signal returning the field config
|
|
1967
1893
|
if (safeIsSignal(field)) {
|
|
1968
1894
|
try {
|
|
1969
1895
|
const result = field();
|
|
1970
|
-
// If the result is an object, that's our field config
|
|
1971
1896
|
if (result && typeof result === 'object') {
|
|
1972
1897
|
return result;
|
|
1973
1898
|
}
|
|
1974
1899
|
}
|
|
1975
1900
|
catch {
|
|
1976
|
-
// Fall through
|
|
1977
1901
|
}
|
|
1978
1902
|
}
|
|
1979
1903
|
// Generic fallback for getters
|
|
@@ -1996,17 +1920,13 @@ class FieldSyncService {
|
|
|
1996
1920
|
}
|
|
1997
1921
|
try {
|
|
1998
1922
|
const effectRef = runInInjectionContext(this.injector, () => effect(() => {
|
|
1999
|
-
// Skip if we're updating from internal to prevent circular updates
|
|
2000
1923
|
if (this._isUpdatingFromInternal) {
|
|
2001
1924
|
return;
|
|
2002
1925
|
}
|
|
2003
|
-
// Resolve the field object. If fieldInput is a signal, this line tracks it as a dependency.
|
|
2004
1926
|
const field = this.resolveField(fieldInput);
|
|
2005
1927
|
if (!field) {
|
|
2006
1928
|
return;
|
|
2007
1929
|
}
|
|
2008
|
-
// Read the field value directly in the effect context
|
|
2009
|
-
// This ensures all signal dependencies (including computed signals) are properly tracked
|
|
2010
1930
|
let fieldValue = null;
|
|
2011
1931
|
try {
|
|
2012
1932
|
const fieldValueRef = field.value;
|
|
@@ -2014,28 +1934,21 @@ class FieldSyncService {
|
|
|
2014
1934
|
fieldValue = null;
|
|
2015
1935
|
}
|
|
2016
1936
|
else if (typeof fieldValueRef === 'function') {
|
|
2017
|
-
// For Angular 21 Signal Forms, field.value is often a function that returns a signal
|
|
2018
|
-
// Call it in effect context to track dependencies properly
|
|
2019
1937
|
try {
|
|
2020
1938
|
const funcResult = fieldValueRef();
|
|
2021
|
-
// Check if the result is a signal (common in Angular 21 Signal Forms)
|
|
2022
1939
|
if (safeIsSignal(funcResult)) {
|
|
2023
|
-
// Read the signal value in effect context to track dependencies
|
|
2024
1940
|
const signalResult = funcResult();
|
|
2025
1941
|
fieldValue = (signalResult !== undefined && signalResult !== null) ? signalResult : null;
|
|
2026
1942
|
}
|
|
2027
1943
|
else if (safeIsSignal(fieldValueRef)) {
|
|
2028
|
-
// If fieldValueRef itself is a signal, read it directly
|
|
2029
1944
|
const signalResult = fieldValueRef();
|
|
2030
1945
|
fieldValue = (signalResult !== undefined && signalResult !== null) ? signalResult : null;
|
|
2031
1946
|
}
|
|
2032
1947
|
else {
|
|
2033
|
-
// Result is a direct value
|
|
2034
1948
|
fieldValue = (funcResult !== undefined && funcResult !== null) ? funcResult : null;
|
|
2035
1949
|
}
|
|
2036
1950
|
}
|
|
2037
1951
|
catch {
|
|
2038
|
-
// If calling fails, try checking if it's a signal itself
|
|
2039
1952
|
if (safeIsSignal(fieldValueRef)) {
|
|
2040
1953
|
try {
|
|
2041
1954
|
const signalResult = fieldValueRef();
|
|
@@ -2051,46 +1964,29 @@ class FieldSyncService {
|
|
|
2051
1964
|
}
|
|
2052
1965
|
}
|
|
2053
1966
|
else if (safeIsSignal(fieldValueRef)) {
|
|
2054
|
-
// For signals (including computed), read directly in effect context
|
|
2055
|
-
// This ensures computed signals track their underlying dependencies
|
|
2056
|
-
// Reading the signal here automatically tracks all its dependencies
|
|
2057
1967
|
const signalResult = fieldValueRef();
|
|
2058
1968
|
fieldValue = (signalResult !== undefined && signalResult !== null) ? signalResult : null;
|
|
2059
1969
|
}
|
|
2060
1970
|
else if (fieldValueRef instanceof Date) {
|
|
2061
|
-
// Direct Date value
|
|
2062
1971
|
fieldValue = fieldValueRef;
|
|
2063
1972
|
}
|
|
2064
1973
|
else if (typeof fieldValueRef === 'object') {
|
|
2065
|
-
// Could be a range object or array
|
|
2066
1974
|
fieldValue = fieldValueRef;
|
|
2067
1975
|
}
|
|
2068
1976
|
else {
|
|
2069
|
-
// Fallback to helper
|
|
2070
1977
|
fieldValue = this.readFieldValue(field);
|
|
2071
1978
|
}
|
|
2072
1979
|
}
|
|
2073
1980
|
catch {
|
|
2074
|
-
// Fallback to helper on any error
|
|
2075
1981
|
fieldValue = this.readFieldValue(field);
|
|
2076
1982
|
}
|
|
2077
|
-
// Normalize the value using the same normalization function as the component
|
|
2078
|
-
// This ensures consistent value comparison
|
|
2079
1983
|
const normalizedValue = callbacks.normalizeValue(fieldValue);
|
|
2080
|
-
// Determine if we should update
|
|
2081
|
-
// Always update on initial load (even if null) to ensure component is initialized
|
|
2082
1984
|
const isInitialLoad = this._lastKnownFieldValue === undefined;
|
|
2083
|
-
// Use isValueEqual to compare values - this handles date normalization and equality correctly
|
|
2084
|
-
// This prevents overwriting values that were just set internally, even if effect runs after flag reset
|
|
2085
1985
|
const valuesAreEqual = this._lastKnownFieldValue !== undefined &&
|
|
2086
1986
|
callbacks.isValueEqual(normalizedValue, this._lastKnownFieldValue);
|
|
2087
1987
|
const valueChanged = !isInitialLoad && !valuesAreEqual;
|
|
2088
1988
|
const isValueTransition = (this._lastKnownFieldValue === null || this._lastKnownFieldValue === undefined) &&
|
|
2089
1989
|
fieldValue !== null && fieldValue !== undefined;
|
|
2090
|
-
// Always update on initial load (even if null), value change, or value transition
|
|
2091
|
-
// BUT skip if values are equal (prevents overwriting values set internally)
|
|
2092
|
-
// This ensures the component value is always set, including null values
|
|
2093
|
-
// but prevents circular updates when we just set the value internally
|
|
2094
1990
|
if ((isInitialLoad || valueChanged || isValueTransition) && !valuesAreEqual) {
|
|
2095
1991
|
this._lastKnownFieldValue = normalizedValue;
|
|
2096
1992
|
callbacks.onValueChanged(normalizedValue);
|
|
@@ -2098,25 +1994,16 @@ class FieldSyncService {
|
|
|
2098
1994
|
callbacks.onStateChanged?.();
|
|
2099
1995
|
}
|
|
2100
1996
|
else if (!valuesAreEqual && this._lastKnownFieldValue !== normalizedValue) {
|
|
2101
|
-
// Update last known value even if we don't trigger callbacks
|
|
2102
|
-
// This handles edge cases where values are equal but references differ
|
|
2103
|
-
// But only if values are not equal according to isValueEqual
|
|
2104
1997
|
this._lastKnownFieldValue = normalizedValue;
|
|
2105
1998
|
}
|
|
2106
|
-
// Always update disabled state
|
|
2107
1999
|
const disabled = this.readDisabledState(field);
|
|
2108
2000
|
callbacks.onDisabledChanged(disabled);
|
|
2109
|
-
// Always update required state
|
|
2110
2001
|
const required = this.readRequiredState(field);
|
|
2111
2002
|
callbacks.onRequiredChanged?.(required);
|
|
2112
|
-
// Always update error state (for Angular Signal Forms validation)
|
|
2113
2003
|
const hasError = this.hasValidationErrors(field);
|
|
2114
2004
|
callbacks.onErrorStateChanged?.(hasError);
|
|
2115
2005
|
}));
|
|
2116
2006
|
this._fieldEffectRef = effectRef;
|
|
2117
|
-
// Effects run immediately when created, so the effect has already run
|
|
2118
|
-
// and read the signal value in a reactive context. This ensures computed
|
|
2119
|
-
// signals are properly tracked and all dependencies are registered.
|
|
2120
2007
|
return effectRef;
|
|
2121
2008
|
}
|
|
2122
2009
|
catch (error) {
|
|
@@ -2132,13 +2019,10 @@ class FieldSyncService {
|
|
|
2132
2019
|
return false;
|
|
2133
2020
|
const fieldValue = this.readFieldValue(field);
|
|
2134
2021
|
const normalizedValue = callbacks.normalizeValue(fieldValue);
|
|
2135
|
-
// Check if value has changed or if this is initial load
|
|
2136
2022
|
const hasValueChanged = !callbacks.isValueEqual(normalizedValue, this._lastKnownFieldValue);
|
|
2137
2023
|
const isInitialLoad = this._lastKnownFieldValue === undefined;
|
|
2138
2024
|
const isValueTransition = (this._lastKnownFieldValue === null || this._lastKnownFieldValue === undefined) &&
|
|
2139
2025
|
fieldValue !== null && fieldValue !== undefined;
|
|
2140
|
-
// Always sync on initial load (even if null), value change, or value transition
|
|
2141
|
-
// This ensures the component value is set correctly, including null values
|
|
2142
2026
|
if (isInitialLoad || hasValueChanged || isValueTransition) {
|
|
2143
2027
|
this._lastKnownFieldValue = normalizedValue;
|
|
2144
2028
|
callbacks.onValueChanged(normalizedValue);
|
|
@@ -2152,7 +2036,6 @@ class FieldSyncService {
|
|
|
2152
2036
|
callbacks.onErrorStateChanged?.(hasError);
|
|
2153
2037
|
return true;
|
|
2154
2038
|
}
|
|
2155
|
-
// Update last known value even if we don't trigger callbacks
|
|
2156
2039
|
if (this._lastKnownFieldValue !== normalizedValue) {
|
|
2157
2040
|
this._lastKnownFieldValue = normalizedValue;
|
|
2158
2041
|
}
|
|
@@ -2169,61 +2052,43 @@ class FieldSyncService {
|
|
|
2169
2052
|
if (!field || typeof field !== 'object') {
|
|
2170
2053
|
return;
|
|
2171
2054
|
}
|
|
2172
|
-
// Set flag to prevent effect from processing this update
|
|
2173
|
-
// This prevents circular updates when we update the field from component
|
|
2174
2055
|
this._isUpdatingFromInternal = true;
|
|
2175
2056
|
try {
|
|
2176
|
-
// The value passed in should already be normalized by the component
|
|
2177
|
-
// Store it as the last known value BEFORE updating the field
|
|
2178
|
-
// This ensures the effect sees a match if it runs, preventing overwrites
|
|
2179
2057
|
const normalizedValue = value;
|
|
2180
2058
|
this._lastKnownFieldValue = normalizedValue;
|
|
2181
|
-
// Try setValue first (preferred method for Angular 21 Signal Forms)
|
|
2182
|
-
// This works with computed signal patterns where setValue updates the underlying signal
|
|
2183
2059
|
if (typeof field.setValue === 'function') {
|
|
2184
2060
|
try {
|
|
2185
2061
|
field.setValue(normalizedValue);
|
|
2186
2062
|
if (typeof field.markAsDirty === 'function') {
|
|
2187
2063
|
field.markAsDirty();
|
|
2188
2064
|
}
|
|
2189
|
-
// Use microtask delay to ensure effect has time to see the flag
|
|
2190
|
-
// This prevents race condition where effect runs after flag is reset
|
|
2191
2065
|
Promise.resolve().then(() => {
|
|
2192
2066
|
this._isUpdatingFromInternal = false;
|
|
2193
2067
|
});
|
|
2194
2068
|
return;
|
|
2195
2069
|
}
|
|
2196
2070
|
catch {
|
|
2197
|
-
// If setValue fails, try updateValue as fallback
|
|
2198
|
-
// Don't return here, continue to try updateValue
|
|
2199
2071
|
}
|
|
2200
2072
|
}
|
|
2201
|
-
// Try updateValue as alternative method
|
|
2202
2073
|
if (typeof field.updateValue === 'function') {
|
|
2203
2074
|
try {
|
|
2204
2075
|
field.updateValue(() => normalizedValue);
|
|
2205
2076
|
if (typeof field.markAsDirty === 'function') {
|
|
2206
2077
|
field.markAsDirty();
|
|
2207
2078
|
}
|
|
2208
|
-
// Use microtask delay to ensure effect has time to see the flag
|
|
2209
2079
|
Promise.resolve().then(() => {
|
|
2210
2080
|
this._isUpdatingFromInternal = false;
|
|
2211
2081
|
});
|
|
2212
2082
|
return;
|
|
2213
2083
|
}
|
|
2214
2084
|
catch {
|
|
2215
|
-
// If updateValue also fails, continue to fallback
|
|
2216
2085
|
}
|
|
2217
2086
|
}
|
|
2218
|
-
// Fallback: try to update the underlying signal directly if field.value is a signal
|
|
2219
|
-
// This handles cases where field.value is a writable signal or a function returning a signal
|
|
2220
2087
|
try {
|
|
2221
2088
|
const val = field.value;
|
|
2222
|
-
// For Angular 21 Signal Forms, field.value might be a function that returns the signal
|
|
2223
2089
|
if (typeof val === 'function') {
|
|
2224
2090
|
try {
|
|
2225
2091
|
const signalOrValue = val();
|
|
2226
|
-
// If it's a signal, try to update it
|
|
2227
2092
|
if (safeIsSignal(signalOrValue)) {
|
|
2228
2093
|
const writableSignal = signalOrValue;
|
|
2229
2094
|
if (typeof writableSignal.set === 'function') {
|
|
@@ -2231,7 +2096,6 @@ class FieldSyncService {
|
|
|
2231
2096
|
if (typeof field.markAsDirty === 'function') {
|
|
2232
2097
|
field.markAsDirty();
|
|
2233
2098
|
}
|
|
2234
|
-
// Use microtask delay to ensure effect has time to see the flag
|
|
2235
2099
|
Promise.resolve().then(() => {
|
|
2236
2100
|
this._isUpdatingFromInternal = false;
|
|
2237
2101
|
});
|
|
@@ -2240,11 +2104,8 @@ class FieldSyncService {
|
|
|
2240
2104
|
}
|
|
2241
2105
|
}
|
|
2242
2106
|
catch {
|
|
2243
|
-
// If calling fails, continue to check if val itself is a signal
|
|
2244
2107
|
}
|
|
2245
2108
|
}
|
|
2246
|
-
// Check if it's a writable signal (has .set method)
|
|
2247
|
-
// Compatible with all Angular 17+ versions
|
|
2248
2109
|
if (safeIsSignal(val)) {
|
|
2249
2110
|
const writableSignal = val;
|
|
2250
2111
|
if (typeof writableSignal.set === 'function') {
|
|
@@ -2252,7 +2113,6 @@ class FieldSyncService {
|
|
|
2252
2113
|
if (typeof field.markAsDirty === 'function') {
|
|
2253
2114
|
field.markAsDirty();
|
|
2254
2115
|
}
|
|
2255
|
-
// Use microtask delay to ensure effect has time to see the flag
|
|
2256
2116
|
Promise.resolve().then(() => {
|
|
2257
2117
|
this._isUpdatingFromInternal = false;
|
|
2258
2118
|
});
|
|
@@ -2261,15 +2121,10 @@ class FieldSyncService {
|
|
|
2261
2121
|
}
|
|
2262
2122
|
}
|
|
2263
2123
|
catch {
|
|
2264
|
-
// Silently handle - field might not support direct signal updates
|
|
2265
|
-
// This is expected for computed signals which are read-only
|
|
2266
2124
|
}
|
|
2267
|
-
// If no update method worked, reset flag immediately
|
|
2268
2125
|
this._isUpdatingFromInternal = false;
|
|
2269
2126
|
}
|
|
2270
2127
|
catch (error) {
|
|
2271
|
-
// Silently handle errors to prevent breaking the application
|
|
2272
|
-
// The error might be due to readonly signals or other constraints
|
|
2273
2128
|
if (isDevMode()) {
|
|
2274
2129
|
console.warn('[ngxsmk-datepicker] Field sync error:', error);
|
|
2275
2130
|
}
|
|
@@ -2279,6 +2134,20 @@ class FieldSyncService {
|
|
|
2279
2134
|
getLastKnownValue() {
|
|
2280
2135
|
return this._lastKnownFieldValue;
|
|
2281
2136
|
}
|
|
2137
|
+
markAsTouched(fieldInput) {
|
|
2138
|
+
const field = this.resolveField(fieldInput);
|
|
2139
|
+
if (!field || typeof field !== 'object') {
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
try {
|
|
2143
|
+
if (typeof field.markAsTouched === 'function') {
|
|
2144
|
+
field.markAsTouched();
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
catch {
|
|
2148
|
+
// Ignore errors when marking as touched
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2282
2151
|
cleanup() {
|
|
2283
2152
|
if (this._fieldEffectRef) {
|
|
2284
2153
|
this._fieldEffectRef.destroy();
|
|
@@ -5618,6 +5487,9 @@ class NgxsmkDatepickerComponent {
|
|
|
5618
5487
|
this.valueChange.emit(normalizedVal);
|
|
5619
5488
|
this.onChange(normalizedVal);
|
|
5620
5489
|
this.onTouched();
|
|
5490
|
+
if (this._field) {
|
|
5491
|
+
this.fieldSyncService.markAsTouched(this._field);
|
|
5492
|
+
}
|
|
5621
5493
|
if (!this.isInlineMode && val !== null && !this.timeOnly) {
|
|
5622
5494
|
if (this.mode === 'single' || (this.mode === 'range' && this.startDate && this.endDate)) {
|
|
5623
5495
|
this.isCalendarOpen = false;
|
|
@@ -6886,6 +6758,9 @@ class NgxsmkDatepickerComponent {
|
|
|
6886
6758
|
this._focused = false;
|
|
6887
6759
|
this.stateChanges.next();
|
|
6888
6760
|
this.onTouched();
|
|
6761
|
+
if (this._field) {
|
|
6762
|
+
this.fieldSyncService.markAsTouched(this._field);
|
|
6763
|
+
}
|
|
6889
6764
|
}
|
|
6890
6765
|
if (!this.allowTyping)
|
|
6891
6766
|
return;
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { AfterViewInit, OnDestroy, EventEmitter, ElementRef,
|
|
2
|
+
import { AfterViewInit, OnDestroy, EventEmitter, ElementRef, EffectRef, Signal, OnInit, OnChanges, TemplateRef, SimpleChanges, InjectionToken, Provider } from '@angular/core';
|
|
3
3
|
import { ControlValueAccessor } from '@angular/forms';
|
|
4
4
|
import { Subject } from 'rxjs';
|
|
5
5
|
|
|
@@ -160,9 +160,9 @@ interface ValidationError {
|
|
|
160
160
|
message?: string;
|
|
161
161
|
}
|
|
162
162
|
type SignalFormFieldConfig = {
|
|
163
|
-
value?: DatepickerValue | (() => DatepickerValue) | {
|
|
164
|
-
(): DatepickerValue;
|
|
165
|
-
} | Signal<DatepickerValue>;
|
|
163
|
+
value?: DatepickerValue | string | (() => DatepickerValue | string) | {
|
|
164
|
+
(): DatepickerValue | string;
|
|
165
|
+
} | Signal<DatepickerValue | string>;
|
|
166
166
|
disabled?: boolean | (() => boolean) | {
|
|
167
167
|
(): boolean;
|
|
168
168
|
} | Signal<boolean>;
|
|
@@ -181,13 +181,12 @@ type SignalFormFieldConfig = {
|
|
|
181
181
|
touched?: boolean | (() => boolean) | {
|
|
182
182
|
(): boolean;
|
|
183
183
|
} | Signal<boolean>;
|
|
184
|
-
setValue?: (value: DatepickerValue) => void;
|
|
185
|
-
updateValue?: (updater: () => DatepickerValue) => void;
|
|
184
|
+
setValue?: (value: DatepickerValue | string) => void;
|
|
185
|
+
updateValue?: (updater: () => DatepickerValue | string) => void;
|
|
186
186
|
markAsDirty?: () => void;
|
|
187
|
-
|
|
188
|
-
[key: string]: unknown;
|
|
187
|
+
markAsTouched?: () => void;
|
|
189
188
|
};
|
|
190
|
-
type SignalFormField =
|
|
189
|
+
type SignalFormField = any;
|
|
191
190
|
interface FieldSyncCallbacks {
|
|
192
191
|
onValueChanged: (value: DatepickerValue) => void;
|
|
193
192
|
onDisabledChanged: (disabled: boolean) => void;
|
|
@@ -204,41 +203,17 @@ declare class FieldSyncService {
|
|
|
204
203
|
private _lastKnownFieldValue;
|
|
205
204
|
private _isUpdatingFromInternal;
|
|
206
205
|
private readonly injector;
|
|
207
|
-
/**
|
|
208
|
-
* Helper function to safely read a field value, handling both functions and signals
|
|
209
|
-
* This handles Angular 21 Signal Forms where field.value can be a signal directly
|
|
210
|
-
* Note: Angular signals (including computed) are detected as 'function' type
|
|
211
|
-
*/
|
|
212
206
|
private readFieldValue;
|
|
213
|
-
/**
|
|
214
|
-
* Helper function to safely read disabled state
|
|
215
|
-
* Handles both direct values, functions, and signals (Angular 21 Signal Forms)
|
|
216
|
-
*/
|
|
217
207
|
private readDisabledState;
|
|
218
|
-
/**
|
|
219
|
-
* Helper function to safely read validation errors from Angular Signal Forms
|
|
220
|
-
* Returns the errors array from the field's errors signal
|
|
221
|
-
*/
|
|
222
208
|
private readFieldErrors;
|
|
223
|
-
/**
|
|
224
|
-
* Helper function to safely read required state
|
|
225
|
-
* Handles both direct values, functions, and signals (Angular 21 Signal Forms)
|
|
226
|
-
* Also checks for 'required' validation errors in Angular Signal Forms
|
|
227
|
-
*/
|
|
228
209
|
private readRequiredState;
|
|
229
|
-
/**
|
|
230
|
-
* Helper function to determine if the field has validation errors
|
|
231
|
-
* Used to set the error state on the datepicker component
|
|
232
|
-
*/
|
|
233
210
|
private hasValidationErrors;
|
|
234
|
-
/**
|
|
235
|
-
* Helper to resolve the actual field object from a potential signal or function
|
|
236
|
-
*/
|
|
237
211
|
private resolveField;
|
|
238
212
|
setupFieldSync(fieldInput: SignalFormField, callbacks: FieldSyncCallbacks): EffectRef | null;
|
|
239
213
|
syncFieldValue(fieldInput: SignalFormField | Signal<SignalFormField> | (() => unknown) | unknown, callbacks: FieldSyncCallbacks): boolean;
|
|
240
214
|
updateFieldFromInternal(value: DatepickerValue, fieldInput: SignalFormField | Signal<SignalFormField> | (() => unknown) | unknown): void;
|
|
241
215
|
getLastKnownValue(): DatepickerValue | undefined;
|
|
216
|
+
markAsTouched(fieldInput: SignalFormField | Signal<SignalFormField> | (() => unknown) | unknown): void;
|
|
242
217
|
cleanup(): void;
|
|
243
218
|
static ɵfac: i0.ɵɵFactoryDeclaration<FieldSyncService, never>;
|
|
244
219
|
static ɵprov: i0.ɵɵInjectableDeclaration<FieldSyncService>;
|