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.
@@ -1,600 +1,600 @@
1
- # Signal Forms Integration
2
-
3
- **Last updated:** June 3, 2026 - **Current stable:** v2.3.1
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
+