ngxsmk-datepicker
Version:
<!-- SEO Keywords: Angular DatePicker, Angular Date Range Picker, Lightweight Calendar Component, Angular Signals DatePicker, SSR Ready DatePicker, Zoneless Angular, A11y DatePicker, Mobile-Friendly DatePicker, Ionic DatePicker Meta Description: The
600 lines (477 loc) • 18.9 kB
Markdown
# Signal Forms Integration
**Last updated:** March 21, 2026 · **Current stable:** v2.2.8
This guide covers using ngxsmk-datepicker with Angular 21+ Signal Forms API.
## Overview
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.
## Basic Signal Forms Setup
### Creating a Signal Form
```typescript
import { Component, signal, form, objectSchema } from '@angular/core';
import { NgxsmkDatepickerComponent } from 'ngxsmk-datepicker';
@Component({
selector: 'app-signal-form',
standalone: true,
imports: [NgxsmkDatepickerComponent],
template: `
<form>
<ngxsmk-datepicker
[field]="myForm.dateInQuestion"
mode="single"
placeholder="Select a date">
</ngxsmk-datepicker>
</form>
`
})
export class SignalFormComponent {
// Create a signal for your form data
localObject = signal({
dateInQuestion: new Date(),
name: 'John Doe'
});
// Create a signal form
myForm = form(this.localObject, objectSchema({
dateInQuestion: objectSchema<Date>(),
name: objectSchema<string>()
}));
}
```
## Server-Side Data Integration
### With httpResource
When data comes from a server, use `httpResource` with signal forms:
```typescript
import { Component, inject, signal, linkedSignal, computed } from '@angular/core';
import { httpResource } from '@angular/common/http';
import { HttpClient } from '@angular/common/http';
import { form, objectSchema } from '@angular/forms';
import { NgxsmkDatepickerComponent } from 'ngxsmk-datepicker';
@Component({
selector: 'app-server-form',
standalone: true,
imports: [NgxsmkDatepickerComponent],
template: `
<form>
<ngxsmk-datepicker
[field]="myForm.dateInQuestion"
mode="single"
placeholder="Select a date">
</ngxsmk-datepicker>
</form>
`
})
export class ServerFormComponent {
private http = inject(HttpClient);
// Fetch data from server
resource = httpResource({
request: () => this.http.get<{ dateInQuestion: Date }>('/api/data'),
loader: signal(false)
});
// Link the response to a signal
localObject = linkedSignal(() => this.resource.response.value() || {
dateInQuestion: new Date()
});
// Create form from linked signal
myForm = form(this.localObject, objectSchema({
dateInQuestion: objectSchema<Date>()
}));
}
```
## Two-Way Binding
The `[field]` input provides automatic two-way binding:
```typescript
@Component({
// ...
})
export class TwoWayComponent {
localObject = signal({ date: new Date() });
myForm = form(this.localObject, objectSchema({
date: objectSchema<Date>()
}));
// The datepicker automatically:
// 1. Reads from myForm.date.value()
// 2. Updates myForm.date when user selects a date
// 3. Handles disabled state from myForm.date.disabled()
}
```
### Signal Field Resolution (v2.0.5+)
The datepicker includes a robust resolution mechanism for signal-based fields. It can handle:
- **Direct Signals**: A signal that contains the field configuration.
- **Signals with Properties**: A signal function that has field properties (like `value`, `disabled`, `setValue`) attached directly to it (common in some Signal Form implementations).
- **Nested Signals**: Signals that return a field configuration object when executed.
The datepicker intelligently detects these patterns and unwraps them automatically.
### Enhanced Type Safety
The library now exports `SignalFormFieldConfig` to allow you to strictly type your field configurations:
```typescript
import { SignalFormFieldConfig } from 'ngxsmk-datepicker';
const config: SignalFormFieldConfig = {
value: signal(new Date()),
disabled: () => false,
required: true
};
```
**TypeScript Compatibility (v2.0.5+):**
The datepicker is fully compatible with Angular 21+ `FieldTree<string | Date | null, string>` structure. The types accept:
- `WritableSignal<Date | null>` for date values
- `WritableSignal<string | null>` for string date values (automatically converted to Date)
- Any Angular Signal Forms field configuration
String values from Signal Forms are automatically normalized to Date objects internally, ensuring seamless integration with Angular 21+ forms.
## Dirty State Tracking
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:
```typescript
@Component({
selector: 'app-form',
standalone: true,
imports: [NgxsmkDatepickerComponent],
template: `
<form>
<ngxsmk-datepicker
[field]="myForm.dateDue"
mode="single"
placeholder="Select a date">
</ngxsmk-datepicker>
<button
type="submit"
[disabled]="!myForm().dirty()">
Save Changes
</button>
</form>
`
})
export class FormComponent {
action = signal({ dateDue: new Date() });
myForm = form(this.action, objectSchema({
dateDue: objectSchema<Date>()
}));
submitForm() {
if (!this.myForm().dirty()) {
console.log('No changes to save');
return;
}
// Submit form...
}
}
```
**Important Notes:**
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.
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:
- ✅ **Recommended**: Use only `[field]="myForm.dateField"` for automatic dirty tracking
- ⚠️ **Alternative**: Use `[value]` and `(valueChange)` with proper form API methods if you need manual control
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:
```typescript
onDateChange(newDate: Date): void {
// Use setValue to ensure dirty tracking works
if (typeof this.myForm.dateField.setValue === 'function') {
this.myForm.dateField.setValue(newDate);
} else if (typeof this.myForm.dateField.updateValue === 'function') {
this.myForm.dateField.updateValue(() => newDate);
}
}
```
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.
## Manual Updates
You can also manually update form values:
```typescript
@Component({
// ...
})
export class ManualUpdateComponent {
localObject = signal({ date: new Date() });
myForm = form(this.localObject, objectSchema({
date: objectSchema<Date>()
}));
updateDate(newDate: Date) {
// Option 1: Update the underlying signal
this.localObject.update(obj => ({
...obj,
date: newDate
}));
// Option 2: Use form field's setValue (if available)
if (typeof this.myForm.date.setValue === 'function') {
this.myForm.date.setValue(newDate);
}
}
}
```
## Alternative: Manual Binding with valueChange (Stabilized Pattern)
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:
```typescript
import { Component, signal, computed, form, objectSchema } from '@angular/core';
import { NgxsmkDatepickerComponent } from 'ngxsmk-datepicker';
@Component({
selector: 'app-stable-form',
standalone: true,
imports: [NgxsmkDatepickerComponent],
template: `
<ngxsmk-datepicker
class="w-full border:none"
[value]="myDate()"
(valueChange)="onMyDateChange($any($event))"
mode="single"
placeholder="Select a date">
</ngxsmk-datepicker>
`
})
export class StableFormComponent {
localObject = signal({ myDate: new Date() });
myForm = form(this.localObject, objectSchema({
myDate: objectSchema<Date>()
}));
// Get a signal reference to the date field value
myDate = computed(() => this.myForm.value().myDate);
onMyDateChange(newDate: Date): void {
// Use setValue to ensure dirty state tracking works
if (typeof this.myForm.myDate.setValue === 'function') {
this.myForm.myDate.setValue(newDate);
} else if (typeof this.myForm.myDate.updateValue === 'function') {
this.myForm.myDate.updateValue(() => newDate);
} else {
// Fallback: directly mutate the form value object
// Note: This may not track dirty state correctly
this.myForm.value().myDate = newDate;
}
}
}
```
**Why this pattern works:**
- Uses form API methods (`setValue`/`updateValue`) to ensure dirty state tracking
- Prevents potential change detection loops
- More explicit control over when updates occur
- Useful when `[field]` binding causes stability issues
**Note:** The `$any($event)` cast may be needed if there's a type mismatch between `DatepickerValue` and your expected `Date` type.
**⚠️ Warning**: Direct mutation (`this.myForm.value().myDate = newDate`) bypasses Angular's dirty tracking mechanism. Always prefer using `setValue()` or `updateValue()` when available.
## Validation
Signal Forms support validation. The datepicker respects the field's disabled state:
```typescript
import { Component, signal, form, objectSchema, validators } from '@angular/core';
@Component({
// ...
})
export class ValidatedFormComponent {
localObject = signal({ date: null as Date | null });
myForm = form(this.localObject, objectSchema({
date: objectSchema<Date | null>({
validators: [
validators.required()
]
})
}));
// The datepicker will automatically reflect the disabled state
// when the field is invalid or disabled
}
```
### Note on Native Validation
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"`.
```html
<ngxsmk-datepicker [field]="myForm.date" [allowTyping]="true" required ...></ngxsmk-datepicker>
```
## Date Range Forms
For date range selection:
```typescript
@Component({
// ...
})
export class RangeFormComponent {
localObject = signal({
startDate: new Date(),
endDate: new Date()
});
myForm = form(this.localObject, objectSchema({
startDate: objectSchema<Date>(),
endDate: objectSchema<Date>()
}));
}
```
```html
<ngxsmk-datepicker
[field]="myForm.startDate"
mode="single"
placeholder="Start date">
</ngxsmk-datepicker>
<ngxsmk-datepicker
[field]="myForm.endDate"
mode="single"
placeholder="End date">
</ngxsmk-datepicker>
```
Or use a single range picker:
```typescript
localObject = signal({
dateRange: { start: new Date(), end: new Date() } as { start: Date; end: Date } | null
});
myForm = form(this.localObject, objectSchema({
dateRange: objectSchema<{ start: Date; end: Date } | null>()
}));
```
```html
<ngxsmk-datepicker
[field]="myForm.dateRange"
mode="range">
</ngxsmk-datepicker>
```
## Migration from Reactive Forms
### Before (Reactive Forms)
```typescript
import { FormGroup, FormControl } from '@angular/forms';
export class OldFormComponent {
form = new FormGroup({
date: new FormControl<Date | null>(null)
});
}
```
```html
<ngxsmk-datepicker
formControlName="date"
mode="single">
</ngxsmk-datepicker>
```
### After (Signal Forms)
```typescript
import { signal, form, objectSchema } from '@angular/core';
export class NewFormComponent {
localObject = signal({ date: null as Date | null });
myForm = form(this.localObject, objectSchema({
date: objectSchema<Date | null>()
}));
}
```
```html
<ngxsmk-datepicker
[field]="myForm.date"
mode="single">
</ngxsmk-datepicker>
```
## Benefits of Signal Forms
1. **Automatic Synchronization**: The `[field]` input automatically syncs with form state
2. **Reactive Updates**: Changes to the form field automatically update the datepicker
3. **Server Integration**: Works seamlessly with `httpResource` and `linkedSignal`
4. **Type Safety**: Full TypeScript support with proper types
5. **Performance**: Signals provide better performance than traditional reactive forms
## Server-Side Data with Manual Binding (Workaround Pattern)
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:
```typescript
import { Component, signal, computed, form, objectSchema } from '@angular/core';
import { NgxsmkDatepickerComponent, DatepickerValue } from 'ngxsmk-datepicker';
@Component({
selector: 'app-server-form-manual',
standalone: true,
imports: [NgxsmkDatepickerComponent],
template: `
<ngxsmk-datepicker
class="w-full border:none"
[value]="dateInQuestion()"
(valueChange)="onDateChange($any($event))"
mode="single"
placeholder="Select a date">
</ngxsmk-datepicker>
`
})
export class ServerFormManualComponent {
localObject = signal<{ dateInQuestion: Date | null }>({
dateInQuestion: null
});
myForm = form(this.localObject, objectSchema({
dateInQuestion: objectSchema<Date | null>()
}));
dateInQuestion = computed(() => {
const value = this.myForm.value().dateInQuestion;
if (value && typeof value === 'string') {
return new Date(value);
}
return value;
});
onDateChange(newDate: DatepickerValue | null): void {
if (newDate) {
const dateValue = newDate instanceof Date
? newDate
: new Date(newDate.toLocaleString());
this.localObject.update(obj => ({
...obj,
dateInQuestion: dateValue
}));
} else {
this.localObject.update(obj => ({
...obj,
dateInQuestion: null
}));
}
}
updateFormFromServer(serverDate: Date | string): void {
const dateValue = serverDate instanceof Date
? serverDate
: new Date(serverDate);
this.localObject.update(obj => ({
...obj,
dateInQuestion: dateValue
}));
}
resetForm(): void {
this.localObject.set({
dateInQuestion: null
});
}
}
```
**Key points of this pattern:**
- Uses `computed()` to create a reactive signal that reads from the form value
- Updates the underlying `localObject` signal when dates change, which automatically updates the form
- Ensures initial server values populate correctly by updating `localObject` when data arrives
- Works around readonly form limitations by not directly binding to the form field
- Handles both initial population and subsequent updates
**When to use this pattern:**
- When `[field]` binding doesn't populate initial server values
- When working with readonly form signals
- When you need more control over when form updates occur
- When you need to handle date format conversions (string to Date)
## Troubleshooting
### Field not updating
If the field value isn't updating, ensure:
1. The field is properly initialized: `localObject = signal({ date: ... })`
2. The form is created correctly: `form(this.localObject, objectSchema({ ... }))`
3. The field reference is correct: `[field]="myForm.date"` (not `myForm().date`)
### Initial value not showing
If the initial value from the server isn't showing:
**With `[field]` binding:**
1. Ensure `localObject` is initialized with the server data
2. Use `linkedSignal` for reactive server data
3. Check that the date value is a valid Date object
4. If using readonly form, consider the manual binding pattern above
**With manual `[value]` binding:**
1. Use a `computed()` signal that reads from `myForm.value().fieldName`
2. Update the underlying `localObject` signal when server data arrives
3. Ensure the computed signal is properly reactive to form changes
4. Convert string dates to Date objects if your server returns strings
### Readonly form signal issues
If you're using `protected readonly form = form(...)` and controls aren't updating:
1. **Option 1**: Remove `readonly` if possible
2. **Option 2**: Use the manual binding pattern with `[value]` and `(valueChange)` shown above
3. **Option 3**: Update the underlying `localObject` signal instead of the form directly
### Disabled state not working
The datepicker automatically reads `field.disabled()`. If it's not working:
1. Ensure the field has a `disabled` property or function
2. Check that the form validation is set up correctly
3. When using manual binding, you may need to manually bind `[disabledState]`
### Form not marking as dirty
If `form().dirty()` returns `false` after selecting a date:
1. **Ensure you're using `[field]` binding**: The `[field]` input automatically uses the form's API methods to track dirty state.
```html
<!-- ✅ Correct - uses [field] binding -->
<ngxsmk-datepicker [field]="myForm.dateField" mode="single"></ngxsmk-datepicker>
```
2. **Avoid mixing `[field]` with manual `(valueChange)`**: Don't use both together, as the manual handler may bypass form tracking:
```html
<!-- ❌ Incorrect - manual handler bypasses form API -->
<ngxsmk-datepicker
[field]="myForm.dateField"
(valueChange)="dateField.set($event)"
mode="single">
</ngxsmk-datepicker>
```
3. **Use form API methods in manual handlers**: If you must use manual binding, use `setValue()` or `updateValue()`:
```typescript
onDateChange(newDate: Date): void {
// ✅ Correct - uses form API
this.myForm.dateField.setValue(newDate);
// ❌ Incorrect - bypasses dirty tracking
// this.dateField.set(newDate);
}
```
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.