ngxsmk-datepicker 2.3.1 → 2.4.0
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 +942 -922
- package/docs/API.md +2423 -2413
- package/docs/COMPATIBILITY.md +471 -471
- package/docs/INTEGRATION.md +703 -703
- package/docs/IONIC_INTEGRATION.md +228 -228
- package/docs/LOCALE-GUIDE.md +300 -300
- package/docs/PLUGIN-ARCHITECTURE.md +930 -930
- package/docs/SSR-EXAMPLE.md +427 -427
- package/docs/THEME-TOKENS.md +324 -324
- package/docs/TIMEZONE.md +307 -307
- package/docs/extension-points.md +419 -419
- package/docs/signal-forms.md +600 -600
- package/docs/signals.md +266 -266
- package/docs/ssr.md +305 -305
- package/package.json +106 -104
- package/schematics/collection.json +10 -0
- package/schematics/ng-add/schema.json +14 -0
- package/schematics/tsconfig.json +17 -0
- package/CHANGELOG.md +0 -1246
- package/LICENSE +0 -21
- package/MIGRATION.md +0 -1794
- package/docs/FEATURE_SCOPING.md +0 -60
- package/docs/IONIC_TESTING.md +0 -148
- package/docs/REFACTOR_PLAN.md +0 -38
- package/docs/SEO.md +0 -214
- package/fesm2022/ngxsmk-datepicker.mjs +0 -14693
- package/types/ngxsmk-datepicker.d.ts +0 -2489
package/docs/signal-forms.md
CHANGED
|
@@ -1,600 +1,600 @@
|
|
|
1
|
-
# Signal Forms Integration
|
|
2
|
-
|
|
3
|
-
**Last updated:**
|
|
4
|
-
|
|
5
|
-
This guide covers using ngxsmk-datepicker with Angular 21+ Signal Forms API.
|
|
6
|
-
|
|
7
|
-
## Overview
|
|
8
|
-
|
|
9
|
-
Angular 21 introduced Signal Forms, a new reactive forms API built on signals. The datepicker provides first-class support through the `[field]` input, enabling seamless two-way binding with signal form fields.
|
|
10
|
-
|
|
11
|
-
## Basic Signal Forms Setup
|
|
12
|
-
|
|
13
|
-
### Creating a Signal Form
|
|
14
|
-
|
|
15
|
-
```typescript
|
|
16
|
-
import { Component, signal, form, objectSchema } from '@angular/core';
|
|
17
|
-
import { NgxsmkDatepickerComponent } from 'ngxsmk-datepicker';
|
|
18
|
-
|
|
19
|
-
@Component({
|
|
20
|
-
selector: 'app-signal-form',
|
|
21
|
-
standalone: true,
|
|
22
|
-
imports: [NgxsmkDatepickerComponent],
|
|
23
|
-
template: `
|
|
24
|
-
<form>
|
|
25
|
-
<ngxsmk-datepicker
|
|
26
|
-
[field]="myForm.dateInQuestion"
|
|
27
|
-
mode="single"
|
|
28
|
-
placeholder="Select a date">
|
|
29
|
-
</ngxsmk-datepicker>
|
|
30
|
-
</form>
|
|
31
|
-
`
|
|
32
|
-
})
|
|
33
|
-
export class SignalFormComponent {
|
|
34
|
-
// Create a signal for your form data
|
|
35
|
-
localObject = signal({
|
|
36
|
-
dateInQuestion: new Date(),
|
|
37
|
-
name: 'John Doe'
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// Create a signal form
|
|
41
|
-
myForm = form(this.localObject, objectSchema({
|
|
42
|
-
dateInQuestion: objectSchema<Date>(),
|
|
43
|
-
name: objectSchema<string>()
|
|
44
|
-
}));
|
|
45
|
-
}
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## Server-Side Data Integration
|
|
49
|
-
|
|
50
|
-
### With httpResource
|
|
51
|
-
|
|
52
|
-
When data comes from a server, use `httpResource` with signal forms:
|
|
53
|
-
|
|
54
|
-
```typescript
|
|
55
|
-
import { Component, inject, signal, linkedSignal, computed } from '@angular/core';
|
|
56
|
-
import { httpResource } from '@angular/common/http';
|
|
57
|
-
import { HttpClient } from '@angular/common/http';
|
|
58
|
-
import { form, objectSchema } from '@angular/forms';
|
|
59
|
-
import { NgxsmkDatepickerComponent } from 'ngxsmk-datepicker';
|
|
60
|
-
|
|
61
|
-
@Component({
|
|
62
|
-
selector: 'app-server-form',
|
|
63
|
-
standalone: true,
|
|
64
|
-
imports: [NgxsmkDatepickerComponent],
|
|
65
|
-
template: `
|
|
66
|
-
<form>
|
|
67
|
-
<ngxsmk-datepicker
|
|
68
|
-
[field]="myForm.dateInQuestion"
|
|
69
|
-
mode="single"
|
|
70
|
-
placeholder="Select a date">
|
|
71
|
-
</ngxsmk-datepicker>
|
|
72
|
-
</form>
|
|
73
|
-
`
|
|
74
|
-
})
|
|
75
|
-
export class ServerFormComponent {
|
|
76
|
-
private http = inject(HttpClient);
|
|
77
|
-
|
|
78
|
-
// Fetch data from server
|
|
79
|
-
resource = httpResource({
|
|
80
|
-
request: () => this.http.get<{ dateInQuestion: Date }>('/api/data'),
|
|
81
|
-
loader: signal(false)
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
// Link the response to a signal
|
|
85
|
-
localObject = linkedSignal(() => this.resource.response.value() || {
|
|
86
|
-
dateInQuestion: new Date()
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
// Create form from linked signal
|
|
90
|
-
myForm = form(this.localObject, objectSchema({
|
|
91
|
-
dateInQuestion: objectSchema<Date>()
|
|
92
|
-
}));
|
|
93
|
-
}
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
## Two-Way Binding
|
|
97
|
-
|
|
98
|
-
The `[field]` input provides automatic two-way binding:
|
|
99
|
-
|
|
100
|
-
```typescript
|
|
101
|
-
@Component({
|
|
102
|
-
// ...
|
|
103
|
-
})
|
|
104
|
-
export class TwoWayComponent {
|
|
105
|
-
localObject = signal({ date: new Date() });
|
|
106
|
-
|
|
107
|
-
myForm = form(this.localObject, objectSchema({
|
|
108
|
-
date: objectSchema<Date>()
|
|
109
|
-
}));
|
|
110
|
-
|
|
111
|
-
// The datepicker automatically:
|
|
112
|
-
// 1. Reads from myForm.date.value()
|
|
113
|
-
// 2. Updates myForm.date when user selects a date
|
|
114
|
-
// 3. Handles disabled state from myForm.date.disabled()
|
|
115
|
-
}
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
### Signal Field Resolution (v2.0.5+)
|
|
119
|
-
|
|
120
|
-
The datepicker includes a robust resolution mechanism for signal-based fields. It can handle:
|
|
121
|
-
- **Direct Signals**: A signal that contains the field configuration.
|
|
122
|
-
- **Signals with Properties**: A signal function that has field properties (like `value`, `disabled`, `setValue`) attached directly to it (common in some Signal Form implementations).
|
|
123
|
-
- **Nested Signals**: Signals that return a field configuration object when executed.
|
|
124
|
-
|
|
125
|
-
The datepicker intelligently detects these patterns and unwraps them automatically.
|
|
126
|
-
|
|
127
|
-
### Enhanced Type Safety
|
|
128
|
-
|
|
129
|
-
The library now exports `SignalFormFieldConfig` to allow you to strictly type your field configurations:
|
|
130
|
-
|
|
131
|
-
```typescript
|
|
132
|
-
import { SignalFormFieldConfig } from 'ngxsmk-datepicker';
|
|
133
|
-
|
|
134
|
-
const config: SignalFormFieldConfig = {
|
|
135
|
-
value: signal(new Date()),
|
|
136
|
-
disabled: () => false,
|
|
137
|
-
required: true
|
|
138
|
-
};
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
**TypeScript Compatibility (v2.0.5+):**
|
|
142
|
-
|
|
143
|
-
The datepicker is fully compatible with Angular 21+ `FieldTree<string | Date | null, string>` structure. The types accept:
|
|
144
|
-
- `WritableSignal<Date | null>` for date values
|
|
145
|
-
- `WritableSignal<string | null>` for string date values (automatically converted to Date)
|
|
146
|
-
- Any Angular Signal Forms field configuration
|
|
147
|
-
|
|
148
|
-
String values from Signal Forms are automatically normalized to Date objects internally, ensuring seamless integration with Angular 21+ forms.
|
|
149
|
-
|
|
150
|
-
## Dirty State Tracking
|
|
151
|
-
|
|
152
|
-
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:
|
|
153
|
-
|
|
154
|
-
```typescript
|
|
155
|
-
@Component({
|
|
156
|
-
selector: 'app-form',
|
|
157
|
-
standalone: true,
|
|
158
|
-
imports: [NgxsmkDatepickerComponent],
|
|
159
|
-
template: `
|
|
160
|
-
<form>
|
|
161
|
-
<ngxsmk-datepicker
|
|
162
|
-
[field]="myForm.dateDue"
|
|
163
|
-
mode="single"
|
|
164
|
-
placeholder="Select a date">
|
|
165
|
-
</ngxsmk-datepicker>
|
|
166
|
-
|
|
167
|
-
<button
|
|
168
|
-
type="submit"
|
|
169
|
-
[disabled]="!myForm().dirty()">
|
|
170
|
-
Save Changes
|
|
171
|
-
</button>
|
|
172
|
-
</form>
|
|
173
|
-
`
|
|
174
|
-
})
|
|
175
|
-
export class FormComponent {
|
|
176
|
-
action = signal({ dateDue: new Date() });
|
|
177
|
-
myForm = form(this.action, objectSchema({
|
|
178
|
-
dateDue: objectSchema<Date>()
|
|
179
|
-
}));
|
|
180
|
-
|
|
181
|
-
submitForm() {
|
|
182
|
-
if (!this.myForm().dirty()) {
|
|
183
|
-
console.log('No changes to save');
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
// Submit form...
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
**Important Notes:**
|
|
192
|
-
|
|
193
|
-
1. **Use `[field]` binding for automatic dirty tracking**: The datepicker uses the field's `setValue()` or `updateValue()` methods when available, which properly track dirty state in Angular Signal Forms.
|
|
194
|
-
|
|
195
|
-
2. **Avoid mixing `[field]` with manual `(valueChange)` handlers**: If you use both `[field]` and `(valueChange)="dateField.set($event)"`, the manual handler bypasses the form API and may prevent dirty state tracking. Use one or the other:
|
|
196
|
-
- ✅ **Recommended**: Use only `[field]="myForm.dateField"` for automatic dirty tracking
|
|
197
|
-
- ⚠️ **Alternative**: Use `[value]` and `(valueChange)` with proper form API methods if you need manual control
|
|
198
|
-
|
|
199
|
-
3. **Manual binding pattern**: If you must use manual binding (e.g., for stability issues), ensure you update the form using the field's API methods:
|
|
200
|
-
```typescript
|
|
201
|
-
onDateChange(newDate: Date): void {
|
|
202
|
-
// Use setValue to ensure dirty tracking works
|
|
203
|
-
if (typeof this.myForm.dateField.setValue === 'function') {
|
|
204
|
-
this.myForm.dateField.setValue(newDate);
|
|
205
|
-
} else if (typeof this.myForm.dateField.updateValue === 'function') {
|
|
206
|
-
this.myForm.dateField.updateValue(() => newDate);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
4. **Dev mode warnings**: If the datepicker cannot use `setValue()` or `updateValue()` (e.g., field doesn't provide these methods), it will fall back to direct signal mutation and log a warning in dev mode. This fallback may not track dirty state correctly.
|
|
212
|
-
|
|
213
|
-
## Manual Updates
|
|
214
|
-
|
|
215
|
-
You can also manually update form values:
|
|
216
|
-
|
|
217
|
-
```typescript
|
|
218
|
-
@Component({
|
|
219
|
-
// ...
|
|
220
|
-
})
|
|
221
|
-
export class ManualUpdateComponent {
|
|
222
|
-
localObject = signal({ date: new Date() });
|
|
223
|
-
|
|
224
|
-
myForm = form(this.localObject, objectSchema({
|
|
225
|
-
date: objectSchema<Date>()
|
|
226
|
-
}));
|
|
227
|
-
|
|
228
|
-
updateDate(newDate: Date) {
|
|
229
|
-
// Option 1: Update the underlying signal
|
|
230
|
-
this.localObject.update(obj => ({
|
|
231
|
-
...obj,
|
|
232
|
-
date: newDate
|
|
233
|
-
}));
|
|
234
|
-
|
|
235
|
-
// Option 2: Use form field's setValue (if available)
|
|
236
|
-
if (typeof this.myForm.date.setValue === 'function') {
|
|
237
|
-
this.myForm.date.setValue(newDate);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
## Alternative: Manual Binding with valueChange (Stabilized Pattern)
|
|
244
|
-
|
|
245
|
-
If you experience stability issues with the `[field]` binding, you can use manual binding with `[value]` and `(valueChange)`. **Important**: To ensure dirty state tracking works correctly, use the field's API methods instead of direct mutation:
|
|
246
|
-
|
|
247
|
-
```typescript
|
|
248
|
-
import { Component, signal, computed, form, objectSchema } from '@angular/core';
|
|
249
|
-
import { NgxsmkDatepickerComponent } from 'ngxsmk-datepicker';
|
|
250
|
-
|
|
251
|
-
@Component({
|
|
252
|
-
selector: 'app-stable-form',
|
|
253
|
-
standalone: true,
|
|
254
|
-
imports: [NgxsmkDatepickerComponent],
|
|
255
|
-
template: `
|
|
256
|
-
<ngxsmk-datepicker
|
|
257
|
-
class="w-full border:none"
|
|
258
|
-
[value]="myDate()"
|
|
259
|
-
(valueChange)="onMyDateChange($any($event))"
|
|
260
|
-
mode="single"
|
|
261
|
-
placeholder="Select a date">
|
|
262
|
-
</ngxsmk-datepicker>
|
|
263
|
-
`
|
|
264
|
-
})
|
|
265
|
-
export class StableFormComponent {
|
|
266
|
-
localObject = signal({ myDate: new Date() });
|
|
267
|
-
|
|
268
|
-
myForm = form(this.localObject, objectSchema({
|
|
269
|
-
myDate: objectSchema<Date>()
|
|
270
|
-
}));
|
|
271
|
-
|
|
272
|
-
// Get a signal reference to the date field value
|
|
273
|
-
myDate = computed(() => this.myForm.value().myDate);
|
|
274
|
-
|
|
275
|
-
onMyDateChange(newDate: Date): void {
|
|
276
|
-
// Use setValue to ensure dirty state tracking works
|
|
277
|
-
if (typeof this.myForm.myDate.setValue === 'function') {
|
|
278
|
-
this.myForm.myDate.setValue(newDate);
|
|
279
|
-
} else if (typeof this.myForm.myDate.updateValue === 'function') {
|
|
280
|
-
this.myForm.myDate.updateValue(() => newDate);
|
|
281
|
-
} else {
|
|
282
|
-
// Fallback: directly mutate the form value object
|
|
283
|
-
// Note: This may not track dirty state correctly
|
|
284
|
-
this.myForm.value().myDate = newDate;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
**Why this pattern works:**
|
|
291
|
-
- Uses form API methods (`setValue`/`updateValue`) to ensure dirty state tracking
|
|
292
|
-
- Prevents potential change detection loops
|
|
293
|
-
- More explicit control over when updates occur
|
|
294
|
-
- Useful when `[field]` binding causes stability issues
|
|
295
|
-
|
|
296
|
-
**Note:** The `$any($event)` cast may be needed if there's a type mismatch between `DatepickerValue` and your expected `Date` type.
|
|
297
|
-
|
|
298
|
-
**⚠️ Warning**: Direct mutation (`this.myForm.value().myDate = newDate`) bypasses Angular's dirty tracking mechanism. Always prefer using `setValue()` or `updateValue()` when available.
|
|
299
|
-
|
|
300
|
-
## Validation
|
|
301
|
-
|
|
302
|
-
Signal Forms support validation. The datepicker respects the field's disabled state:
|
|
303
|
-
|
|
304
|
-
```typescript
|
|
305
|
-
import { Component, signal, form, objectSchema, validators } from '@angular/core';
|
|
306
|
-
|
|
307
|
-
@Component({
|
|
308
|
-
// ...
|
|
309
|
-
})
|
|
310
|
-
export class ValidatedFormComponent {
|
|
311
|
-
localObject = signal({ date: null as Date | null });
|
|
312
|
-
|
|
313
|
-
myForm = form(this.localObject, objectSchema({
|
|
314
|
-
date: objectSchema<Date | null>({
|
|
315
|
-
validators: [
|
|
316
|
-
validators.required()
|
|
317
|
-
]
|
|
318
|
-
})
|
|
319
|
-
}));
|
|
320
|
-
|
|
321
|
-
// The datepicker will automatically reflect the disabled state
|
|
322
|
-
// when the field is invalid or disabled
|
|
323
|
-
}
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
### Note on Native Validation
|
|
327
|
-
By default, the datepicker input is `readonly`. Browsers do not validate `readonly` fields. To enable native browser validation (e.g., blocking submit on empty required fields), set `[allowTyping]="true"`.
|
|
328
|
-
|
|
329
|
-
```html
|
|
330
|
-
<ngxsmk-datepicker [field]="myForm.date" [allowTyping]="true" required ...></ngxsmk-datepicker>
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
## Date Range Forms
|
|
334
|
-
|
|
335
|
-
For date range selection:
|
|
336
|
-
|
|
337
|
-
```typescript
|
|
338
|
-
@Component({
|
|
339
|
-
// ...
|
|
340
|
-
})
|
|
341
|
-
export class RangeFormComponent {
|
|
342
|
-
localObject = signal({
|
|
343
|
-
startDate: new Date(),
|
|
344
|
-
endDate: new Date()
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
myForm = form(this.localObject, objectSchema({
|
|
348
|
-
startDate: objectSchema<Date>(),
|
|
349
|
-
endDate: objectSchema<Date>()
|
|
350
|
-
}));
|
|
351
|
-
}
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
```html
|
|
355
|
-
<ngxsmk-datepicker
|
|
356
|
-
[field]="myForm.startDate"
|
|
357
|
-
mode="single"
|
|
358
|
-
placeholder="Start date">
|
|
359
|
-
</ngxsmk-datepicker>
|
|
360
|
-
|
|
361
|
-
<ngxsmk-datepicker
|
|
362
|
-
[field]="myForm.endDate"
|
|
363
|
-
mode="single"
|
|
364
|
-
placeholder="End date">
|
|
365
|
-
</ngxsmk-datepicker>
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
Or use a single range picker:
|
|
369
|
-
|
|
370
|
-
```typescript
|
|
371
|
-
localObject = signal({
|
|
372
|
-
dateRange: { start: new Date(), end: new Date() } as { start: Date; end: Date } | null
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
myForm = form(this.localObject, objectSchema({
|
|
376
|
-
dateRange: objectSchema<{ start: Date; end: Date } | null>()
|
|
377
|
-
}));
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
```html
|
|
381
|
-
<ngxsmk-datepicker
|
|
382
|
-
[field]="myForm.dateRange"
|
|
383
|
-
mode="range">
|
|
384
|
-
</ngxsmk-datepicker>
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
## Migration from Reactive Forms
|
|
388
|
-
|
|
389
|
-
### Before (Reactive Forms)
|
|
390
|
-
|
|
391
|
-
```typescript
|
|
392
|
-
import { FormGroup, FormControl } from '@angular/forms';
|
|
393
|
-
|
|
394
|
-
export class OldFormComponent {
|
|
395
|
-
form = new FormGroup({
|
|
396
|
-
date: new FormControl<Date | null>(null)
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
```html
|
|
402
|
-
<ngxsmk-datepicker
|
|
403
|
-
formControlName="date"
|
|
404
|
-
mode="single">
|
|
405
|
-
</ngxsmk-datepicker>
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
### After (Signal Forms)
|
|
409
|
-
|
|
410
|
-
```typescript
|
|
411
|
-
import { signal, form, objectSchema } from '@angular/core';
|
|
412
|
-
|
|
413
|
-
export class NewFormComponent {
|
|
414
|
-
localObject = signal({ date: null as Date | null });
|
|
415
|
-
|
|
416
|
-
myForm = form(this.localObject, objectSchema({
|
|
417
|
-
date: objectSchema<Date | null>()
|
|
418
|
-
}));
|
|
419
|
-
}
|
|
420
|
-
```
|
|
421
|
-
|
|
422
|
-
```html
|
|
423
|
-
<ngxsmk-datepicker
|
|
424
|
-
[field]="myForm.date"
|
|
425
|
-
mode="single">
|
|
426
|
-
</ngxsmk-datepicker>
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
## Benefits of Signal Forms
|
|
430
|
-
|
|
431
|
-
1. **Automatic Synchronization**: The `[field]` input automatically syncs with form state
|
|
432
|
-
2. **Reactive Updates**: Changes to the form field automatically update the datepicker
|
|
433
|
-
3. **Server Integration**: Works seamlessly with `httpResource` and `linkedSignal`
|
|
434
|
-
4. **Type Safety**: Full TypeScript support with proper types
|
|
435
|
-
5. **Performance**: Signals provide better performance than traditional reactive forms
|
|
436
|
-
|
|
437
|
-
## Server-Side Data with Manual Binding (Workaround Pattern)
|
|
438
|
-
|
|
439
|
-
If you're experiencing issues with `[field]` binding not populating initial values from the server, or if you need to work around readonly form limitations, use this pattern that ensures both initial population and updates work correctly:
|
|
440
|
-
|
|
441
|
-
```typescript
|
|
442
|
-
import { Component, signal, computed, form, objectSchema } from '@angular/core';
|
|
443
|
-
import { NgxsmkDatepickerComponent, DatepickerValue } from 'ngxsmk-datepicker';
|
|
444
|
-
|
|
445
|
-
@Component({
|
|
446
|
-
selector: 'app-server-form-manual',
|
|
447
|
-
standalone: true,
|
|
448
|
-
imports: [NgxsmkDatepickerComponent],
|
|
449
|
-
template: `
|
|
450
|
-
<ngxsmk-datepicker
|
|
451
|
-
class="w-full border:none"
|
|
452
|
-
[value]="dateInQuestion()"
|
|
453
|
-
(valueChange)="onDateChange($any($event))"
|
|
454
|
-
mode="single"
|
|
455
|
-
placeholder="Select a date">
|
|
456
|
-
</ngxsmk-datepicker>
|
|
457
|
-
`
|
|
458
|
-
})
|
|
459
|
-
export class ServerFormManualComponent {
|
|
460
|
-
localObject = signal<{ dateInQuestion: Date | null }>({
|
|
461
|
-
dateInQuestion: null
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
myForm = form(this.localObject, objectSchema({
|
|
465
|
-
dateInQuestion: objectSchema<Date | null>()
|
|
466
|
-
}));
|
|
467
|
-
|
|
468
|
-
dateInQuestion = computed(() => {
|
|
469
|
-
const value = this.myForm.value().dateInQuestion;
|
|
470
|
-
if (value && typeof value === 'string') {
|
|
471
|
-
return new Date(value);
|
|
472
|
-
}
|
|
473
|
-
return value;
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
onDateChange(newDate: DatepickerValue | null): void {
|
|
477
|
-
if (newDate) {
|
|
478
|
-
const dateValue = newDate instanceof Date
|
|
479
|
-
? newDate
|
|
480
|
-
: new Date(newDate.toLocaleString());
|
|
481
|
-
|
|
482
|
-
this.localObject.update(obj => ({
|
|
483
|
-
...obj,
|
|
484
|
-
dateInQuestion: dateValue
|
|
485
|
-
}));
|
|
486
|
-
} else {
|
|
487
|
-
this.localObject.update(obj => ({
|
|
488
|
-
...obj,
|
|
489
|
-
dateInQuestion: null
|
|
490
|
-
}));
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
updateFormFromServer(serverDate: Date | string): void {
|
|
495
|
-
const dateValue = serverDate instanceof Date
|
|
496
|
-
? serverDate
|
|
497
|
-
: new Date(serverDate);
|
|
498
|
-
|
|
499
|
-
this.localObject.update(obj => ({
|
|
500
|
-
...obj,
|
|
501
|
-
dateInQuestion: dateValue
|
|
502
|
-
}));
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
resetForm(): void {
|
|
506
|
-
this.localObject.set({
|
|
507
|
-
dateInQuestion: null
|
|
508
|
-
});
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
**Key points of this pattern:**
|
|
514
|
-
- Uses `computed()` to create a reactive signal that reads from the form value
|
|
515
|
-
- Updates the underlying `localObject` signal when dates change, which automatically updates the form
|
|
516
|
-
- Ensures initial server values populate correctly by updating `localObject` when data arrives
|
|
517
|
-
- Works around readonly form limitations by not directly binding to the form field
|
|
518
|
-
- Handles both initial population and subsequent updates
|
|
519
|
-
|
|
520
|
-
**When to use this pattern:**
|
|
521
|
-
- When `[field]` binding doesn't populate initial server values
|
|
522
|
-
- When working with readonly form signals
|
|
523
|
-
- When you need more control over when form updates occur
|
|
524
|
-
- When you need to handle date format conversions (string to Date)
|
|
525
|
-
|
|
526
|
-
## Troubleshooting
|
|
527
|
-
|
|
528
|
-
### Field not updating
|
|
529
|
-
|
|
530
|
-
If the field value isn't updating, ensure:
|
|
531
|
-
1. The field is properly initialized: `localObject = signal({ date: ... })`
|
|
532
|
-
2. The form is created correctly: `form(this.localObject, objectSchema({ ... }))`
|
|
533
|
-
3. The field reference is correct: `[field]="myForm.date"` (not `myForm().date`)
|
|
534
|
-
|
|
535
|
-
### Initial value not showing
|
|
536
|
-
|
|
537
|
-
If the initial value from the server isn't showing:
|
|
538
|
-
|
|
539
|
-
**With `[field]` binding:**
|
|
540
|
-
1. Ensure `localObject` is initialized with the server data
|
|
541
|
-
2. Use `linkedSignal` for reactive server data
|
|
542
|
-
3. Check that the date value is a valid Date object
|
|
543
|
-
4. If using readonly form, consider the manual binding pattern above
|
|
544
|
-
|
|
545
|
-
**With manual `[value]` binding:**
|
|
546
|
-
1. Use a `computed()` signal that reads from `myForm.value().fieldName`
|
|
547
|
-
2. Update the underlying `localObject` signal when server data arrives
|
|
548
|
-
3. Ensure the computed signal is properly reactive to form changes
|
|
549
|
-
4. Convert string dates to Date objects if your server returns strings
|
|
550
|
-
|
|
551
|
-
### Readonly form signal issues
|
|
552
|
-
|
|
553
|
-
If you're using `protected readonly form = form(...)` and controls aren't updating:
|
|
554
|
-
|
|
555
|
-
1. **Option 1**: Remove `readonly` if possible
|
|
556
|
-
2. **Option 2**: Use the manual binding pattern with `[value]` and `(valueChange)` shown above
|
|
557
|
-
3. **Option 3**: Update the underlying `localObject` signal instead of the form directly
|
|
558
|
-
|
|
559
|
-
### Disabled state not working
|
|
560
|
-
|
|
561
|
-
The datepicker automatically reads `field.disabled()`. If it's not working:
|
|
562
|
-
1. Ensure the field has a `disabled` property or function
|
|
563
|
-
2. Check that the form validation is set up correctly
|
|
564
|
-
3. When using manual binding, you may need to manually bind `[disabledState]`
|
|
565
|
-
|
|
566
|
-
### Form not marking as dirty
|
|
567
|
-
|
|
568
|
-
If `form().dirty()` returns `false` after selecting a date:
|
|
569
|
-
|
|
570
|
-
1. **Ensure you're using `[field]` binding**: The `[field]` input automatically uses the form's API methods to track dirty state.
|
|
571
|
-
```html
|
|
572
|
-
<!-- ✅ Correct - uses [field] binding -->
|
|
573
|
-
<ngxsmk-datepicker [field]="myForm.dateField" mode="single"></ngxsmk-datepicker>
|
|
574
|
-
```
|
|
575
|
-
|
|
576
|
-
2. **Avoid mixing `[field]` with manual `(valueChange)`**: Don't use both together, as the manual handler may bypass form tracking:
|
|
577
|
-
```html
|
|
578
|
-
<!-- ❌ Incorrect - manual handler bypasses form API -->
|
|
579
|
-
<ngxsmk-datepicker
|
|
580
|
-
[field]="myForm.dateField"
|
|
581
|
-
(valueChange)="dateField.set($event)"
|
|
582
|
-
mode="single">
|
|
583
|
-
</ngxsmk-datepicker>
|
|
584
|
-
```
|
|
585
|
-
|
|
586
|
-
3. **Use form API methods in manual handlers**: If you must use manual binding, use `setValue()` or `updateValue()`:
|
|
587
|
-
```typescript
|
|
588
|
-
onDateChange(newDate: Date): void {
|
|
589
|
-
// ✅ Correct - uses form API
|
|
590
|
-
this.myForm.dateField.setValue(newDate);
|
|
591
|
-
|
|
592
|
-
// ❌ Incorrect - bypasses dirty tracking
|
|
593
|
-
// this.dateField.set(newDate);
|
|
594
|
-
}
|
|
595
|
-
```
|
|
596
|
-
|
|
597
|
-
4. **Check dev console warnings**: In development mode, the datepicker logs warnings if it falls back to direct signal mutation, which may not track dirty state correctly.
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
1
|
+
# Signal Forms Integration
|
|
2
|
+
|
|
3
|
+
**Last updated:** July 2, 2026 - **Current stable:** v2.4.0
|
|
4
|
+
|
|
5
|
+
This guide covers using ngxsmk-datepicker with Angular 21+ Signal Forms API.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
Angular 21 introduced Signal Forms, a new reactive forms API built on signals. The datepicker provides first-class support through the `[field]` input, enabling seamless two-way binding with signal form fields.
|
|
10
|
+
|
|
11
|
+
## Basic Signal Forms Setup
|
|
12
|
+
|
|
13
|
+
### Creating a Signal Form
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { Component, signal, form, objectSchema } from '@angular/core';
|
|
17
|
+
import { NgxsmkDatepickerComponent } from 'ngxsmk-datepicker';
|
|
18
|
+
|
|
19
|
+
@Component({
|
|
20
|
+
selector: 'app-signal-form',
|
|
21
|
+
standalone: true,
|
|
22
|
+
imports: [NgxsmkDatepickerComponent],
|
|
23
|
+
template: `
|
|
24
|
+
<form>
|
|
25
|
+
<ngxsmk-datepicker
|
|
26
|
+
[field]="myForm.dateInQuestion"
|
|
27
|
+
mode="single"
|
|
28
|
+
placeholder="Select a date">
|
|
29
|
+
</ngxsmk-datepicker>
|
|
30
|
+
</form>
|
|
31
|
+
`
|
|
32
|
+
})
|
|
33
|
+
export class SignalFormComponent {
|
|
34
|
+
// Create a signal for your form data
|
|
35
|
+
localObject = signal({
|
|
36
|
+
dateInQuestion: new Date(),
|
|
37
|
+
name: 'John Doe'
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Create a signal form
|
|
41
|
+
myForm = form(this.localObject, objectSchema({
|
|
42
|
+
dateInQuestion: objectSchema<Date>(),
|
|
43
|
+
name: objectSchema<string>()
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Server-Side Data Integration
|
|
49
|
+
|
|
50
|
+
### With httpResource
|
|
51
|
+
|
|
52
|
+
When data comes from a server, use `httpResource` with signal forms:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { Component, inject, signal, linkedSignal, computed } from '@angular/core';
|
|
56
|
+
import { httpResource } from '@angular/common/http';
|
|
57
|
+
import { HttpClient } from '@angular/common/http';
|
|
58
|
+
import { form, objectSchema } from '@angular/forms';
|
|
59
|
+
import { NgxsmkDatepickerComponent } from 'ngxsmk-datepicker';
|
|
60
|
+
|
|
61
|
+
@Component({
|
|
62
|
+
selector: 'app-server-form',
|
|
63
|
+
standalone: true,
|
|
64
|
+
imports: [NgxsmkDatepickerComponent],
|
|
65
|
+
template: `
|
|
66
|
+
<form>
|
|
67
|
+
<ngxsmk-datepicker
|
|
68
|
+
[field]="myForm.dateInQuestion"
|
|
69
|
+
mode="single"
|
|
70
|
+
placeholder="Select a date">
|
|
71
|
+
</ngxsmk-datepicker>
|
|
72
|
+
</form>
|
|
73
|
+
`
|
|
74
|
+
})
|
|
75
|
+
export class ServerFormComponent {
|
|
76
|
+
private http = inject(HttpClient);
|
|
77
|
+
|
|
78
|
+
// Fetch data from server
|
|
79
|
+
resource = httpResource({
|
|
80
|
+
request: () => this.http.get<{ dateInQuestion: Date }>('/api/data'),
|
|
81
|
+
loader: signal(false)
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Link the response to a signal
|
|
85
|
+
localObject = linkedSignal(() => this.resource.response.value() || {
|
|
86
|
+
dateInQuestion: new Date()
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Create form from linked signal
|
|
90
|
+
myForm = form(this.localObject, objectSchema({
|
|
91
|
+
dateInQuestion: objectSchema<Date>()
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Two-Way Binding
|
|
97
|
+
|
|
98
|
+
The `[field]` input provides automatic two-way binding:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
@Component({
|
|
102
|
+
// ...
|
|
103
|
+
})
|
|
104
|
+
export class TwoWayComponent {
|
|
105
|
+
localObject = signal({ date: new Date() });
|
|
106
|
+
|
|
107
|
+
myForm = form(this.localObject, objectSchema({
|
|
108
|
+
date: objectSchema<Date>()
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
// The datepicker automatically:
|
|
112
|
+
// 1. Reads from myForm.date.value()
|
|
113
|
+
// 2. Updates myForm.date when user selects a date
|
|
114
|
+
// 3. Handles disabled state from myForm.date.disabled()
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Signal Field Resolution (v2.0.5+)
|
|
119
|
+
|
|
120
|
+
The datepicker includes a robust resolution mechanism for signal-based fields. It can handle:
|
|
121
|
+
- **Direct Signals**: A signal that contains the field configuration.
|
|
122
|
+
- **Signals with Properties**: A signal function that has field properties (like `value`, `disabled`, `setValue`) attached directly to it (common in some Signal Form implementations).
|
|
123
|
+
- **Nested Signals**: Signals that return a field configuration object when executed.
|
|
124
|
+
|
|
125
|
+
The datepicker intelligently detects these patterns and unwraps them automatically.
|
|
126
|
+
|
|
127
|
+
### Enhanced Type Safety
|
|
128
|
+
|
|
129
|
+
The library now exports `SignalFormFieldConfig` to allow you to strictly type your field configurations:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { SignalFormFieldConfig } from 'ngxsmk-datepicker';
|
|
133
|
+
|
|
134
|
+
const config: SignalFormFieldConfig = {
|
|
135
|
+
value: signal(new Date()),
|
|
136
|
+
disabled: () => false,
|
|
137
|
+
required: true
|
|
138
|
+
};
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**TypeScript Compatibility (v2.0.5+):**
|
|
142
|
+
|
|
143
|
+
The datepicker is fully compatible with Angular 21+ `FieldTree<string | Date | null, string>` structure. The types accept:
|
|
144
|
+
- `WritableSignal<Date | null>` for date values
|
|
145
|
+
- `WritableSignal<string | null>` for string date values (automatically converted to Date)
|
|
146
|
+
- Any Angular Signal Forms field configuration
|
|
147
|
+
|
|
148
|
+
String values from Signal Forms are automatically normalized to Date objects internally, ensuring seamless integration with Angular 21+ forms.
|
|
149
|
+
|
|
150
|
+
## Dirty State Tracking
|
|
151
|
+
|
|
152
|
+
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:
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
@Component({
|
|
156
|
+
selector: 'app-form',
|
|
157
|
+
standalone: true,
|
|
158
|
+
imports: [NgxsmkDatepickerComponent],
|
|
159
|
+
template: `
|
|
160
|
+
<form>
|
|
161
|
+
<ngxsmk-datepicker
|
|
162
|
+
[field]="myForm.dateDue"
|
|
163
|
+
mode="single"
|
|
164
|
+
placeholder="Select a date">
|
|
165
|
+
</ngxsmk-datepicker>
|
|
166
|
+
|
|
167
|
+
<button
|
|
168
|
+
type="submit"
|
|
169
|
+
[disabled]="!myForm().dirty()">
|
|
170
|
+
Save Changes
|
|
171
|
+
</button>
|
|
172
|
+
</form>
|
|
173
|
+
`
|
|
174
|
+
})
|
|
175
|
+
export class FormComponent {
|
|
176
|
+
action = signal({ dateDue: new Date() });
|
|
177
|
+
myForm = form(this.action, objectSchema({
|
|
178
|
+
dateDue: objectSchema<Date>()
|
|
179
|
+
}));
|
|
180
|
+
|
|
181
|
+
submitForm() {
|
|
182
|
+
if (!this.myForm().dirty()) {
|
|
183
|
+
console.log('No changes to save');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
// Submit form...
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Important Notes:**
|
|
192
|
+
|
|
193
|
+
1. **Use `[field]` binding for automatic dirty tracking**: The datepicker uses the field's `setValue()` or `updateValue()` methods when available, which properly track dirty state in Angular Signal Forms.
|
|
194
|
+
|
|
195
|
+
2. **Avoid mixing `[field]` with manual `(valueChange)` handlers**: If you use both `[field]` and `(valueChange)="dateField.set($event)"`, the manual handler bypasses the form API and may prevent dirty state tracking. Use one or the other:
|
|
196
|
+
- ✅ **Recommended**: Use only `[field]="myForm.dateField"` for automatic dirty tracking
|
|
197
|
+
- ⚠️ **Alternative**: Use `[value]` and `(valueChange)` with proper form API methods if you need manual control
|
|
198
|
+
|
|
199
|
+
3. **Manual binding pattern**: If you must use manual binding (e.g., for stability issues), ensure you update the form using the field's API methods:
|
|
200
|
+
```typescript
|
|
201
|
+
onDateChange(newDate: Date): void {
|
|
202
|
+
// Use setValue to ensure dirty tracking works
|
|
203
|
+
if (typeof this.myForm.dateField.setValue === 'function') {
|
|
204
|
+
this.myForm.dateField.setValue(newDate);
|
|
205
|
+
} else if (typeof this.myForm.dateField.updateValue === 'function') {
|
|
206
|
+
this.myForm.dateField.updateValue(() => newDate);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
4. **Dev mode warnings**: If the datepicker cannot use `setValue()` or `updateValue()` (e.g., field doesn't provide these methods), it will fall back to direct signal mutation and log a warning in dev mode. This fallback may not track dirty state correctly.
|
|
212
|
+
|
|
213
|
+
## Manual Updates
|
|
214
|
+
|
|
215
|
+
You can also manually update form values:
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
@Component({
|
|
219
|
+
// ...
|
|
220
|
+
})
|
|
221
|
+
export class ManualUpdateComponent {
|
|
222
|
+
localObject = signal({ date: new Date() });
|
|
223
|
+
|
|
224
|
+
myForm = form(this.localObject, objectSchema({
|
|
225
|
+
date: objectSchema<Date>()
|
|
226
|
+
}));
|
|
227
|
+
|
|
228
|
+
updateDate(newDate: Date) {
|
|
229
|
+
// Option 1: Update the underlying signal
|
|
230
|
+
this.localObject.update(obj => ({
|
|
231
|
+
...obj,
|
|
232
|
+
date: newDate
|
|
233
|
+
}));
|
|
234
|
+
|
|
235
|
+
// Option 2: Use form field's setValue (if available)
|
|
236
|
+
if (typeof this.myForm.date.setValue === 'function') {
|
|
237
|
+
this.myForm.date.setValue(newDate);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Alternative: Manual Binding with valueChange (Stabilized Pattern)
|
|
244
|
+
|
|
245
|
+
If you experience stability issues with the `[field]` binding, you can use manual binding with `[value]` and `(valueChange)`. **Important**: To ensure dirty state tracking works correctly, use the field's API methods instead of direct mutation:
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
import { Component, signal, computed, form, objectSchema } from '@angular/core';
|
|
249
|
+
import { NgxsmkDatepickerComponent } from 'ngxsmk-datepicker';
|
|
250
|
+
|
|
251
|
+
@Component({
|
|
252
|
+
selector: 'app-stable-form',
|
|
253
|
+
standalone: true,
|
|
254
|
+
imports: [NgxsmkDatepickerComponent],
|
|
255
|
+
template: `
|
|
256
|
+
<ngxsmk-datepicker
|
|
257
|
+
class="w-full border:none"
|
|
258
|
+
[value]="myDate()"
|
|
259
|
+
(valueChange)="onMyDateChange($any($event))"
|
|
260
|
+
mode="single"
|
|
261
|
+
placeholder="Select a date">
|
|
262
|
+
</ngxsmk-datepicker>
|
|
263
|
+
`
|
|
264
|
+
})
|
|
265
|
+
export class StableFormComponent {
|
|
266
|
+
localObject = signal({ myDate: new Date() });
|
|
267
|
+
|
|
268
|
+
myForm = form(this.localObject, objectSchema({
|
|
269
|
+
myDate: objectSchema<Date>()
|
|
270
|
+
}));
|
|
271
|
+
|
|
272
|
+
// Get a signal reference to the date field value
|
|
273
|
+
myDate = computed(() => this.myForm.value().myDate);
|
|
274
|
+
|
|
275
|
+
onMyDateChange(newDate: Date): void {
|
|
276
|
+
// Use setValue to ensure dirty state tracking works
|
|
277
|
+
if (typeof this.myForm.myDate.setValue === 'function') {
|
|
278
|
+
this.myForm.myDate.setValue(newDate);
|
|
279
|
+
} else if (typeof this.myForm.myDate.updateValue === 'function') {
|
|
280
|
+
this.myForm.myDate.updateValue(() => newDate);
|
|
281
|
+
} else {
|
|
282
|
+
// Fallback: directly mutate the form value object
|
|
283
|
+
// Note: This may not track dirty state correctly
|
|
284
|
+
this.myForm.value().myDate = newDate;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**Why this pattern works:**
|
|
291
|
+
- Uses form API methods (`setValue`/`updateValue`) to ensure dirty state tracking
|
|
292
|
+
- Prevents potential change detection loops
|
|
293
|
+
- More explicit control over when updates occur
|
|
294
|
+
- Useful when `[field]` binding causes stability issues
|
|
295
|
+
|
|
296
|
+
**Note:** The `$any($event)` cast may be needed if there's a type mismatch between `DatepickerValue` and your expected `Date` type.
|
|
297
|
+
|
|
298
|
+
**⚠️ Warning**: Direct mutation (`this.myForm.value().myDate = newDate`) bypasses Angular's dirty tracking mechanism. Always prefer using `setValue()` or `updateValue()` when available.
|
|
299
|
+
|
|
300
|
+
## Validation
|
|
301
|
+
|
|
302
|
+
Signal Forms support validation. The datepicker respects the field's disabled state:
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
import { Component, signal, form, objectSchema, validators } from '@angular/core';
|
|
306
|
+
|
|
307
|
+
@Component({
|
|
308
|
+
// ...
|
|
309
|
+
})
|
|
310
|
+
export class ValidatedFormComponent {
|
|
311
|
+
localObject = signal({ date: null as Date | null });
|
|
312
|
+
|
|
313
|
+
myForm = form(this.localObject, objectSchema({
|
|
314
|
+
date: objectSchema<Date | null>({
|
|
315
|
+
validators: [
|
|
316
|
+
validators.required()
|
|
317
|
+
]
|
|
318
|
+
})
|
|
319
|
+
}));
|
|
320
|
+
|
|
321
|
+
// The datepicker will automatically reflect the disabled state
|
|
322
|
+
// when the field is invalid or disabled
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Note on Native Validation
|
|
327
|
+
By default, the datepicker input is `readonly`. Browsers do not validate `readonly` fields. To enable native browser validation (e.g., blocking submit on empty required fields), set `[allowTyping]="true"`.
|
|
328
|
+
|
|
329
|
+
```html
|
|
330
|
+
<ngxsmk-datepicker [field]="myForm.date" [allowTyping]="true" required ...></ngxsmk-datepicker>
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Date Range Forms
|
|
334
|
+
|
|
335
|
+
For date range selection:
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
@Component({
|
|
339
|
+
// ...
|
|
340
|
+
})
|
|
341
|
+
export class RangeFormComponent {
|
|
342
|
+
localObject = signal({
|
|
343
|
+
startDate: new Date(),
|
|
344
|
+
endDate: new Date()
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
myForm = form(this.localObject, objectSchema({
|
|
348
|
+
startDate: objectSchema<Date>(),
|
|
349
|
+
endDate: objectSchema<Date>()
|
|
350
|
+
}));
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
```html
|
|
355
|
+
<ngxsmk-datepicker
|
|
356
|
+
[field]="myForm.startDate"
|
|
357
|
+
mode="single"
|
|
358
|
+
placeholder="Start date">
|
|
359
|
+
</ngxsmk-datepicker>
|
|
360
|
+
|
|
361
|
+
<ngxsmk-datepicker
|
|
362
|
+
[field]="myForm.endDate"
|
|
363
|
+
mode="single"
|
|
364
|
+
placeholder="End date">
|
|
365
|
+
</ngxsmk-datepicker>
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Or use a single range picker:
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
localObject = signal({
|
|
372
|
+
dateRange: { start: new Date(), end: new Date() } as { start: Date; end: Date } | null
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
myForm = form(this.localObject, objectSchema({
|
|
376
|
+
dateRange: objectSchema<{ start: Date; end: Date } | null>()
|
|
377
|
+
}));
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
```html
|
|
381
|
+
<ngxsmk-datepicker
|
|
382
|
+
[field]="myForm.dateRange"
|
|
383
|
+
mode="range">
|
|
384
|
+
</ngxsmk-datepicker>
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## Migration from Reactive Forms
|
|
388
|
+
|
|
389
|
+
### Before (Reactive Forms)
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
import { FormGroup, FormControl } from '@angular/forms';
|
|
393
|
+
|
|
394
|
+
export class OldFormComponent {
|
|
395
|
+
form = new FormGroup({
|
|
396
|
+
date: new FormControl<Date | null>(null)
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
```html
|
|
402
|
+
<ngxsmk-datepicker
|
|
403
|
+
formControlName="date"
|
|
404
|
+
mode="single">
|
|
405
|
+
</ngxsmk-datepicker>
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### After (Signal Forms)
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
import { signal, form, objectSchema } from '@angular/core';
|
|
412
|
+
|
|
413
|
+
export class NewFormComponent {
|
|
414
|
+
localObject = signal({ date: null as Date | null });
|
|
415
|
+
|
|
416
|
+
myForm = form(this.localObject, objectSchema({
|
|
417
|
+
date: objectSchema<Date | null>()
|
|
418
|
+
}));
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
```html
|
|
423
|
+
<ngxsmk-datepicker
|
|
424
|
+
[field]="myForm.date"
|
|
425
|
+
mode="single">
|
|
426
|
+
</ngxsmk-datepicker>
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
## Benefits of Signal Forms
|
|
430
|
+
|
|
431
|
+
1. **Automatic Synchronization**: The `[field]` input automatically syncs with form state
|
|
432
|
+
2. **Reactive Updates**: Changes to the form field automatically update the datepicker
|
|
433
|
+
3. **Server Integration**: Works seamlessly with `httpResource` and `linkedSignal`
|
|
434
|
+
4. **Type Safety**: Full TypeScript support with proper types
|
|
435
|
+
5. **Performance**: Signals provide better performance than traditional reactive forms
|
|
436
|
+
|
|
437
|
+
## Server-Side Data with Manual Binding (Workaround Pattern)
|
|
438
|
+
|
|
439
|
+
If you're experiencing issues with `[field]` binding not populating initial values from the server, or if you need to work around readonly form limitations, use this pattern that ensures both initial population and updates work correctly:
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
import { Component, signal, computed, form, objectSchema } from '@angular/core';
|
|
443
|
+
import { NgxsmkDatepickerComponent, DatepickerValue } from 'ngxsmk-datepicker';
|
|
444
|
+
|
|
445
|
+
@Component({
|
|
446
|
+
selector: 'app-server-form-manual',
|
|
447
|
+
standalone: true,
|
|
448
|
+
imports: [NgxsmkDatepickerComponent],
|
|
449
|
+
template: `
|
|
450
|
+
<ngxsmk-datepicker
|
|
451
|
+
class="w-full border:none"
|
|
452
|
+
[value]="dateInQuestion()"
|
|
453
|
+
(valueChange)="onDateChange($any($event))"
|
|
454
|
+
mode="single"
|
|
455
|
+
placeholder="Select a date">
|
|
456
|
+
</ngxsmk-datepicker>
|
|
457
|
+
`
|
|
458
|
+
})
|
|
459
|
+
export class ServerFormManualComponent {
|
|
460
|
+
localObject = signal<{ dateInQuestion: Date | null }>({
|
|
461
|
+
dateInQuestion: null
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
myForm = form(this.localObject, objectSchema({
|
|
465
|
+
dateInQuestion: objectSchema<Date | null>()
|
|
466
|
+
}));
|
|
467
|
+
|
|
468
|
+
dateInQuestion = computed(() => {
|
|
469
|
+
const value = this.myForm.value().dateInQuestion;
|
|
470
|
+
if (value && typeof value === 'string') {
|
|
471
|
+
return new Date(value);
|
|
472
|
+
}
|
|
473
|
+
return value;
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
onDateChange(newDate: DatepickerValue | null): void {
|
|
477
|
+
if (newDate) {
|
|
478
|
+
const dateValue = newDate instanceof Date
|
|
479
|
+
? newDate
|
|
480
|
+
: new Date(newDate.toLocaleString());
|
|
481
|
+
|
|
482
|
+
this.localObject.update(obj => ({
|
|
483
|
+
...obj,
|
|
484
|
+
dateInQuestion: dateValue
|
|
485
|
+
}));
|
|
486
|
+
} else {
|
|
487
|
+
this.localObject.update(obj => ({
|
|
488
|
+
...obj,
|
|
489
|
+
dateInQuestion: null
|
|
490
|
+
}));
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
updateFormFromServer(serverDate: Date | string): void {
|
|
495
|
+
const dateValue = serverDate instanceof Date
|
|
496
|
+
? serverDate
|
|
497
|
+
: new Date(serverDate);
|
|
498
|
+
|
|
499
|
+
this.localObject.update(obj => ({
|
|
500
|
+
...obj,
|
|
501
|
+
dateInQuestion: dateValue
|
|
502
|
+
}));
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
resetForm(): void {
|
|
506
|
+
this.localObject.set({
|
|
507
|
+
dateInQuestion: null
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
**Key points of this pattern:**
|
|
514
|
+
- Uses `computed()` to create a reactive signal that reads from the form value
|
|
515
|
+
- Updates the underlying `localObject` signal when dates change, which automatically updates the form
|
|
516
|
+
- Ensures initial server values populate correctly by updating `localObject` when data arrives
|
|
517
|
+
- Works around readonly form limitations by not directly binding to the form field
|
|
518
|
+
- Handles both initial population and subsequent updates
|
|
519
|
+
|
|
520
|
+
**When to use this pattern:**
|
|
521
|
+
- When `[field]` binding doesn't populate initial server values
|
|
522
|
+
- When working with readonly form signals
|
|
523
|
+
- When you need more control over when form updates occur
|
|
524
|
+
- When you need to handle date format conversions (string to Date)
|
|
525
|
+
|
|
526
|
+
## Troubleshooting
|
|
527
|
+
|
|
528
|
+
### Field not updating
|
|
529
|
+
|
|
530
|
+
If the field value isn't updating, ensure:
|
|
531
|
+
1. The field is properly initialized: `localObject = signal({ date: ... })`
|
|
532
|
+
2. The form is created correctly: `form(this.localObject, objectSchema({ ... }))`
|
|
533
|
+
3. The field reference is correct: `[field]="myForm.date"` (not `myForm().date`)
|
|
534
|
+
|
|
535
|
+
### Initial value not showing
|
|
536
|
+
|
|
537
|
+
If the initial value from the server isn't showing:
|
|
538
|
+
|
|
539
|
+
**With `[field]` binding:**
|
|
540
|
+
1. Ensure `localObject` is initialized with the server data
|
|
541
|
+
2. Use `linkedSignal` for reactive server data
|
|
542
|
+
3. Check that the date value is a valid Date object
|
|
543
|
+
4. If using readonly form, consider the manual binding pattern above
|
|
544
|
+
|
|
545
|
+
**With manual `[value]` binding:**
|
|
546
|
+
1. Use a `computed()` signal that reads from `myForm.value().fieldName`
|
|
547
|
+
2. Update the underlying `localObject` signal when server data arrives
|
|
548
|
+
3. Ensure the computed signal is properly reactive to form changes
|
|
549
|
+
4. Convert string dates to Date objects if your server returns strings
|
|
550
|
+
|
|
551
|
+
### Readonly form signal issues
|
|
552
|
+
|
|
553
|
+
If you're using `protected readonly form = form(...)` and controls aren't updating:
|
|
554
|
+
|
|
555
|
+
1. **Option 1**: Remove `readonly` if possible
|
|
556
|
+
2. **Option 2**: Use the manual binding pattern with `[value]` and `(valueChange)` shown above
|
|
557
|
+
3. **Option 3**: Update the underlying `localObject` signal instead of the form directly
|
|
558
|
+
|
|
559
|
+
### Disabled state not working
|
|
560
|
+
|
|
561
|
+
The datepicker automatically reads `field.disabled()`. If it's not working:
|
|
562
|
+
1. Ensure the field has a `disabled` property or function
|
|
563
|
+
2. Check that the form validation is set up correctly
|
|
564
|
+
3. When using manual binding, you may need to manually bind `[disabledState]`
|
|
565
|
+
|
|
566
|
+
### Form not marking as dirty
|
|
567
|
+
|
|
568
|
+
If `form().dirty()` returns `false` after selecting a date:
|
|
569
|
+
|
|
570
|
+
1. **Ensure you're using `[field]` binding**: The `[field]` input automatically uses the form's API methods to track dirty state.
|
|
571
|
+
```html
|
|
572
|
+
<!-- ✅ Correct - uses [field] binding -->
|
|
573
|
+
<ngxsmk-datepicker [field]="myForm.dateField" mode="single"></ngxsmk-datepicker>
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
2. **Avoid mixing `[field]` with manual `(valueChange)`**: Don't use both together, as the manual handler may bypass form tracking:
|
|
577
|
+
```html
|
|
578
|
+
<!-- ❌ Incorrect - manual handler bypasses form API -->
|
|
579
|
+
<ngxsmk-datepicker
|
|
580
|
+
[field]="myForm.dateField"
|
|
581
|
+
(valueChange)="dateField.set($event)"
|
|
582
|
+
mode="single">
|
|
583
|
+
</ngxsmk-datepicker>
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
3. **Use form API methods in manual handlers**: If you must use manual binding, use `setValue()` or `updateValue()`:
|
|
587
|
+
```typescript
|
|
588
|
+
onDateChange(newDate: Date): void {
|
|
589
|
+
// ✅ Correct - uses form API
|
|
590
|
+
this.myForm.dateField.setValue(newDate);
|
|
591
|
+
|
|
592
|
+
// ❌ Incorrect - bypasses dirty tracking
|
|
593
|
+
// this.dateField.set(newDate);
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
4. **Check dev console warnings**: In development mode, the datepicker logs warnings if it falls back to direct signal mutation, which may not track dirty state correctly.
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
|