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
930 lines (750 loc) • 25.1 kB
Markdown
# Plugin Architecture
**Last updated:** March 21, 2026 · **Current stable:** v2.2.8
ngxsmk-datepicker features a powerful **plugin architecture** that allows you to extend and customize the component's behavior without modifying its core code. This architecture is built on a **hook-based system** that provides extension points throughout the component's lifecycle.
## Table of Contents
- [Overview](#overview)
- [Architecture Principles](#architecture-principles)
- [Plugin Types](#plugin-types)
- [Creating Plugins](#creating-plugins)
- [Plugin Lifecycle](#plugin-lifecycle)
- [Advanced Patterns](#advanced-patterns)
- [Best Practices](#best-practices)
## Overview
The plugin architecture in ngxsmk-datepicker is designed around the concept of **hooks** - functions that are called at specific points in the component's execution flow. These hooks allow you to:
- **Intercept** component behavior before it executes
- **Modify** data and UI rendering
- **Extend** functionality with custom logic
- **Validate** user input with custom rules
- **React** to component events and state changes
### Key Concepts
```
┌─────────────────────────────────────────────────────────────┐
│ Datepicker Component │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Core Component Logic │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Render │ │ Validate │ │ Events │ │ │
│ │ │ Hooks │ │ Hooks │ │ Hooks │ │ │
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
│ │ │ │ │ │ │
│ │ └─────────────┼─────────────┘ │ │
│ │ │ │ │
│ │ ┌──────▼──────┐ │ │
│ │ │ Plugin │ │ │
│ │ │ Registry │ │ │
│ │ └──────┬──────┘ │ │
│ │ │ │ │
│ └─────────────────────┼───────────────────────────────┘ │
│ │ │
└────────────────────────┼───────────────────────────────────┘
│
│
┌───────────────┼───────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ Plugin │ │ Plugin │ │ Plugin │
│ A │ │ B │ │ C │
└─────────┘ └─────────┘ └─────────┘
```
## Architecture Principles
### 1. **Non-Invasive Extension**
Plugins extend functionality without modifying core code:
```typescript
// ✅ Good: Plugin extends behavior
const myPlugin: DatepickerHooks = {
validateDate: (date) => date.getDay() !== 0 // Disable Sundays
};
// ❌ Bad: Modifying core component (not possible, but conceptually wrong)
// Don't try to override internal methods
```
### 2. **Composable Architecture**
Multiple plugins can work together:
```typescript
// Combine multiple plugins
const weekendPlugin: DatepickerHooks = {
validateDate: (date) => date.getDay() !== 0 && date.getDay() !== 6
};
const businessDaysPlugin: DatepickerHooks = {
getDayCellClasses: (date, isSelected, isDisabled, isToday, isHoliday) => {
const day = date.getDay();
if (day === 0 || day === 6) {
return ['weekend-day'];
}
return [];
}
};
// Use both together
const combinedHooks: DatepickerHooks = {
...weekendPlugin,
...businessDaysPlugin
};
```
### 3. **Type-Safe Extensions**
All plugins are fully typed with TypeScript:
```typescript
import { DatepickerHooks } from 'ngxsmk-datepicker';
// TypeScript ensures type safety
const plugin: DatepickerHooks = {
validateDate: (date: Date, currentValue: DatepickerValue, mode: string) => {
// TypeScript knows the exact types
return true;
}
};
```
### 4. **Optional Execution**
Hooks are optional - the component works without them:
```typescript
// Component works fine without plugins
<ngxsmk-datepicker mode="single"></ngxsmk-datepicker>
// Plugins are optional enhancements
<ngxsmk-datepicker
[hooks]="myPlugin"
mode="single">
</ngxsmk-datepicker>
```
## Plugin Types
The plugin architecture consists of **5 main plugin types**, each serving a specific purpose:
### 1. **Rendering Plugins** (`DayCellRenderHook`)
Control how dates are rendered in the calendar:
```typescript
interface DayCellRenderHook {
getDayCellClasses?(date: Date, isSelected: boolean, isDisabled: boolean, isToday: boolean, isHoliday: boolean): string[];
getDayCellTooltip?(date: Date, holidayLabel: string | null): string | null;
formatDayNumber?(date: Date): string;
}
```
**Use Cases:**
- Custom styling for specific dates
- Conditional CSS classes
- Custom tooltips
- Date number formatting
**Example:**
```typescript
const stylingPlugin: DatepickerHooks = {
getDayCellClasses: (date, isSelected, isDisabled, isToday, isHoliday) => {
const classes: string[] = [];
// Add custom classes based on date properties
if (isToday) classes.push('custom-today');
if (isHoliday) classes.push('custom-holiday');
if (date.getDate() === 1) classes.push('first-of-month');
return classes;
},
getDayCellTooltip: (date, holidayLabel) => {
if (holidayLabel) {
return `🎉 ${holidayLabel}`;
}
return `Date: ${date.toLocaleDateString()}`;
}
};
```
### 2. **Validation Plugins** (`ValidationHook`)
Add custom validation rules:
```typescript
interface ValidationHook {
validateDate?(date: Date, currentValue: DatepickerValue, mode: string): boolean;
validateRange?(startDate: Date, endDate: Date): boolean;
getValidationError?(date: Date): string | null;
}
```
**Use Cases:**
- Business rule validation
- Date range constraints
- Custom disabled date logic
- Error message customization
**Example:**
```typescript
const validationPlugin: DatepickerHooks = {
validateDate: (date, currentValue, mode) => {
// Prevent past dates
const today = new Date();
today.setHours(0, 0, 0, 0);
if (date < today) {
return false;
}
// Prevent weekends
const day = date.getDay();
if (day === 0 || day === 6) {
return false;
}
return true;
},
validateRange: (startDate, endDate) => {
// Ensure minimum 3-day range
const diffTime = endDate.getTime() - startDate.getTime();
const diffDays = diffTime / (1000 * 60 * 60 * 24);
return diffDays >= 3;
},
getValidationError: (date) => {
const today = new Date();
today.setHours(0, 0, 0, 0);
if (date < today) {
return 'Cannot select past dates';
}
return null;
}
};
```
### 3. **Keyboard Shortcut Plugins** (`KeyboardShortcutHook`)
Add custom keyboard shortcuts:
```typescript
interface KeyboardShortcutHook {
handleShortcut?(event: KeyboardEvent, context: KeyboardShortcutContext): boolean;
getShortcutHelp?(): KeyboardShortcutHelp[];
}
```
**Use Cases:**
- Custom navigation shortcuts
- Application-specific shortcuts
- Power user features
- Accessibility enhancements
**Example:**
```typescript
const shortcutPlugin: DatepickerHooks = {
handleShortcut: (event, context) => {
// Custom shortcut: Ctrl+1 for first day of month
if (event.ctrlKey && event.key === '1') {
// Navigate to first day
return true; // Handled
}
// Custom shortcut: Ctrl+L for last day of month
if (event.ctrlKey && event.key === 'l') {
// Navigate to last day
return true; // Handled
}
return false; // Not handled, use default
},
getShortcutHelp: () => [
{ key: 'Ctrl+1', description: 'Go to first day of month' },
{ key: 'Ctrl+L', description: 'Go to last day of month' }
]
};
```
### 4. **Formatting Plugins** (`DateFormatHook`)
Customize date formatting:
```typescript
interface DateFormatHook {
formatDisplayValue?(value: DatepickerValue, mode: string): string;
formatAriaLabel?(date: Date): string;
}
```
**Use Cases:**
- Custom date display formats
- Localized formatting
- Accessibility labels
- Brand-specific formatting
**Example:**
```typescript
const formattingPlugin: DatepickerHooks = {
formatDisplayValue: (value, mode) => {
if (mode === 'single' && value instanceof Date) {
// Custom format: "Monday, January 15, 2024"
return value.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
if (mode === 'range' && typeof value === 'object' && 'start' in value) {
const range = value as { start: Date; end: Date };
return `${range.start.toLocaleDateString()} → ${range.end.toLocaleDateString()}`;
}
return ''; // Use default
},
formatAriaLabel: (date) => {
return `Select ${date.toLocaleDateString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric'
})}`;
}
};
```
### 5. **Event Plugins** (`EventHook`)
React to component lifecycle events:
```typescript
interface EventHook {
beforeDateSelect?(date: Date, currentValue: DatepickerValue): boolean;
afterDateSelect?(date: Date, newValue: DatepickerValue): void;
onCalendarOpen?(): void;
onCalendarClose?(): void;
}
```
**Use Cases:**
- Analytics tracking
- Side effects (API calls, logging)
- Pre-selection validation
- Post-selection actions
**Example:**
```typescript
const eventPlugin: DatepickerHooks = {
beforeDateSelect: (date, currentValue) => {
// Log selection attempt
console.log('Attempting to select:', date);
// Prevent selection on specific dates
if (date.getDate() === 13) {
return false; // Prevent selection
}
return true; // Allow selection
},
afterDateSelect: (date, newValue) => {
// Track analytics
analytics.track('date_selected', {
date: date.toISOString(),
mode: 'single'
});
// Perform side effects
this.updateRelatedComponents(newValue);
},
onCalendarOpen: () => {
console.log('Calendar opened');
analytics.track('calendar_opened');
},
onCalendarClose: () => {
console.log('Calendar closed');
analytics.track('calendar_closed');
}
};
```
## Creating Plugins
### Basic Plugin Structure
A plugin is simply an object that implements one or more hook interfaces:
```typescript
import { DatepickerHooks } from 'ngxsmk-datepicker';
// Create a plugin
const myPlugin: DatepickerHooks = {
// Implement any hooks you need
validateDate: (date) => {
// Your logic here
return true;
}
};
// Use the plugin
@Component({
template: `
<ngxsmk-datepicker
[hooks]="myPlugin"
mode="single">
</ngxsmk-datepicker>
`
})
export class MyComponent {
myPlugin = myPlugin;
}
```
### Reusable Plugin Classes
Create reusable plugin classes for better organization:
```typescript
// plugins/weekend-blocker.plugin.ts
import { DatepickerHooks } from 'ngxsmk-datepicker';
export class WeekendBlockerPlugin implements DatepickerHooks {
validateDate(date: Date): boolean {
const day = date.getDay();
return day !== 0 && day !== 6; // Block weekends
}
getDayCellClasses(date: Date, isSelected: boolean, isDisabled: boolean, isToday: boolean, isHoliday: boolean): string[] {
const day = date.getDay();
if (day === 0 || day === 6) {
return ['weekend-disabled'];
}
return [];
}
}
// Usage
@Component({
template: `
<ngxsmk-datepicker
[hooks]="weekendPlugin"
mode="single">
</ngxsmk-datepicker>
`
})
export class MyComponent {
weekendPlugin = new WeekendBlockerPlugin();
}
```
### Plugin Factories
Create plugins with configuration:
```typescript
// plugins/business-days.plugin.ts
import { DatepickerHooks } from 'ngxsmk-datepicker';
export interface BusinessDaysConfig {
allowedDays: number[]; // 0 = Sunday, 1 = Monday, etc.
customMessage?: string;
}
export function createBusinessDaysPlugin(config: BusinessDaysConfig): DatepickerHooks {
return {
validateDate: (date: Date) => {
const day = date.getDay();
return config.allowedDays.includes(day);
},
getValidationError: (date: Date) => {
if (!config.allowedDays.includes(date.getDay())) {
return config.customMessage || 'Only business days are allowed';
}
return null;
}
};
}
// Usage
@Component({
template: `
<ngxsmk-datepicker
[hooks]="businessDaysPlugin"
mode="single">
</ngxsmk-datepicker>
`
})
export class MyComponent {
businessDaysPlugin = createBusinessDaysPlugin({
allowedDays: [1, 2, 3, 4, 5], // Monday to Friday
customMessage: 'Only weekdays are selectable'
});
}
```
### Composing Multiple Plugins
Combine multiple plugins:
```typescript
// Combine plugins using object spread
const combinedPlugin: DatepickerHooks = {
...weekendBlockerPlugin,
...businessDaysPlugin,
...customStylingPlugin
};
// Or use a utility function
function combinePlugins(...plugins: DatepickerHooks[]): DatepickerHooks {
return Object.assign({}, ...plugins);
}
const combined = combinePlugins(
weekendBlockerPlugin,
businessDaysPlugin,
customStylingPlugin
);
```
## Plugin Lifecycle
Understanding when hooks are called helps you write effective plugins:
### 1. **Initialization Phase**
```
Component Init → Calendar Generation → Hook: getDayCellClasses (for each date)
```
### 2. **User Interaction Phase**
```
User Clicks Date
↓
Hook: beforeDateSelect (can prevent selection)
↓
Hook: validateDate (can prevent selection)
↓
Date Selected (if allowed)
↓
Hook: afterDateSelect
```
### 3. **Rendering Phase**
```
Calendar Render
↓
For each date:
- Hook: getDayCellClasses
- Hook: getDayCellTooltip
- Hook: formatDayNumber
```
### 4. **Keyboard Interaction Phase**
```
Key Press
↓
Hook: handleShortcut (can handle custom shortcuts)
↓
Default Shortcut Handling (if not handled)
```
### 5. **Formatting Phase**
```
Value Change
↓
Hook: formatDisplayValue (for input display)
↓
Display Updated
```
## Advanced Patterns
### 1. **Plugin with State**
Plugins can maintain their own state:
```typescript
export class StatefulPlugin implements DatepickerHooks {
private selectedDates: Date[] = [];
validateDate(date: Date, currentValue: DatepickerValue): boolean {
// Limit to 5 selections
if (Array.isArray(currentValue)) {
return currentValue.length < 5;
}
return true;
}
afterDateSelect(date: Date, newValue: DatepickerValue): void {
// Track selections
if (Array.isArray(newValue)) {
this.selectedDates = newValue;
}
}
}
```
### 2. **Plugin with Dependencies**
Plugins can depend on services:
```typescript
import { Injectable } from '@angular/core';
import { DatepickerHooks } from 'ngxsmk-datepicker';
import { AnalyticsService } from './analytics.service';
@Injectable()
export class AnalyticsPlugin implements DatepickerHooks {
constructor(private analytics: AnalyticsService) {}
afterDateSelect(date: Date, newValue: DatepickerValue): void {
this.analytics.track('date_selected', {
date: date.toISOString(),
value: newValue
});
}
onCalendarOpen(): void {
this.analytics.track('calendar_opened');
}
onCalendarClose(): void {
this.analytics.track('calendar_closed');
}
}
```
### 3. **Conditional Plugin Application**
Apply plugins conditionally:
```typescript
@Component({
template: `
<ngxsmk-datepicker
[hooks]="activePlugins"
mode="single">
</ngxsmk-datepicker>
`
})
export class MyComponent {
enableWeekendBlocking = true;
enableAnalytics = false;
get activePlugins(): DatepickerHooks {
const plugins: DatepickerHooks = {};
if (this.enableWeekendBlocking) {
Object.assign(plugins, weekendBlockerPlugin);
}
if (this.enableAnalytics) {
Object.assign(plugins, analyticsPlugin);
}
return plugins;
}
}
```
### 4. **Plugin Middleware Pattern**
Create middleware-style plugins:
```typescript
function createValidationMiddleware(
...validators: Array<(date: Date) => boolean>
): DatepickerHooks {
return {
validateDate: (date: Date) => {
// All validators must pass
return validators.every(validator => validator(date));
}
};
}
// Usage
const validationPlugin = createValidationMiddleware(
(date) => date.getDay() !== 0, // Not Sunday
(date) => date.getDay() !== 6, // Not Saturday
(date) => date >= new Date() // Not in past
);
```
## Best Practices
### 1. **Keep Plugins Focused**
Each plugin should have a single responsibility:
```typescript
// ✅ Good: Focused plugin
const weekendBlocker: DatepickerHooks = {
validateDate: (date) => date.getDay() !== 0 && date.getDay() !== 6
};
// ❌ Bad: Too many responsibilities
const megaPlugin: DatepickerHooks = {
validateDate: (date) => { /* ... */ },
getDayCellClasses: (date) => { /* ... */ },
formatDisplayValue: (value) => { /* ... */ },
handleShortcut: (event) => { /* ... */ },
// ... too many things
};
```
### 2. **Make Plugins Reusable**
Design plugins to be reusable across projects:
```typescript
// ✅ Good: Configurable and reusable
export function createBusinessDaysPlugin(config: BusinessDaysConfig): DatepickerHooks {
return {
validateDate: (date) => config.allowedDays.includes(date.getDay())
};
}
// ❌ Bad: Hard-coded and not reusable
const myBusinessDays: DatepickerHooks = {
validateDate: (date) => [1, 2, 3, 4, 5].includes(date.getDay())
};
```
### 3. **Optimize Performance**
Keep hook functions lightweight:
```typescript
// ✅ Good: Lightweight and memoized
const memoizedValidation = new Map<string, boolean>();
const optimizedPlugin: DatepickerHooks = {
validateDate: (date) => {
const key = date.toISOString().split('T')[0];
if (memoizedValidation.has(key)) {
return memoizedValidation.get(key)!;
}
const result = /* expensive validation */;
memoizedValidation.set(key, result);
return result;
}
};
// ❌ Bad: Expensive operation on every call
const slowPlugin: DatepickerHooks = {
validateDate: (date) => {
// Expensive API call on every validation
return this.apiService.checkDate(date).toPromise();
}
};
```
### 4. **Provide Clear Error Messages**
Use `getValidationError` for user-friendly messages:
```typescript
// ✅ Good: Clear error messages
const userFriendlyPlugin: DatepickerHooks = {
validateDate: (date) => {
const today = new Date();
today.setHours(0, 0, 0, 0);
return date >= today;
},
getValidationError: (date) => {
const today = new Date();
today.setHours(0, 0, 0, 0);
if (date < today) {
return 'Please select a date from today onwards';
}
return null;
}
};
```
### 5. **Test Your Plugins**
Write tests for your plugins:
```typescript
describe('WeekendBlockerPlugin', () => {
it('should block weekends', () => {
const plugin = new WeekendBlockerPlugin();
const saturday = new Date(2024, 0, 6); // Saturday
const monday = new Date(2024, 0, 8); // Monday
expect(plugin.validateDate?.(saturday)).toBe(false);
expect(plugin.validateDate?.(monday)).toBe(true);
});
});
```
## Complete Example
Here's a complete example of a production-ready plugin:
```typescript
// plugins/business-calendar.plugin.ts
import { DatepickerHooks, DatepickerValue } from 'ngxsmk-datepicker';
export interface BusinessCalendarConfig {
businessDays: number[]; // [1,2,3,4,5] for Mon-Fri
holidays: Date[]; // Array of holiday dates
minAdvanceDays: number; // Minimum days in advance
maxAdvanceDays: number; // Maximum days in advance
}
export function createBusinessCalendarPlugin(
config: BusinessCalendarConfig
): DatepickerHooks {
const today = new Date();
today.setHours(0, 0, 0, 0);
const minDate = new Date(today);
minDate.setDate(today.getDate() + config.minAdvanceDays);
const maxDate = new Date(today);
maxDate.setDate(today.getDate() + config.maxAdvanceDays);
const isHoliday = (date: Date): boolean => {
return config.holidays.some(holiday => {
return holiday.toDateString() === date.toDateString();
});
};
const isBusinessDay = (date: Date): boolean => {
const day = date.getDay();
return config.businessDays.includes(day) && !isHoliday(date);
};
return {
validateDate: (date: Date) => {
// Check if within date range
if (date < minDate || date > maxDate) {
return false;
}
// Check if business day
return isBusinessDay(date);
},
getValidationError: (date: Date) => {
if (date < minDate) {
return `Please select a date at least ${config.minAdvanceDays} days in advance`;
}
if (date > maxDate) {
return `Please select a date within ${config.maxAdvanceDays} days`;
}
if (!isBusinessDay(date)) {
return 'Please select a business day';
}
return null;
},
getDayCellClasses: (date, isSelected, isDisabled, isToday, isHoliday) => {
const classes: string[] = [];
if (!isBusinessDay(date)) {
classes.push('non-business-day');
}
if (isHoliday(date)) {
classes.push('holiday');
}
return classes;
},
getDayCellTooltip: (date, holidayLabel) => {
if (isHoliday(date)) {
return holidayLabel || 'Holiday';
}
if (!isBusinessDay(date)) {
return 'Not a business day';
}
return null;
}
};
}
// Usage
@Component({
template: `
<ngxsmk-datepicker
[hooks]="businessCalendarPlugin"
mode="single"
placeholder="Select a business day">
</ngxsmk-datepicker>
`
})
export class BookingComponent {
businessCalendarPlugin = createBusinessCalendarPlugin({
businessDays: [1, 2, 3, 4, 5], // Monday to Friday
holidays: [
new Date(2024, 0, 1), // New Year's Day
new Date(2024, 6, 4), // Independence Day
new Date(2024, 11, 25) // Christmas
],
minAdvanceDays: 1,
maxAdvanceDays: 90
});
}
```
## Summary
The plugin architecture in ngxsmk-datepicker provides:
- ✅ **Non-invasive extension** - Extend without modifying core code
- ✅ **Type safety** - Full TypeScript support
- ✅ **Composability** - Combine multiple plugins
- ✅ **Flexibility** - Optional and configurable
- ✅ **Performance** - Lightweight hook system
- ✅ **Maintainability** - Clear separation of concerns
For more examples and detailed hook documentation, see [Extension Points Guide](./extension-points.md).