@ng-vui/date-picker
v1.0.0
Published
Angular Date Picker Component for UI Library
Maintainers
Readme
@ng-vui/date-picker
A fully-featured Angular date picker component with calendar view, date ranges, localization, and accessibility support.
📚 Quick Navigation
| Section | Description | Perfect For | |---------|-------------|------------| | 🚀 Features | Complete feature overview | Understanding capabilities | | 📦 Installation | Setup and installation | Getting started | | ⚡ Quick Start | Configuration examples | Initial setup | | 🎯 Basic Usage | Simple implementation | First-time setup | | 🔧 Configuration | Settings and options | Customization | | ✨ Advanced Features | Complex use cases | Advanced implementation | | 🎨 Styling & Theming | Visual customization | UI/UX design | | 🌍 Internationalization | Multi-language support | Global applications | | 🧪 Testing | Unit testing examples | Quality assurance | | 🚀 Performance | Optimization strategies | Large applications |
🎯 Use Cases & Examples
Single Date Selection
Perfect for birthdays, deadlines, and appointment scheduling.
<my-date-picker [(ngModel)]="birthday" [options]="{dateFormat: 'dd/mm/yyyy'}"></my-date-picker>Date Range Selection
Ideal for booking systems, reporting periods, and date filters.
<my-date-picker [(ngModel)]="dateRange" [options]="{dateRange: true, rangeSeparator: ' - '}"></my-date-picker>Form Integration
Seamless integration with Angular reactive forms and validation.
<my-date-picker [formControl]="dateControl" [options]="formDateOptions"></my-date-picker>Localization
Support for multiple languages and regional date formats.
<my-date-picker [options]="{dayLabels: germanDayLabels, monthLabels: germanMonthLabels}"></my-date-picker>Validation & Restrictions
Min/max dates, disabled dates, and custom validation rules.
<my-date-picker [options]="{disableDates: weekends, minYear: 2020, maxYear: 2025}"></my-date-picker>🚀 Features
- ✅ Date Selection - Single date, date range, and multiple dates
- ✅ Calendar Views - Month, year, and decade navigation
- ✅ Internationalization - Multiple locales and date formats
- ✅ Accessibility - Full ARIA support and keyboard navigation
- ✅ Validation - Min/max dates, disabled dates, and custom validators
- ✅ Reactive Forms - Angular Forms integration
- ✅ Customizable - Custom templates and styling
- ✅ Mobile Friendly - Touch-optimized interface
- ✅ TypeScript - Full type safety
📦 Installation
npm install @ng-vui/date-picker⚡ Quick Start
1. Basic Date Picker
const basicOptions: IMyDpOptions = {
dateFormat: 'dd/mm/yyyy',
markCurrentDay: true,
todayBtnTxt: 'Today',
clearBtnTxt: 'Clear'
};2. Date Range Picker
const rangeOptions: IMyDpOptions = {
dateRange: true,
rangeSeparator: ' - ',
dateFormat: 'dd/mm/yyyy',
markCurrentDay: true,
showWeekNumbers: true
};3. Localized Date Picker
const localizedOptions: IMyDpOptions = {
dateFormat: 'dd.mm.yyyy',
firstDayOfWeek: 'mo',
monthLabels: {
1: 'Januar', 2: 'Februar', 3: 'März', 4: 'April',
5: 'Mai', 6: 'Juni', 7: 'Juli', 8: 'August',
9: 'September', 10: 'Oktober', 11: 'November', 12: 'Dezember'
},
dayLabels: { su: 'So', mo: 'Mo', tu: 'Di', we: 'Mi', th: 'Do', fr: 'Fr', sa: 'Sa' }
};4. Validated Date Picker
const validatedOptions: IMyDpOptions = {
dateFormat: 'yyyy-mm-dd',
minYear: 1900,
maxYear: 2030,
disableUntil: { year: 2020, month: 1, day: 1 },
disableSince: { year: 2025, month: 12, day: 31 },
disableWeekends: false
};🎯 Basic Usage
Import
import { MyDatePickerModule } from '@ng-vui/date-picker';
// For modules
@NgModule({
imports: [MyDatePickerModule],
// ... module config
})
// For standalone components
import { MyDatePickerComponent } from '@ng-vui/date-picker';
@Component({
standalone: true,
imports: [MyDatePickerComponent],
// ... component config
})Simple Date Picker
@Component({
selector: 'app-basic-example',
template: `
<my-date-picker
[options]="myDatePickerOptions"
[(ngModel)]="selectedDate"
placeholder="Select date">
</my-date-picker>
<div class="mt-3" *ngIf="selectedDate">
Selected: {{ selectedDate | json }}
</div>
`
})
export class BasicExampleComponent {
selectedDate: any = null;
myDatePickerOptions: IMyDpOptions = {
dateFormat: 'dd.mm.yyyy',
firstDayOfWeek: 'mo',
sunHighlight: true,
markCurrentDay: true,
markCurrentMonth: true,
markCurrentYear: true,
monthLabels: {
1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr', 5: 'May', 6: 'Jun',
7: 'Jul', 8: 'Aug', 9: 'Sep', 10: 'Oct', 11: 'Nov', 12: 'Dec'
},
dayLabels: { su: 'Sun', mo: 'Mon', tu: 'Tue', we: 'Wed', th: 'Thu', fr: 'Fri', sa: 'Sat' },
todayBtnTxt: 'Today',
clearBtnTxt: 'Clear',
closeBtnTxt: 'Close'
};
}With Reactive Forms and Validation
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators, AbstractControl, ValidationErrors, ReactiveFormsModule } from '@angular/forms';
import { MyDatePickerComponent } from '@ng-vui/date-picker';
@Component({
standalone: true,
imports: [ReactiveFormsModule, MyDatePickerComponent],
template: `
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<div class="space-y-4">
<div class="form-group">
<label class="block text-sm font-medium mb-1">Birth Date *</label>
<my-date-picker
formControlName="birthDate"
[options]="birthDateOptions"
placeholder="Select your birth date"
class="w-full">
</my-date-picker>
<div *ngIf="getBirthDateError()" class="mt-1 text-sm text-red-600">
{{ getBirthDateError() }}
</div>
</div>
<div class="form-group">
<label class="block text-sm font-medium mb-1">Appointment Date *</label>
<my-date-picker
formControlName="appointmentDate"
[options]="appointmentOptions"
placeholder="Select appointment date"
class="w-full">
</my-date-picker>
<div *ngIf="getAppointmentError()" class="mt-1 text-sm text-red-600">
{{ getAppointmentError() }}
</div>
</div>
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
Schedule Appointment
</button>
</div>
</form>
`
})
export class ReactiveFormComponent implements OnInit {
userForm!: FormGroup;
isSubmitted = false;
birthDateOptions: IMyDpOptions = {
dateFormat: 'dd/mm/yyyy',
disableFuture: true, // Disable future dates for birth date
minYear: 1900,
maxYear: new Date().getFullYear(),
showTodayBtn: false,
markCurrentDay: true
};
appointmentOptions: IMyDpOptions = {
dateFormat: 'dd/mm/yyyy',
disablePast: true, // Disable past dates for appointments
disableWeekends: true, // No weekend appointments
showTodayBtn: true,
markCurrentDay: true,
todayBtnTxt: 'Today',
clearBtnTxt: 'Clear'
};
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.userForm = this.fb.group({
birthDate: ['', [
Validators.required,
this.birthDateValidator.bind(this)
]],
appointmentDate: ['', [
Validators.required,
this.appointmentDateValidator.bind(this)
]]
});
}
// Birth date validation
birthDateValidator(control: AbstractControl): ValidationErrors | null {
if (!control.value) return null;
const selectedDate = new Date(control.value.jsdate);
const today = new Date();
const minDate = new Date('1900-01-01');
if (selectedDate > today) {
return { futureDate: true };
}
if (selectedDate < minDate) {
return { tooOld: true };
}
// Check if person is under 18
const age = today.getFullYear() - selectedDate.getFullYear();
if (age < 18) {
return { underAge: true };
}
return null;
}
// Appointment date validation
appointmentDateValidator(control: AbstractControl): ValidationErrors | null {
if (!control.value) return null;
const selectedDate = new Date(control.value.jsdate);
const today = new Date();
const dayOfWeek = selectedDate.getDay();
// Check if it's in the past
if (selectedDate < today) {
return { pastDate: true };
}
// Check if it's a weekend
if (dayOfWeek === 0 || dayOfWeek === 6) {
return { weekendNotAllowed: true };
}
// Check if it's too far in the future (max 6 months)
const maxDate = new Date(today);
maxDate.setMonth(today.getMonth() + 6);
if (selectedDate > maxDate) {
return { tooFarInFuture: true };
}
return null;
}
getBirthDateError(): string | null {
const control = this.userForm.get('birthDate');
if (!control || (!control.touched && !this.isSubmitted)) return null;
if (control.errors?.['required']) {
return 'Birth date is required';
}
if (control.errors?.['futureDate']) {
return 'Birth date cannot be in the future';
}
if (control.errors?.['tooOld']) {
return 'Please enter a valid birth date';
}
if (control.errors?.['underAge']) {
return 'You must be at least 18 years old';
}
return null;
}
getAppointmentError(): string | null {
const control = this.userForm.get('appointmentDate');
if (!control || (!control.touched && !this.isSubmitted)) return null;
if (control.errors?.['required']) {
return 'Appointment date is required';
}
if (control.errors?.['pastDate']) {
return 'Appointment date cannot be in the past';
}
if (control.errors?.['weekendNotAllowed']) {
return 'Weekend appointments are not available';
}
if (control.errors?.['tooFarInFuture']) {
return 'Appointments can only be scheduled up to 6 months in advance';
}
return null;
}
onSubmit() {
this.isSubmitted = true;
if (this.userForm.valid) {
console.log('Form submitted:', this.userForm.value);
}
}
}🚨 Error Handling & Validation
Built-in Validation Support
The VUI date picker component integrates seamlessly with Angular's reactive forms and provides comprehensive validation capabilities for date inputs:
Required Validation
@Component({
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div class="form-group">
<label>Event Date *</label>
<my-date-picker
formControlName="eventDate"
[options]="eventOptions"
placeholder="Select event date">
</my-date-picker>
<div *ngIf="getEventDateError()" class="mt-1 text-sm text-red-600">
{{ getEventDateError() }}
</div>
</div>
</form>
`
})
export class RequiredValidationExample {
form = this.fb.group({
eventDate: ['', Validators.required] // Required date selection
});
eventOptions: IMyDpOptions = {
dateFormat: 'dd/mm/yyyy',
markCurrentDay: true
};
submitted = false;
getEventDateError(): string | null {
const control = this.form.get('eventDate');
if (!control || (!control.touched && !this.submitted)) return null;
if (control.errors?.['required']) {
return 'Event date is required for registration';
}
return null;
}
onSubmit() {
this.submitted = true;
if (this.form.valid) {
console.log('Event scheduled:', this.form.value);
}
}
}Date Range Validation
@Component({
template: `
<form [formGroup]="travelForm" (ngSubmit)="onSubmit()">
<div class="grid grid-cols-2 gap-4">
<div>
<label>Departure Date *</label>
<my-date-picker
formControlName="departureDate"
[options]="departureDateOptions"
placeholder="Select departure"
(dateChanged)="onDepartureDateChange($event)">
</my-date-picker>
<div *ngIf="getDepartureDateError()" class="mt-1 text-sm text-red-600">
{{ getDepartureDateError() }}
</div>
</div>
<div>
<label>Return Date *</label>
<my-date-picker
formControlName="returnDate"
[options]="returnDateOptions"
placeholder="Select return">
</my-date-picker>
<div *ngIf="getReturnDateError()" class="mt-1 text-sm text-red-600">
{{ getReturnDateError() }}
</div>
</div>
</div>
</form>
`
})
export class DateRangeValidationExample implements OnInit {
travelForm = this.fb.group({
departureDate: ['', [Validators.required, this.departureDateValidator.bind(this)]],
returnDate: ['', [Validators.required, this.returnDateValidator.bind(this)]]
});
departureDateOptions: IMyDpOptions = {
dateFormat: 'dd/mm/yyyy',
disablePast: true,
markCurrentDay: true
};
returnDateOptions: IMyDpOptions = {
dateFormat: 'dd/mm/yyyy',
disablePast: true,
markCurrentDay: true
};
submitted = false;
ngOnInit() {
// Watch for departure date changes to update return date restrictions
this.travelForm.get('departureDate')?.valueChanges.subscribe(() => {
this.updateReturnDateOptions();
this.travelForm.get('returnDate')?.updateValueAndValidity();
});
}
departureDateValidator(control: AbstractControl): ValidationErrors | null {
if (!control.value) return null;
const selectedDate = new Date(control.value.jsdate);
const today = new Date();
// Must be at least tomorrow
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);
if (selectedDate < tomorrow) {
return { tooSoon: true };
}
// Cannot be more than 1 year in advance
const maxDate = new Date(today);
maxDate.setFullYear(today.getFullYear() + 1);
if (selectedDate > maxDate) {
return { tooFarAhead: true };
}
return null;
}
returnDateValidator(control: AbstractControl): ValidationErrors | null {
if (!control.value) return null;
const returnDate = new Date(control.value.jsdate);
const departureControl = this.travelForm?.get('departureDate');
if (departureControl?.value) {
const departureDate = new Date(departureControl.value.jsdate);
// Return date must be after departure date
if (returnDate <= departureDate) {
return { beforeDeparture: true };
}
// Trip cannot exceed 30 days
const diffTime = returnDate.getTime() - departureDate.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays > 30) {
return { tripTooLong: true };
}
}
return null;
}
onDepartureDateChange(event: any) {
this.updateReturnDateOptions();
}
private updateReturnDateOptions() {
const departureDate = this.travelForm.get('departureDate')?.value;
if (departureDate) {
this.returnDateOptions = {
...this.returnDateOptions,
disableUntil: {
year: departureDate.date.year,
month: departureDate.date.month,
day: departureDate.date.day
}
};
}
}
getDepartureDateError(): string | null {
const control = this.travelForm.get('departureDate');
if (!control || (!control.touched && !this.submitted)) return null;
if (control.errors?.['required']) {
return 'Departure date is required';
}
if (control.errors?.['tooSoon']) {
return 'Departure must be at least 1 day in advance';
}
if (control.errors?.['tooFarAhead']) {
return 'Cannot book more than 1 year in advance';
}
return null;
}
getReturnDateError(): string | null {
const control = this.travelForm.get('returnDate');
if (!control || (!control.touched && !this.submitted)) return null;
if (control.errors?.['required']) {
return 'Return date is required';
}
if (control.errors?.['beforeDeparture']) {
return 'Return date must be after departure date';
}
if (control.errors?.['tripTooLong']) {
return 'Trip duration cannot exceed 30 days';
}
return null;
}
onSubmit() {
this.submitted = true;
if (this.travelForm.valid) {
console.log('Travel booked:', this.travelForm.value);
}
}
}Business Rules Validation
@Component({
template: `
<form [formGroup]="businessForm">
<div class="space-y-4">
<div>
<label>Meeting Date *</label>
<my-date-picker
formControlName="meetingDate"
[options]="businessDateOptions"
placeholder="Select meeting date">
</my-date-picker>
<div *ngIf="getMeetingDateError()" class="mt-1 text-sm text-red-600">
{{ getMeetingDateError() }}
</div>
</div>
<div>
<label>Project Deadline *</label>
<my-date-picker
formControlName="deadline"
[options]="deadlineOptions"
placeholder="Select deadline">
</my-date-picker>
<div *ngIf="getDeadlineError()" class="mt-1 text-sm text-red-600">
{{ getDeadlineError() }}
</div>
</div>
</div>
</form>
`
})
export class BusinessRulesValidationExample {
businessForm = this.fb.group({
meetingDate: ['', [Validators.required, this.businessDateValidator.bind(this)]],
deadline: ['', [Validators.required, this.deadlineValidator.bind(this)]]
});
// Company holidays
private holidays: IMyDate[] = [
{ year: 2024, month: 1, day: 1 }, // New Year
{ year: 2024, month: 7, day: 4 }, // Independence Day
{ year: 2024, month: 11, day: 28 }, // Thanksgiving
{ year: 2024, month: 12, day: 25 } // Christmas
];
businessDateOptions: IMyDpOptions = {
dateFormat: 'dd/mm/yyyy',
disablePast: true,
disableWeekends: true,
disableDates: this.holidays,
markCurrentDay: true
};
deadlineOptions: IMyDpOptions = {
dateFormat: 'dd/mm/yyyy',
disablePast: true,
markCurrentDay: true,
showTodayBtn: false
};
submitted = false;
businessDateValidator(control: AbstractControl): ValidationErrors | null {
if (!control.value) return null;
const selectedDate = new Date(control.value.jsdate);
const dayOfWeek = selectedDate.getDay();
// Check for weekends (already disabled in UI, but validate)
if (dayOfWeek === 0 || dayOfWeek === 6) {
return { weekendNotAllowed: true };
}
// Check for holidays
const isHoliday = this.holidays.some(holiday =>
holiday.year === control.value.date.year &&
holiday.month === control.value.date.month &&
holiday.day === control.value.date.day
);
if (isHoliday) {
return { holidayNotAllowed: true };
}
// Must be at least 2 business days in advance
const today = new Date();
const businessDaysAhead = this.calculateBusinessDays(today, selectedDate);
if (businessDaysAhead < 2) {
return { insufficientAdvanceNotice: true };
}
return null;
}
deadlineValidator(control: AbstractControl): ValidationErrors | null {
if (!control.value) return null;
const deadline = new Date(control.value.jsdate);
const today = new Date();
// Minimum 1 week for project completion
const minDeadline = new Date(today);
minDeadline.setDate(today.getDate() + 7);
if (deadline < minDeadline) {
return { deadlineTooSoon: true };
}
// Maximum 1 year for project planning
const maxDeadline = new Date(today);
maxDeadline.setFullYear(today.getFullYear() + 1);
if (deadline > maxDeadline) {
return { deadlineTooFar: true };
}
return null;
}
private calculateBusinessDays(startDate: Date, endDate: Date): number {
let count = 0;
const current = new Date(startDate);
while (current < endDate) {
const dayOfWeek = current.getDay();
if (dayOfWeek !== 0 && dayOfWeek !== 6) { // Not weekend
// Check if it's not a holiday
const isHoliday = this.holidays.some(holiday =>
holiday.year === current.getFullYear() &&
holiday.month === current.getMonth() + 1 &&
holiday.day === current.getDate()
);
if (!isHoliday) {
count++;
}
}
current.setDate(current.getDate() + 1);
}
return count;
}
getMeetingDateError(): string | null {
const control = this.businessForm.get('meetingDate');
if (!control || (!control.touched && !this.submitted)) return null;
if (control.errors?.['required']) {
return 'Meeting date is required';
}
if (control.errors?.['weekendNotAllowed']) {
return 'Meetings cannot be scheduled on weekends';
}
if (control.errors?.['holidayNotAllowed']) {
return 'Meetings cannot be scheduled on company holidays';
}
if (control.errors?.['insufficientAdvanceNotice']) {
return 'Meetings must be scheduled at least 2 business days in advance';
}
return null;
}
getDeadlineError(): string | null {
const control = this.businessForm.get('deadline');
if (!control || (!control.touched && !this.submitted)) return null;
if (control.errors?.['required']) {
return 'Project deadline is required';
}
if (control.errors?.['deadlineTooSoon']) {
return 'Project deadline must be at least 1 week from now';
}
if (control.errors?.['deadlineTooFar']) {
return 'Project deadline cannot be more than 1 year away';
}
return null;
}
}Age and Legal Date Validation
@Component({
template: `
<form [formGroup]="registrationForm">
<div class="space-y-4">
<div>
<label>Birth Date *</label>
<my-date-picker
formControlName="birthDate"
[options]="birthDateOptions"
placeholder="Enter your birth date">
</my-date-picker>
<div *ngIf="getBirthDateError()" class="mt-1 text-sm text-red-600">
{{ getBirthDateError() }}
</div>
<div *ngIf="getAge() > 0" class="mt-1 text-sm text-gray-600">
Age: {{ getAge() }} years old
</div>
</div>
<div>
<label>License Expiry Date *</label>
<my-date-picker
formControlName="licenseExpiry"
[options]="licenseOptions"
placeholder="License expiry date">
</my-date-picker>
<div *ngIf="getLicenseError()" class="mt-1 text-sm text-red-600">
{{ getLicenseError() }}
</div>
</div>
</div>
</form>
`
})
export class AgeLegalValidationExample {
registrationForm = this.fb.group({
birthDate: ['', [Validators.required, this.birthDateValidator.bind(this)]],
licenseExpiry: ['', [Validators.required, this.licenseExpiryValidator.bind(this)]]
});
birthDateOptions: IMyDpOptions = {
dateFormat: 'dd/mm/yyyy',
disableFuture: true,
maxYear: new Date().getFullYear(),
minYear: 1920,
markCurrentDay: true
};
licenseOptions: IMyDpOptions = {
dateFormat: 'dd/mm/yyyy',
disablePast: true,
markCurrentDay: true
};
submitted = false;
birthDateValidator(control: AbstractControl): ValidationErrors | null {
if (!control.value) return null;
const birthDate = new Date(control.value.jsdate);
const today = new Date();
// Future date check
if (birthDate > today) {
return { futureDate: true };
}
// Age calculation
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
// Minimum age validation (13 for social media, 18 for adult services, etc.)
if (age < 13) {
return { tooYoung: true };
}
// Maximum reasonable age
if (age > 120) {
return { unrealisticAge: true };
}
// Driving age validation (if this is for a driving-related service)
if (age < 16) {
return { belowDrivingAge: true };
}
return null;
}
licenseExpiryValidator(control: AbstractControl): ValidationErrors | null {
if (!control.value) return null;
const expiryDate = new Date(control.value.jsdate);
const today = new Date();
// Must be valid (not expired)
if (expiryDate <= today) {
return { licenseExpired: true };
}
// Should have at least 30 days remaining
const daysUntilExpiry = Math.ceil((expiryDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
if (daysUntilExpiry < 30) {
return { expiringSoon: true };
}
// Reasonable maximum (licenses don't last more than 10 years typically)
const maxDate = new Date(today);
maxDate.setFullYear(today.getFullYear() + 10);
if (expiryDate > maxDate) {
return { unrealisticExpiry: true };
}
return null;
}
getAge(): number {
const birthDateControl = this.registrationForm.get('birthDate');
if (!birthDateControl?.value) return 0;
const birthDate = new Date(birthDateControl.value.jsdate);
const today = new Date();
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
return age;
}
getBirthDateError(): string | null {
const control = this.registrationForm.get('birthDate');
if (!control || (!control.touched && !this.submitted)) return null;
if (control.errors?.['required']) {
return 'Birth date is required';
}
if (control.errors?.['futureDate']) {
return 'Birth date cannot be in the future';
}
if (control.errors?.['tooYoung']) {
return 'You must be at least 13 years old to register';
}
if (control.errors?.['belowDrivingAge']) {
return 'You must be at least 16 years old for this service';
}
if (control.errors?.['unrealisticAge']) {
return 'Please enter a valid birth date';
}
return null;
}
getLicenseError(): string | null {
const control = this.registrationForm.get('licenseExpiry');
if (!control || (!control.touched && !this.submitted)) return null;
if (control.errors?.['required']) {
return 'License expiry date is required';
}
if (control.errors?.['licenseExpired']) {
return 'License has already expired';
}
if (control.errors?.['expiringSoon']) {
return 'License expires too soon (minimum 30 days required)';
}
if (control.errors?.['unrealisticExpiry']) {
return 'Please enter a valid license expiry date';
}
return null;
}
}Common Validation Patterns
Cross-Field Date Validation
// Validator that compares two date fields
export function dateRangeValidator(startFieldName: string, endFieldName: string) {
return (formGroup: AbstractControl): ValidationErrors | null => {
const startControl = formGroup.get(startFieldName);
const endControl = formGroup.get(endFieldName);
if (!startControl?.value || !endControl?.value) {
return null;
}
const startDate = new Date(startControl.value.jsdate);
const endDate = new Date(endControl.value.jsdate);
if (startDate >= endDate) {
return { dateRangeInvalid: { startField: startFieldName, endField: endFieldName } };
}
return null;
};
}
// Usage in form group
this.form = this.fb.group({
startDate: ['', Validators.required],
endDate: ['', Validators.required]
}, {
validators: [dateRangeValidator('startDate', 'endDate')]
});Dynamic Date Restrictions
// Method to dynamically update date picker options
updateDateRestrictions(baseDate: Date, component: 'start' | 'end') {
if (component === 'end') {
this.endDateOptions = {
...this.endDateOptions,
disableUntil: {
year: baseDate.getFullYear(),
month: baseDate.getMonth() + 1,
day: baseDate.getDate() - 1
}
};
}
}🔧 API Reference
MyDatePickerComponent
Inputs
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| options | IMyDpOptions | {} | Configuration options |
| locale | string | 'en' | Locale for date formatting |
| defaultMonth | string | - | Default month to show (yyyy-mm) |
| placeholder | string | - | Input placeholder text |
| selector | string | - | CSS selector for input styling |
Outputs
| Event | Type | Description |
|-------|------|-------------|
| dateChanged | EventEmitter<IMyDateModel> | Date selection changed |
| inputFieldChanged | EventEmitter<IMyInputFieldChanged> | Input field value changed |
| calendarViewChanged | EventEmitter<IMyCalendarViewChanged> | Calendar view changed |
| calendarToggle | EventEmitter<number> | Calendar opened/closed |
| inputFocusBlur | EventEmitter<IMyInputFocusBlur> | Input focus/blur events |
IMyDpOptions Interface
interface IMyDpOptions {
// Date format and display
dateFormat?: string; // Date format (e.g., 'dd.mm.yyyy', 'yyyy-mm-dd')
monthLabels?: IMyMonthLabels; // Month labels
dayLabels?: IMyDayLabels; // Day labels
dayLabelsFull?: IMyDayLabels; // Full day labels
// Calendar behavior
firstDayOfWeek?: string; // First day of week ('su', 'mo', etc.)
satHighlight?: boolean; // Highlight Saturdays
sunHighlight?: boolean; // Highlight Sundays
highlightDates?: IMyDate[]; // Dates to highlight
markCurrentDay?: boolean; // Mark current day
markCurrentMonth?: boolean; // Mark current month
markCurrentYear?: boolean; // Mark current year
// Date restrictions
disableUntil?: IMyDate; // Disable dates until
disableSince?: IMyDate; // Disable dates since
disableDates?: IMyDate[]; // Specific dates to disable
disableDateRanges?: IMyDateRange[]; // Date ranges to disable
disableWeekends?: boolean; // Disable weekends
disableFuture?: boolean; // Disable future dates
disablePast?: boolean; // Disable past dates
// Year and month limits
minYear?: number; // Minimum selectable year
maxYear?: number; // Maximum selectable year
// UI elements
showTodayBtn?: boolean; // Show today button
showClearBtn?: boolean; // Show clear button
showCloseBtn?: boolean; // Show close button
todayBtnTxt?: string; // Today button text
clearBtnTxt?: string; // Clear button text
closeBtnTxt?: string; // Close button text
// Calendar size and position
width?: string; // Calendar width
height?: string; // Calendar height
selectorHeight?: string; // Input height
selectorWidth?: string; // Input width
// Animation and timing
openSelectorTopOfInput?: boolean; // Open calendar above input
showSelectorArrow?: boolean; // Show calendar arrow
alignSelectorRight?: boolean; // Align calendar to right
// Inline mode
inline?: boolean; // Display as inline calendar
showDateFormatPlaceholder?: boolean; // Show format placeholder
// Validation
allowDeselectDate?: boolean; // Allow deselecting current date
// Styling
stylesData?: IMyStyles; // Custom styles
divHostElement?: IMyDivHostElement; // Host element styling
}Date Model Interface
interface IMyDateModel {
date: IMyDate; // Selected date object
jsdate?: Date; // JavaScript Date object
formatted: string; // Formatted date string
epoc: number; // Unix timestamp
}
interface IMyDate {
year: number; // Year (e.g., 2024)
month: number; // Month (1-12)
day: number; // Day (1-31)
}🎯 Advanced Examples
Date Range Selection
@Component({
template: `
<div class="form-row">
<div class="col-md-6">
<label>Start Date</label>
<my-date-picker
[(ngModel)]="startDate"
[options]="startDateOptions"
placeholder="Select start date">
</my-date-picker>
</div>
<div class="col-md-6">
<label>End Date</label>
<my-date-picker
[(ngModel)]="endDate"
[options]="endDateOptions"
placeholder="Select end date">
</my-date-picker>
</div>
</div>
<div class="mt-3" *ngIf="startDate && endDate">
Duration: {{ calculateDuration() }} days
</div>
`
})
export class DateRangeComponent {
startDate: any = null;
endDate: any = null;
startDateOptions: IMyDpOptions = {
dateFormat: 'yyyy-mm-dd',
disablePast: true,
showTodayBtn: true
};
endDateOptions: IMyDpOptions = {
dateFormat: 'yyyy-mm-dd',
disablePast: true,
showTodayBtn: true
};
ngOnInit() {
// Update end date options when start date changes
this.updateEndDateRestrictions();
}
onStartDateChanged() {
this.updateEndDateRestrictions();
// Clear end date if it's before start date
if (this.endDate && this.startDate &&
new Date(this.endDate.jsdate) < new Date(this.startDate.jsdate)) {
this.endDate = null;
}
}
private updateEndDateRestrictions() {
if (this.startDate) {
this.endDateOptions = {
...this.endDateOptions,
disableUntil: {
year: this.startDate.date.year,
month: this.startDate.date.month,
day: this.startDate.date.day - 1
}
};
}
}
calculateDuration(): number {
if (!this.startDate || !this.endDate) return 0;
const start = new Date(this.startDate.jsdate);
const end = new Date(this.endDate.jsdate);
const diffTime = Math.abs(end.getTime() - start.getTime());
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
}
}Custom Styling and Themes
@Component({
template: `
<my-date-picker
[options]="customOptions"
[(ngModel)]="selectedDate"
placeholder="Select date">
</my-date-picker>
`
})
export class CustomStyledComponent {
selectedDate: any = null;
customOptions: IMyDpOptions = {
dateFormat: 'dd/mm/yyyy',
firstDayOfWeek: 'mo',
sunHighlight: true,
markCurrentDay: true,
stylesData: {
selector: 'dp1',
styles: `
.dp1 .myDpSelector {
border: 2px solid #007bff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.dp1 .myDpSelectorArrow:after {
border-bottom-color: #007bff;
}
.dp1 .myDpDayCurrMonth:hover,
.dp1 .myDpMonthLabel:hover,
.dp1 .myDpYearLabel:hover {
background-color: #e3f2fd;
color: #1976d2;
}
.dp1 .myDpSelectedDay {
background-color: #007bff;
color: white;
border-radius: 50%;
}
.dp1 .myDpHeaderBtn {
background-color: #007bff;
color: white;
border-radius: 4px;
}
.dp1 .myDpFooterBtn {
background-color: #6c757d;
color: white;
border-radius: 4px;
margin: 2px;
}
.dp1 .myDpFooterBtn:hover {
background-color: #495057;
}
`
}
};
}Localization Example
@Component({
template: `
<div class="form-group">
<label>Language</label>
<select [(ngModel)]="currentLocale" (change)="changeLocale()" class="form-control">
<option value="en">English</option>
<option value="es">Español</option>
<option value="fr">Français</option>
<option value="de">Deutsch</option>
</select>
</div>
<div class="form-group">
<label>{{ getLabel('selectDate') }}</label>
<my-date-picker
[options]="localizedOptions"
[(ngModel)]="selectedDate"
[placeholder]="getLabel('placeholder')">
</my-date-picker>
</div>
`
})
export class LocalizationComponent {
selectedDate: any = null;
currentLocale = 'en';
localizedOptions: IMyDpOptions = {};
private locales = {
en: {
monthLabels: { 1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr', 5: 'May', 6: 'Jun',
7: 'Jul', 8: 'Aug', 9: 'Sep', 10: 'Oct', 11: 'Nov', 12: 'Dec' },
dayLabels: { su: 'Sun', mo: 'Mon', tu: 'Tue', we: 'Wed', th: 'Thu', fr: 'Fri', sa: 'Sat' },
todayBtnTxt: 'Today',
clearBtnTxt: 'Clear',
closeBtnTxt: 'Close',
selectDate: 'Select Date',
placeholder: 'Click to select date'
},
es: {
monthLabels: { 1: 'Ene', 2: 'Feb', 3: 'Mar', 4: 'Abr', 5: 'May', 6: 'Jun',
7: 'Jul', 8: 'Ago', 9: 'Sep', 10: 'Oct', 11: 'Nov', 12: 'Dic' },
dayLabels: { su: 'Dom', mo: 'Lun', tu: 'Mar', we: 'Mié', th: 'Jue', fr: 'Vie', sa: 'Sáb' },
todayBtnTxt: 'Hoy',
clearBtnTxt: 'Limpiar',
closeBtnTxt: 'Cerrar',
selectDate: 'Seleccionar Fecha',
placeholder: 'Haz clic para seleccionar fecha'
},
// Add more locales...
};
ngOnInit() {
this.changeLocale();
}
changeLocale() {
const locale = this.locales[this.currentLocale as keyof typeof this.locales];
this.localizedOptions = {
dateFormat: 'dd/mm/yyyy',
firstDayOfWeek: 'mo',
markCurrentDay: true,
monthLabels: locale.monthLabels,
dayLabels: locale.dayLabels,
todayBtnTxt: locale.todayBtnTxt,
clearBtnTxt: locale.clearBtnTxt,
closeBtnTxt: locale.closeBtnTxt
};
}
getLabel(key: string): string {
const locale = this.locales[this.currentLocale as keyof typeof this.locales];
return locale[key as keyof typeof locale];
}
}Disabled Dates and Validation
@Component({
template: `
<form [formGroup]="form">
<div class="form-group">
<label>Appointment Date</label>
<my-date-picker
formControlName="appointmentDate"
[options]="appointmentOptions"
placeholder="Select appointment date">
</my-date-picker>
<div *ngIf="form.get('appointmentDate')?.invalid && form.get('appointmentDate')?.touched"
class="text-danger">
<div *ngIf="form.get('appointmentDate')?.hasError('required')">
Appointment date is required
</div>
<div *ngIf="form.get('appointmentDate')?.hasError('weekendNotAllowed')">
Weekend appointments are not available
</div>
<div *ngIf="form.get('appointmentDate')?.hasError('holidayNotAllowed')">
Holiday appointments are not available
</div>
</div>
</div>
<small class="form-text text-muted">
* Appointments are available Monday-Friday, excluding holidays
</small>
</form>
`
})
export class DisabledDatesComponent {
form = this.fb.group({
appointmentDate: ['', [Validators.required, this.appointmentDateValidator.bind(this)]]
});
// Define holidays
private holidays: IMyDate[] = [
{ year: 2024, month: 1, day: 1 }, // New Year
{ year: 2024, month: 7, day: 4 }, // Independence Day
{ year: 2024, month: 12, day: 25 } // Christmas
];
appointmentOptions: IMyDpOptions = {
dateFormat: 'dd/mm/yyyy',
firstDayOfWeek: 'mo',
disablePast: true,
disableWeekends: true,
disableDates: this.holidays,
markCurrentDay: true,
showTodayBtn: false,
sunHighlight: false,
satHighlight: false
};
constructor(private fb: FormBuilder) {}
appointmentDateValidator(control: AbstractControl): ValidationErrors | null {
if (!control.value) return null;
const selectedDate = control.value;
const jsDate = new Date(selectedDate.jsdate);
// Check if it's a weekend
if (jsDate.getDay() === 0 || jsDate.getDay() === 6) {
return { weekendNotAllowed: true };
}
// Check if it's a holiday
const isHoliday = this.holidays.some(holiday =>
holiday.year === selectedDate.date.year &&
holiday.month === selectedDate.date.month &&
holiday.day === selectedDate.date.day
);
if (isHoliday) {
return { holidayNotAllowed: true };
}
return null;
}
}Inline Calendar
@Component({
template: `
<div class="row">
<div class="col-md-6">
<h5>Inline Calendar</h5>
<my-date-picker
[options]="inlineOptions"
[(ngModel)]="selectedDate">
</my-date-picker>
</div>
<div class="col-md-6">
<h5>Selected Date Information</h5>
<div *ngIf="selectedDate" class="card">
<div class="card-body">
<p><strong>Date:</strong> {{ selectedDate.formatted }}</p>
<p><strong>Day of Week:</strong> {{ getDayOfWeek() }}</p>
<p><strong>Week Number:</strong> {{ getWeekNumber() }}</p>
<p><strong>Days from Today:</strong> {{ getDaysFromToday() }}</p>
</div>
</div>
<div *ngIf="!selectedDate" class="alert alert-info">
Please select a date from the calendar
</div>
</div>
</div>
`
})
export class InlineCalendarComponent {
selectedDate: any = null;
inlineOptions: IMyDpOptions = {
dateFormat: 'dd.mm.yyyy',
firstDayOfWeek: 'mo',
inline: true,
showTodayBtn: true,
showClearBtn: true,
markCurrentDay: true,
sunHighlight: true,
width: '100%'
};
getDayOfWeek(): string {
if (!this.selectedDate) return '';
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
return days[new Date(this.selectedDate.jsdate).getDay()];
}
getWeekNumber(): number {
if (!this.selectedDate) return 0;
const date = new Date(this.selectedDate.jsdate);
const firstDay = new Date(date.getFullYear(), 0, 1);
return Math.ceil(((date.getTime() - firstDay.getTime()) / 86400000 + firstDay.getDay() + 1) / 7);
}
getDaysFromToday(): number {
if (!this.selectedDate) return 0;
const today = new Date();
const selected = new Date(this.selectedDate.jsdate);
const diffTime = selected.getTime() - today.getTime();
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
}
}🎨 Styling & Theming
CSS Classes
// Custom date picker styles
.my-date-picker {
.myDpSelector {
border: 2px solid #007bff;
border-radius: 8px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
&:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
}
}
.myDpCalendar {
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
overflow: hidden;
.myDpHeader {
background: linear-gradient(135deg, #007bff, #0056b3);
color: white;
.myDpHeaderBtn {
color: white;
background: transparent;
&:hover {
background: rgba(255, 255, 255, 0.2);
}
}
}
.myDpWeekDays {
background: #f8f9fa;
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
color: #6c757d;
}
.myDpDays {
.myDpDayCurrMonth {
transition: all 0.2s ease;
&:hover {
background: #e3f2fd;
color: #1976d2;
transform: scale(1.1);
}
}
.myDpSelectedDay {
background: #007bff !important;
color: white !important;
border-radius: 50%;
font-weight: bold;
}
.myDpCurrDay {
background: #ffc107;
color: #212529;
border-radius: 50%;
font-weight: bold;
}
.myDpSunHighlight {
color: #dc3545;
}
.myDpSatHighlight {
color: #fd7e14;
}
}
.myDpFooter {
background: #f8f9fa;
border-top: 1px solid #dee2e6;
.myDpFooterBtn {
background: #6c757d;
color: white;
border-radius: 4px;
transition: background 0.2s ease;
&:hover {
background: #495057;
}
&.today-btn {
background: #28a745;
&:hover {
background: #1e7e34;
}
}
&.clear-btn {
background: #dc3545;
&:hover {
background: #c82333;
}
}
}
}
}
}Dark Theme
.dark-theme .my-date-picker {
.myDpSelector {
background: #343a40;
border-color: #6c757d;
color: #ffffff;
}
.myDpCalendar {
background: #495057;
color: #ffffff;
.myDpHeader {
background: linear-gradient(135deg, #6f42c1, #563d7c);
}
.myDpWeekDays {
background: #6c757d;
color: #adb5bd;
}
.myDpDays {
.myDpDayCurrMonth {
&:hover {
background: #6f42c1;
color: white;
}
}
.myDpSelectedDay {
background: #6f42c1 !important;
}
}
.myDpFooter {
background: #6c757d;
border-top-color: #495057;
}
}
}🧪 Testing
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyDatePickerModule } from '@ng-vui/date-picker';
import { FormsModule } from '@angular/forms';
describe('MyDatePickerComponent', () => {
let component: TestComponent;
let fixture: ComponentFixture<TestComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [TestComponent],
imports: [MyDatePickerModule, FormsModule]
});
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
});
it('should render date picker', () => {
fixture.detectChanges();
const datePicker = fixture.nativeElement.querySelector('my-date-picker');
expect(datePicker).toBeTruthy();
});
it('should open calendar on input click', () => {
fixture.detectChanges();
const input = fixture.nativeElement.querySelector('.myDpSelector');
input.click();
fixture.detectChanges();
const calendar = fixture.nativeElement.querySelector('.myDpCalendar');
expect(calendar).toBeTruthy();
});
it('should select date', () => {
spyOn(component, 'onDateChanged');
fixture.detectChanges();
// Open calendar and click on a date
const input = fixture.nativeElement.querySelector('.myDpSelector');
input.click();
fixture.detectChanges();
const dateCell = fixture.nativeElement.querySelector('.myDpDayCurrMonth');
dateCell.click();
expect(component.onDateChanged).toHaveBeenCalled();
});
});
@Component({
template: `
<my-date-picker
[options]="options"
[(ngModel)]="selectedDate"
(dateChanged)="onDateChanged($event)">
</my-date-picker>
`
})
class TestComponent {
selectedDate: any = null;
options = { dateFormat: 'dd.mm.yyyy' };
onDateChanged(event: any) {
console.log('Date changed:', event);
}
}🚀 Performance Tips
- Use OnPush change detection for better performance:
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})- Limit date ranges for better calendar rendering:
options = {
minYear: 2020,
maxYear: 2030
};- Use trackBy functions in templates with date arrays
🤝 Contributing
- Fork the repository
- Create your feature branch:
git checkout -b feature/my-feature - Commit your changes:
git commit -am 'Add my feature' - Push to the branch:
git push origin feature/my-feature - Submit a pull request
📄 License
MIT © VUI
🔗 Related Packages
ng-vui-select-input- Single select dropdownng-vui-grid- Data grid componentng-vui-multi-select- Multi-select dropdown componentng-vui-text-input- Text input componentng-vui-textarea- Textarea componentng-vui-auto-complete- Auto complete component
