ngxsmk-datepicker 2.3.1 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +942 -922
- package/docs/API.md +2423 -2413
- package/docs/COMPATIBILITY.md +471 -471
- package/docs/INTEGRATION.md +703 -703
- package/docs/IONIC_INTEGRATION.md +228 -228
- package/docs/LOCALE-GUIDE.md +300 -300
- package/docs/PLUGIN-ARCHITECTURE.md +930 -930
- package/docs/SSR-EXAMPLE.md +427 -427
- package/docs/THEME-TOKENS.md +324 -324
- package/docs/TIMEZONE.md +307 -307
- package/docs/extension-points.md +419 -419
- package/docs/signal-forms.md +600 -600
- package/docs/signals.md +266 -266
- package/docs/ssr.md +305 -305
- package/package.json +106 -104
- package/schematics/collection.json +10 -0
- package/schematics/ng-add/schema.json +14 -0
- package/schematics/tsconfig.json +17 -0
- package/CHANGELOG.md +0 -1246
- package/LICENSE +0 -21
- package/MIGRATION.md +0 -1794
- package/docs/FEATURE_SCOPING.md +0 -60
- package/docs/IONIC_TESTING.md +0 -148
- package/docs/REFACTOR_PLAN.md +0 -38
- package/docs/SEO.md +0 -214
- package/fesm2022/ngxsmk-datepicker.mjs +0 -14693
- package/types/ngxsmk-datepicker.d.ts +0 -2489
|
@@ -1,930 +1,930 @@
|
|
|
1
|
-
# Plugin Architecture
|
|
2
|
-
|
|
3
|
-
**Last updated:**
|
|
4
|
-
|
|
5
|
-
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.
|
|
6
|
-
|
|
7
|
-
## Table of Contents
|
|
8
|
-
|
|
9
|
-
- [Overview](#overview)
|
|
10
|
-
- [Architecture Principles](#architecture-principles)
|
|
11
|
-
- [Plugin Types](#plugin-types)
|
|
12
|
-
- [Creating Plugins](#creating-plugins)
|
|
13
|
-
- [Plugin Lifecycle](#plugin-lifecycle)
|
|
14
|
-
- [Advanced Patterns](#advanced-patterns)
|
|
15
|
-
- [Best Practices](#best-practices)
|
|
16
|
-
|
|
17
|
-
## Overview
|
|
18
|
-
|
|
19
|
-
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:
|
|
20
|
-
|
|
21
|
-
- **Intercept** component behavior before it executes
|
|
22
|
-
- **Modify** data and UI rendering
|
|
23
|
-
- **Extend** functionality with custom logic
|
|
24
|
-
- **Validate** user input with custom rules
|
|
25
|
-
- **React** to component events and state changes
|
|
26
|
-
|
|
27
|
-
### Key Concepts
|
|
28
|
-
|
|
29
|
-
```
|
|
30
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
31
|
-
│ Datepicker Component │
|
|
32
|
-
│ │
|
|
33
|
-
│ ┌──────────────────────────────────────────────────────┐ │
|
|
34
|
-
│ │ Core Component Logic │ │
|
|
35
|
-
│ │ │ │
|
|
36
|
-
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
|
|
37
|
-
│ │ │ Render │ │ Validate │ │ Events │ │ │
|
|
38
|
-
│ │ │ Hooks │ │ Hooks │ │ Hooks │ │ │
|
|
39
|
-
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
|
|
40
|
-
│ │ │ │ │ │ │
|
|
41
|
-
│ │ └─────────────┼─────────────┘ │ │
|
|
42
|
-
│ │ │ │ │
|
|
43
|
-
│ │ ┌──────▼──────┐ │ │
|
|
44
|
-
│ │ │ Plugin │ │ │
|
|
45
|
-
│ │ │ Registry │ │ │
|
|
46
|
-
│ │ └──────┬──────┘ │ │
|
|
47
|
-
│ │ │ │ │
|
|
48
|
-
│ └─────────────────────┼───────────────────────────────┘ │
|
|
49
|
-
│ │ │
|
|
50
|
-
└────────────────────────┼───────────────────────────────────┘
|
|
51
|
-
│
|
|
52
|
-
│
|
|
53
|
-
┌───────────────┼───────────────┐
|
|
54
|
-
│ │ │
|
|
55
|
-
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
|
|
56
|
-
│ Plugin │ │ Plugin │ │ Plugin │
|
|
57
|
-
│ A │ │ B │ │ C │
|
|
58
|
-
└─────────┘ └─────────┘ └─────────┘
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## Architecture Principles
|
|
62
|
-
|
|
63
|
-
### 1. **Non-Invasive Extension**
|
|
64
|
-
|
|
65
|
-
Plugins extend functionality without modifying core code:
|
|
66
|
-
|
|
67
|
-
```typescript
|
|
68
|
-
// ✅ Good: Plugin extends behavior
|
|
69
|
-
const myPlugin: DatepickerHooks = {
|
|
70
|
-
validateDate: (date) => date.getDay() !== 0 // Disable Sundays
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
// ❌ Bad: Modifying core component (not possible, but conceptually wrong)
|
|
74
|
-
// Don't try to override internal methods
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### 2. **Composable Architecture**
|
|
78
|
-
|
|
79
|
-
Multiple plugins can work together:
|
|
80
|
-
|
|
81
|
-
```typescript
|
|
82
|
-
// Combine multiple plugins
|
|
83
|
-
const weekendPlugin: DatepickerHooks = {
|
|
84
|
-
validateDate: (date) => date.getDay() !== 0 && date.getDay() !== 6
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const businessDaysPlugin: DatepickerHooks = {
|
|
88
|
-
getDayCellClasses: (date, isSelected, isDisabled, isToday, isHoliday) => {
|
|
89
|
-
const day = date.getDay();
|
|
90
|
-
if (day === 0 || day === 6) {
|
|
91
|
-
return ['weekend-day'];
|
|
92
|
-
}
|
|
93
|
-
return [];
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
// Use both together
|
|
98
|
-
const combinedHooks: DatepickerHooks = {
|
|
99
|
-
...weekendPlugin,
|
|
100
|
-
...businessDaysPlugin
|
|
101
|
-
};
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### 3. **Type-Safe Extensions**
|
|
105
|
-
|
|
106
|
-
All plugins are fully typed with TypeScript:
|
|
107
|
-
|
|
108
|
-
```typescript
|
|
109
|
-
import { DatepickerHooks } from 'ngxsmk-datepicker';
|
|
110
|
-
|
|
111
|
-
// TypeScript ensures type safety
|
|
112
|
-
const plugin: DatepickerHooks = {
|
|
113
|
-
validateDate: (date: Date, currentValue: DatepickerValue, mode: string) => {
|
|
114
|
-
// TypeScript knows the exact types
|
|
115
|
-
return true;
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
### 4. **Optional Execution**
|
|
121
|
-
|
|
122
|
-
Hooks are optional - the component works without them:
|
|
123
|
-
|
|
124
|
-
```typescript
|
|
125
|
-
// Component works fine without plugins
|
|
126
|
-
<ngxsmk-datepicker mode="single"></ngxsmk-datepicker>
|
|
127
|
-
|
|
128
|
-
// Plugins are optional enhancements
|
|
129
|
-
<ngxsmk-datepicker
|
|
130
|
-
[hooks]="myPlugin"
|
|
131
|
-
mode="single">
|
|
132
|
-
</ngxsmk-datepicker>
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
## Plugin Types
|
|
136
|
-
|
|
137
|
-
The plugin architecture consists of **5 main plugin types**, each serving a specific purpose:
|
|
138
|
-
|
|
139
|
-
### 1. **Rendering Plugins** (`DayCellRenderHook`)
|
|
140
|
-
|
|
141
|
-
Control how dates are rendered in the calendar:
|
|
142
|
-
|
|
143
|
-
```typescript
|
|
144
|
-
interface DayCellRenderHook {
|
|
145
|
-
getDayCellClasses?(date: Date, isSelected: boolean, isDisabled: boolean, isToday: boolean, isHoliday: boolean): string[];
|
|
146
|
-
getDayCellTooltip?(date: Date, holidayLabel: string | null): string | null;
|
|
147
|
-
formatDayNumber?(date: Date): string;
|
|
148
|
-
}
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
**Use Cases:**
|
|
152
|
-
- Custom styling for specific dates
|
|
153
|
-
- Conditional CSS classes
|
|
154
|
-
- Custom tooltips
|
|
155
|
-
- Date number formatting
|
|
156
|
-
|
|
157
|
-
**Example:**
|
|
158
|
-
```typescript
|
|
159
|
-
const stylingPlugin: DatepickerHooks = {
|
|
160
|
-
getDayCellClasses: (date, isSelected, isDisabled, isToday, isHoliday) => {
|
|
161
|
-
const classes: string[] = [];
|
|
162
|
-
|
|
163
|
-
// Add custom classes based on date properties
|
|
164
|
-
if (isToday) classes.push('custom-today');
|
|
165
|
-
if (isHoliday) classes.push('custom-holiday');
|
|
166
|
-
if (date.getDate() === 1) classes.push('first-of-month');
|
|
167
|
-
|
|
168
|
-
return classes;
|
|
169
|
-
},
|
|
170
|
-
|
|
171
|
-
getDayCellTooltip: (date, holidayLabel) => {
|
|
172
|
-
if (holidayLabel) {
|
|
173
|
-
return `🎉 ${holidayLabel}`;
|
|
174
|
-
}
|
|
175
|
-
return `Date: ${date.toLocaleDateString()}`;
|
|
176
|
-
}
|
|
177
|
-
};
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
### 2. **Validation Plugins** (`ValidationHook`)
|
|
181
|
-
|
|
182
|
-
Add custom validation rules:
|
|
183
|
-
|
|
184
|
-
```typescript
|
|
185
|
-
interface ValidationHook {
|
|
186
|
-
validateDate?(date: Date, currentValue: DatepickerValue, mode: string): boolean;
|
|
187
|
-
validateRange?(startDate: Date, endDate: Date): boolean;
|
|
188
|
-
getValidationError?(date: Date): string | null;
|
|
189
|
-
}
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
**Use Cases:**
|
|
193
|
-
- Business rule validation
|
|
194
|
-
- Date range constraints
|
|
195
|
-
- Custom disabled date logic
|
|
196
|
-
- Error message customization
|
|
197
|
-
|
|
198
|
-
**Example:**
|
|
199
|
-
```typescript
|
|
200
|
-
const validationPlugin: DatepickerHooks = {
|
|
201
|
-
validateDate: (date, currentValue, mode) => {
|
|
202
|
-
// Prevent past dates
|
|
203
|
-
const today = new Date();
|
|
204
|
-
today.setHours(0, 0, 0, 0);
|
|
205
|
-
if (date < today) {
|
|
206
|
-
return false;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Prevent weekends
|
|
210
|
-
const day = date.getDay();
|
|
211
|
-
if (day === 0 || day === 6) {
|
|
212
|
-
return false;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return true;
|
|
216
|
-
},
|
|
217
|
-
|
|
218
|
-
validateRange: (startDate, endDate) => {
|
|
219
|
-
// Ensure minimum 3-day range
|
|
220
|
-
const diffTime = endDate.getTime() - startDate.getTime();
|
|
221
|
-
const diffDays = diffTime / (1000 * 60 * 60 * 24);
|
|
222
|
-
return diffDays >= 3;
|
|
223
|
-
},
|
|
224
|
-
|
|
225
|
-
getValidationError: (date) => {
|
|
226
|
-
const today = new Date();
|
|
227
|
-
today.setHours(0, 0, 0, 0);
|
|
228
|
-
if (date < today) {
|
|
229
|
-
return 'Cannot select past dates';
|
|
230
|
-
}
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
233
|
-
};
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
### 3. **Keyboard Shortcut Plugins** (`KeyboardShortcutHook`)
|
|
237
|
-
|
|
238
|
-
Add custom keyboard shortcuts:
|
|
239
|
-
|
|
240
|
-
```typescript
|
|
241
|
-
interface KeyboardShortcutHook {
|
|
242
|
-
handleShortcut?(event: KeyboardEvent, context: KeyboardShortcutContext): boolean;
|
|
243
|
-
getShortcutHelp?(): KeyboardShortcutHelp[];
|
|
244
|
-
}
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
**Use Cases:**
|
|
248
|
-
- Custom navigation shortcuts
|
|
249
|
-
- Application-specific shortcuts
|
|
250
|
-
- Power user features
|
|
251
|
-
- Accessibility enhancements
|
|
252
|
-
|
|
253
|
-
**Example:**
|
|
254
|
-
```typescript
|
|
255
|
-
const shortcutPlugin: DatepickerHooks = {
|
|
256
|
-
handleShortcut: (event, context) => {
|
|
257
|
-
// Custom shortcut: Ctrl+1 for first day of month
|
|
258
|
-
if (event.ctrlKey && event.key === '1') {
|
|
259
|
-
// Navigate to first day
|
|
260
|
-
return true; // Handled
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Custom shortcut: Ctrl+L for last day of month
|
|
264
|
-
if (event.ctrlKey && event.key === 'l') {
|
|
265
|
-
// Navigate to last day
|
|
266
|
-
return true; // Handled
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
return false; // Not handled, use default
|
|
270
|
-
},
|
|
271
|
-
|
|
272
|
-
getShortcutHelp: () => [
|
|
273
|
-
{ key: 'Ctrl+1', description: 'Go to first day of month' },
|
|
274
|
-
{ key: 'Ctrl+L', description: 'Go to last day of month' }
|
|
275
|
-
]
|
|
276
|
-
};
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
### 4. **Formatting Plugins** (`DateFormatHook`)
|
|
280
|
-
|
|
281
|
-
Customize date formatting:
|
|
282
|
-
|
|
283
|
-
```typescript
|
|
284
|
-
interface DateFormatHook {
|
|
285
|
-
formatDisplayValue?(value: DatepickerValue, mode: string): string;
|
|
286
|
-
formatAriaLabel?(date: Date): string;
|
|
287
|
-
}
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
**Use Cases:**
|
|
291
|
-
- Custom date display formats
|
|
292
|
-
- Localized formatting
|
|
293
|
-
- Accessibility labels
|
|
294
|
-
- Brand-specific formatting
|
|
295
|
-
|
|
296
|
-
**Example:**
|
|
297
|
-
```typescript
|
|
298
|
-
const formattingPlugin: DatepickerHooks = {
|
|
299
|
-
formatDisplayValue: (value, mode) => {
|
|
300
|
-
if (mode === 'single' && value instanceof Date) {
|
|
301
|
-
// Custom format: "Monday, January 15, 2024"
|
|
302
|
-
return value.toLocaleDateString('en-US', {
|
|
303
|
-
weekday: 'long',
|
|
304
|
-
year: 'numeric',
|
|
305
|
-
month: 'long',
|
|
306
|
-
day: 'numeric'
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (mode === 'range' && typeof value === 'object' && 'start' in value) {
|
|
311
|
-
const range = value as { start: Date; end: Date };
|
|
312
|
-
return `${range.start.toLocaleDateString()} → ${range.end.toLocaleDateString()}`;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
return ''; // Use default
|
|
316
|
-
},
|
|
317
|
-
|
|
318
|
-
formatAriaLabel: (date) => {
|
|
319
|
-
return `Select ${date.toLocaleDateString('en-US', {
|
|
320
|
-
weekday: 'long',
|
|
321
|
-
month: 'long',
|
|
322
|
-
day: 'numeric',
|
|
323
|
-
year: 'numeric'
|
|
324
|
-
})}`;
|
|
325
|
-
}
|
|
326
|
-
};
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
### 5. **Event Plugins** (`EventHook`)
|
|
330
|
-
|
|
331
|
-
React to component lifecycle events:
|
|
332
|
-
|
|
333
|
-
```typescript
|
|
334
|
-
interface EventHook {
|
|
335
|
-
beforeDateSelect?(date: Date, currentValue: DatepickerValue): boolean;
|
|
336
|
-
afterDateSelect?(date: Date, newValue: DatepickerValue): void;
|
|
337
|
-
onCalendarOpen?(): void;
|
|
338
|
-
onCalendarClose?(): void;
|
|
339
|
-
}
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
**Use Cases:**
|
|
343
|
-
- Analytics tracking
|
|
344
|
-
- Side effects (API calls, logging)
|
|
345
|
-
- Pre-selection validation
|
|
346
|
-
- Post-selection actions
|
|
347
|
-
|
|
348
|
-
**Example:**
|
|
349
|
-
```typescript
|
|
350
|
-
const eventPlugin: DatepickerHooks = {
|
|
351
|
-
beforeDateSelect: (date, currentValue) => {
|
|
352
|
-
// Log selection attempt
|
|
353
|
-
console.log('Attempting to select:', date);
|
|
354
|
-
|
|
355
|
-
// Prevent selection on specific dates
|
|
356
|
-
if (date.getDate() === 13) {
|
|
357
|
-
return false; // Prevent selection
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
return true; // Allow selection
|
|
361
|
-
},
|
|
362
|
-
|
|
363
|
-
afterDateSelect: (date, newValue) => {
|
|
364
|
-
// Track analytics
|
|
365
|
-
analytics.track('date_selected', {
|
|
366
|
-
date: date.toISOString(),
|
|
367
|
-
mode: 'single'
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
// Perform side effects
|
|
371
|
-
this.updateRelatedComponents(newValue);
|
|
372
|
-
},
|
|
373
|
-
|
|
374
|
-
onCalendarOpen: () => {
|
|
375
|
-
console.log('Calendar opened');
|
|
376
|
-
analytics.track('calendar_opened');
|
|
377
|
-
},
|
|
378
|
-
|
|
379
|
-
onCalendarClose: () => {
|
|
380
|
-
console.log('Calendar closed');
|
|
381
|
-
analytics.track('calendar_closed');
|
|
382
|
-
}
|
|
383
|
-
};
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
## Creating Plugins
|
|
387
|
-
|
|
388
|
-
### Basic Plugin Structure
|
|
389
|
-
|
|
390
|
-
A plugin is simply an object that implements one or more hook interfaces:
|
|
391
|
-
|
|
392
|
-
```typescript
|
|
393
|
-
import { DatepickerHooks } from 'ngxsmk-datepicker';
|
|
394
|
-
|
|
395
|
-
// Create a plugin
|
|
396
|
-
const myPlugin: DatepickerHooks = {
|
|
397
|
-
// Implement any hooks you need
|
|
398
|
-
validateDate: (date) => {
|
|
399
|
-
// Your logic here
|
|
400
|
-
return true;
|
|
401
|
-
}
|
|
402
|
-
};
|
|
403
|
-
|
|
404
|
-
// Use the plugin
|
|
405
|
-
@Component({
|
|
406
|
-
template: `
|
|
407
|
-
<ngxsmk-datepicker
|
|
408
|
-
[hooks]="myPlugin"
|
|
409
|
-
mode="single">
|
|
410
|
-
</ngxsmk-datepicker>
|
|
411
|
-
`
|
|
412
|
-
})
|
|
413
|
-
export class MyComponent {
|
|
414
|
-
myPlugin = myPlugin;
|
|
415
|
-
}
|
|
416
|
-
```
|
|
417
|
-
|
|
418
|
-
### Reusable Plugin Classes
|
|
419
|
-
|
|
420
|
-
Create reusable plugin classes for better organization:
|
|
421
|
-
|
|
422
|
-
```typescript
|
|
423
|
-
// plugins/weekend-blocker.plugin.ts
|
|
424
|
-
import { DatepickerHooks } from 'ngxsmk-datepicker';
|
|
425
|
-
|
|
426
|
-
export class WeekendBlockerPlugin implements DatepickerHooks {
|
|
427
|
-
validateDate(date: Date): boolean {
|
|
428
|
-
const day = date.getDay();
|
|
429
|
-
return day !== 0 && day !== 6; // Block weekends
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
getDayCellClasses(date: Date, isSelected: boolean, isDisabled: boolean, isToday: boolean, isHoliday: boolean): string[] {
|
|
433
|
-
const day = date.getDay();
|
|
434
|
-
if (day === 0 || day === 6) {
|
|
435
|
-
return ['weekend-disabled'];
|
|
436
|
-
}
|
|
437
|
-
return [];
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// Usage
|
|
442
|
-
@Component({
|
|
443
|
-
template: `
|
|
444
|
-
<ngxsmk-datepicker
|
|
445
|
-
[hooks]="weekendPlugin"
|
|
446
|
-
mode="single">
|
|
447
|
-
</ngxsmk-datepicker>
|
|
448
|
-
`
|
|
449
|
-
})
|
|
450
|
-
export class MyComponent {
|
|
451
|
-
weekendPlugin = new WeekendBlockerPlugin();
|
|
452
|
-
}
|
|
453
|
-
```
|
|
454
|
-
|
|
455
|
-
### Plugin Factories
|
|
456
|
-
|
|
457
|
-
Create plugins with configuration:
|
|
458
|
-
|
|
459
|
-
```typescript
|
|
460
|
-
// plugins/business-days.plugin.ts
|
|
461
|
-
import { DatepickerHooks } from 'ngxsmk-datepicker';
|
|
462
|
-
|
|
463
|
-
export interface BusinessDaysConfig {
|
|
464
|
-
allowedDays: number[]; // 0 = Sunday, 1 = Monday, etc.
|
|
465
|
-
customMessage?: string;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
export function createBusinessDaysPlugin(config: BusinessDaysConfig): DatepickerHooks {
|
|
469
|
-
return {
|
|
470
|
-
validateDate: (date: Date) => {
|
|
471
|
-
const day = date.getDay();
|
|
472
|
-
return config.allowedDays.includes(day);
|
|
473
|
-
},
|
|
474
|
-
|
|
475
|
-
getValidationError: (date: Date) => {
|
|
476
|
-
if (!config.allowedDays.includes(date.getDay())) {
|
|
477
|
-
return config.customMessage || 'Only business days are allowed';
|
|
478
|
-
}
|
|
479
|
-
return null;
|
|
480
|
-
}
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
// Usage
|
|
485
|
-
@Component({
|
|
486
|
-
template: `
|
|
487
|
-
<ngxsmk-datepicker
|
|
488
|
-
[hooks]="businessDaysPlugin"
|
|
489
|
-
mode="single">
|
|
490
|
-
</ngxsmk-datepicker>
|
|
491
|
-
`
|
|
492
|
-
})
|
|
493
|
-
export class MyComponent {
|
|
494
|
-
businessDaysPlugin = createBusinessDaysPlugin({
|
|
495
|
-
allowedDays: [1, 2, 3, 4, 5], // Monday to Friday
|
|
496
|
-
customMessage: 'Only weekdays are selectable'
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
### Composing Multiple Plugins
|
|
502
|
-
|
|
503
|
-
Combine multiple plugins:
|
|
504
|
-
|
|
505
|
-
```typescript
|
|
506
|
-
// Combine plugins using object spread
|
|
507
|
-
const combinedPlugin: DatepickerHooks = {
|
|
508
|
-
...weekendBlockerPlugin,
|
|
509
|
-
...businessDaysPlugin,
|
|
510
|
-
...customStylingPlugin
|
|
511
|
-
};
|
|
512
|
-
|
|
513
|
-
// Or use a utility function
|
|
514
|
-
function combinePlugins(...plugins: DatepickerHooks[]): DatepickerHooks {
|
|
515
|
-
return Object.assign({}, ...plugins);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
const combined = combinePlugins(
|
|
519
|
-
weekendBlockerPlugin,
|
|
520
|
-
businessDaysPlugin,
|
|
521
|
-
customStylingPlugin
|
|
522
|
-
);
|
|
523
|
-
```
|
|
524
|
-
|
|
525
|
-
## Plugin Lifecycle
|
|
526
|
-
|
|
527
|
-
Understanding when hooks are called helps you write effective plugins:
|
|
528
|
-
|
|
529
|
-
### 1. **Initialization Phase**
|
|
530
|
-
|
|
531
|
-
```
|
|
532
|
-
Component Init → Calendar Generation → Hook: getDayCellClasses (for each date)
|
|
533
|
-
```
|
|
534
|
-
|
|
535
|
-
### 2. **User Interaction Phase**
|
|
536
|
-
|
|
537
|
-
```
|
|
538
|
-
User Clicks Date
|
|
539
|
-
↓
|
|
540
|
-
Hook: beforeDateSelect (can prevent selection)
|
|
541
|
-
↓
|
|
542
|
-
Hook: validateDate (can prevent selection)
|
|
543
|
-
↓
|
|
544
|
-
Date Selected (if allowed)
|
|
545
|
-
↓
|
|
546
|
-
Hook: afterDateSelect
|
|
547
|
-
```
|
|
548
|
-
|
|
549
|
-
### 3. **Rendering Phase**
|
|
550
|
-
|
|
551
|
-
```
|
|
552
|
-
Calendar Render
|
|
553
|
-
↓
|
|
554
|
-
For each date:
|
|
555
|
-
- Hook: getDayCellClasses
|
|
556
|
-
- Hook: getDayCellTooltip
|
|
557
|
-
- Hook: formatDayNumber
|
|
558
|
-
```
|
|
559
|
-
|
|
560
|
-
### 4. **Keyboard Interaction Phase**
|
|
561
|
-
|
|
562
|
-
```
|
|
563
|
-
Key Press
|
|
564
|
-
↓
|
|
565
|
-
Hook: handleShortcut (can handle custom shortcuts)
|
|
566
|
-
↓
|
|
567
|
-
Default Shortcut Handling (if not handled)
|
|
568
|
-
```
|
|
569
|
-
|
|
570
|
-
### 5. **Formatting Phase**
|
|
571
|
-
|
|
572
|
-
```
|
|
573
|
-
Value Change
|
|
574
|
-
↓
|
|
575
|
-
Hook: formatDisplayValue (for input display)
|
|
576
|
-
↓
|
|
577
|
-
Display Updated
|
|
578
|
-
```
|
|
579
|
-
|
|
580
|
-
## Advanced Patterns
|
|
581
|
-
|
|
582
|
-
### 1. **Plugin with State**
|
|
583
|
-
|
|
584
|
-
Plugins can maintain their own state:
|
|
585
|
-
|
|
586
|
-
```typescript
|
|
587
|
-
export class StatefulPlugin implements DatepickerHooks {
|
|
588
|
-
private selectedDates: Date[] = [];
|
|
589
|
-
|
|
590
|
-
validateDate(date: Date, currentValue: DatepickerValue): boolean {
|
|
591
|
-
// Limit to 5 selections
|
|
592
|
-
if (Array.isArray(currentValue)) {
|
|
593
|
-
return currentValue.length < 5;
|
|
594
|
-
}
|
|
595
|
-
return true;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
afterDateSelect(date: Date, newValue: DatepickerValue): void {
|
|
599
|
-
// Track selections
|
|
600
|
-
if (Array.isArray(newValue)) {
|
|
601
|
-
this.selectedDates = newValue;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
```
|
|
606
|
-
|
|
607
|
-
### 2. **Plugin with Dependencies**
|
|
608
|
-
|
|
609
|
-
Plugins can depend on services:
|
|
610
|
-
|
|
611
|
-
```typescript
|
|
612
|
-
import { Injectable } from '@angular/core';
|
|
613
|
-
import { DatepickerHooks } from 'ngxsmk-datepicker';
|
|
614
|
-
import { AnalyticsService } from './analytics.service';
|
|
615
|
-
|
|
616
|
-
@Injectable()
|
|
617
|
-
export class AnalyticsPlugin implements DatepickerHooks {
|
|
618
|
-
constructor(private analytics: AnalyticsService) {}
|
|
619
|
-
|
|
620
|
-
afterDateSelect(date: Date, newValue: DatepickerValue): void {
|
|
621
|
-
this.analytics.track('date_selected', {
|
|
622
|
-
date: date.toISOString(),
|
|
623
|
-
value: newValue
|
|
624
|
-
});
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
onCalendarOpen(): void {
|
|
628
|
-
this.analytics.track('calendar_opened');
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
onCalendarClose(): void {
|
|
632
|
-
this.analytics.track('calendar_closed');
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
```
|
|
636
|
-
|
|
637
|
-
### 3. **Conditional Plugin Application**
|
|
638
|
-
|
|
639
|
-
Apply plugins conditionally:
|
|
640
|
-
|
|
641
|
-
```typescript
|
|
642
|
-
@Component({
|
|
643
|
-
template: `
|
|
644
|
-
<ngxsmk-datepicker
|
|
645
|
-
[hooks]="activePlugins"
|
|
646
|
-
mode="single">
|
|
647
|
-
</ngxsmk-datepicker>
|
|
648
|
-
`
|
|
649
|
-
})
|
|
650
|
-
export class MyComponent {
|
|
651
|
-
enableWeekendBlocking = true;
|
|
652
|
-
enableAnalytics = false;
|
|
653
|
-
|
|
654
|
-
get activePlugins(): DatepickerHooks {
|
|
655
|
-
const plugins: DatepickerHooks = {};
|
|
656
|
-
|
|
657
|
-
if (this.enableWeekendBlocking) {
|
|
658
|
-
Object.assign(plugins, weekendBlockerPlugin);
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
if (this.enableAnalytics) {
|
|
662
|
-
Object.assign(plugins, analyticsPlugin);
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
return plugins;
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
```
|
|
669
|
-
|
|
670
|
-
### 4. **Plugin Middleware Pattern**
|
|
671
|
-
|
|
672
|
-
Create middleware-style plugins:
|
|
673
|
-
|
|
674
|
-
```typescript
|
|
675
|
-
function createValidationMiddleware(
|
|
676
|
-
...validators: Array<(date: Date) => boolean>
|
|
677
|
-
): DatepickerHooks {
|
|
678
|
-
return {
|
|
679
|
-
validateDate: (date: Date) => {
|
|
680
|
-
// All validators must pass
|
|
681
|
-
return validators.every(validator => validator(date));
|
|
682
|
-
}
|
|
683
|
-
};
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// Usage
|
|
687
|
-
const validationPlugin = createValidationMiddleware(
|
|
688
|
-
(date) => date.getDay() !== 0, // Not Sunday
|
|
689
|
-
(date) => date.getDay() !== 6, // Not Saturday
|
|
690
|
-
(date) => date >= new Date() // Not in past
|
|
691
|
-
);
|
|
692
|
-
```
|
|
693
|
-
|
|
694
|
-
## Best Practices
|
|
695
|
-
|
|
696
|
-
### 1. **Keep Plugins Focused**
|
|
697
|
-
|
|
698
|
-
Each plugin should have a single responsibility:
|
|
699
|
-
|
|
700
|
-
```typescript
|
|
701
|
-
// ✅ Good: Focused plugin
|
|
702
|
-
const weekendBlocker: DatepickerHooks = {
|
|
703
|
-
validateDate: (date) => date.getDay() !== 0 && date.getDay() !== 6
|
|
704
|
-
};
|
|
705
|
-
|
|
706
|
-
// ❌ Bad: Too many responsibilities
|
|
707
|
-
const megaPlugin: DatepickerHooks = {
|
|
708
|
-
validateDate: (date) => { /* ... */ },
|
|
709
|
-
getDayCellClasses: (date) => { /* ... */ },
|
|
710
|
-
formatDisplayValue: (value) => { /* ... */ },
|
|
711
|
-
handleShortcut: (event) => { /* ... */ },
|
|
712
|
-
// ... too many things
|
|
713
|
-
};
|
|
714
|
-
```
|
|
715
|
-
|
|
716
|
-
### 2. **Make Plugins Reusable**
|
|
717
|
-
|
|
718
|
-
Design plugins to be reusable across projects:
|
|
719
|
-
|
|
720
|
-
```typescript
|
|
721
|
-
// ✅ Good: Configurable and reusable
|
|
722
|
-
export function createBusinessDaysPlugin(config: BusinessDaysConfig): DatepickerHooks {
|
|
723
|
-
return {
|
|
724
|
-
validateDate: (date) => config.allowedDays.includes(date.getDay())
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
// ❌ Bad: Hard-coded and not reusable
|
|
729
|
-
const myBusinessDays: DatepickerHooks = {
|
|
730
|
-
validateDate: (date) => [1, 2, 3, 4, 5].includes(date.getDay())
|
|
731
|
-
};
|
|
732
|
-
```
|
|
733
|
-
|
|
734
|
-
### 3. **Optimize Performance**
|
|
735
|
-
|
|
736
|
-
Keep hook functions lightweight:
|
|
737
|
-
|
|
738
|
-
```typescript
|
|
739
|
-
// ✅ Good: Lightweight and memoized
|
|
740
|
-
const memoizedValidation = new Map<string, boolean>();
|
|
741
|
-
|
|
742
|
-
const optimizedPlugin: DatepickerHooks = {
|
|
743
|
-
validateDate: (date) => {
|
|
744
|
-
const key = date.toISOString().split('T')[0];
|
|
745
|
-
if (memoizedValidation.has(key)) {
|
|
746
|
-
return memoizedValidation.get(key)!;
|
|
747
|
-
}
|
|
748
|
-
const result = /* expensive validation */;
|
|
749
|
-
memoizedValidation.set(key, result);
|
|
750
|
-
return result;
|
|
751
|
-
}
|
|
752
|
-
};
|
|
753
|
-
|
|
754
|
-
// ❌ Bad: Expensive operation on every call
|
|
755
|
-
const slowPlugin: DatepickerHooks = {
|
|
756
|
-
validateDate: (date) => {
|
|
757
|
-
// Expensive API call on every validation
|
|
758
|
-
return this.apiService.checkDate(date).toPromise();
|
|
759
|
-
}
|
|
760
|
-
};
|
|
761
|
-
```
|
|
762
|
-
|
|
763
|
-
### 4. **Provide Clear Error Messages**
|
|
764
|
-
|
|
765
|
-
Use `getValidationError` for user-friendly messages:
|
|
766
|
-
|
|
767
|
-
```typescript
|
|
768
|
-
// ✅ Good: Clear error messages
|
|
769
|
-
const userFriendlyPlugin: DatepickerHooks = {
|
|
770
|
-
validateDate: (date) => {
|
|
771
|
-
const today = new Date();
|
|
772
|
-
today.setHours(0, 0, 0, 0);
|
|
773
|
-
return date >= today;
|
|
774
|
-
},
|
|
775
|
-
|
|
776
|
-
getValidationError: (date) => {
|
|
777
|
-
const today = new Date();
|
|
778
|
-
today.setHours(0, 0, 0, 0);
|
|
779
|
-
if (date < today) {
|
|
780
|
-
return 'Please select a date from today onwards';
|
|
781
|
-
}
|
|
782
|
-
return null;
|
|
783
|
-
}
|
|
784
|
-
};
|
|
785
|
-
```
|
|
786
|
-
|
|
787
|
-
### 5. **Test Your Plugins**
|
|
788
|
-
|
|
789
|
-
Write tests for your plugins:
|
|
790
|
-
|
|
791
|
-
```typescript
|
|
792
|
-
describe('WeekendBlockerPlugin', () => {
|
|
793
|
-
it('should block weekends', () => {
|
|
794
|
-
const plugin = new WeekendBlockerPlugin();
|
|
795
|
-
const saturday = new Date(2024, 0, 6); // Saturday
|
|
796
|
-
const monday = new Date(2024, 0, 8); // Monday
|
|
797
|
-
|
|
798
|
-
expect(plugin.validateDate?.(saturday)).toBe(false);
|
|
799
|
-
expect(plugin.validateDate?.(monday)).toBe(true);
|
|
800
|
-
});
|
|
801
|
-
});
|
|
802
|
-
```
|
|
803
|
-
|
|
804
|
-
## Complete Example
|
|
805
|
-
|
|
806
|
-
Here's a complete example of a production-ready plugin:
|
|
807
|
-
|
|
808
|
-
```typescript
|
|
809
|
-
// plugins/business-calendar.plugin.ts
|
|
810
|
-
import { DatepickerHooks, DatepickerValue } from 'ngxsmk-datepicker';
|
|
811
|
-
|
|
812
|
-
export interface BusinessCalendarConfig {
|
|
813
|
-
businessDays: number[]; // [1,2,3,4,5] for Mon-Fri
|
|
814
|
-
holidays: Date[]; // Array of holiday dates
|
|
815
|
-
minAdvanceDays: number; // Minimum days in advance
|
|
816
|
-
maxAdvanceDays: number; // Maximum days in advance
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
export function createBusinessCalendarPlugin(
|
|
820
|
-
config: BusinessCalendarConfig
|
|
821
|
-
): DatepickerHooks {
|
|
822
|
-
const today = new Date();
|
|
823
|
-
today.setHours(0, 0, 0, 0);
|
|
824
|
-
|
|
825
|
-
const minDate = new Date(today);
|
|
826
|
-
minDate.setDate(today.getDate() + config.minAdvanceDays);
|
|
827
|
-
|
|
828
|
-
const maxDate = new Date(today);
|
|
829
|
-
maxDate.setDate(today.getDate() + config.maxAdvanceDays);
|
|
830
|
-
|
|
831
|
-
const isHoliday = (date: Date): boolean => {
|
|
832
|
-
return config.holidays.some(holiday => {
|
|
833
|
-
return holiday.toDateString() === date.toDateString();
|
|
834
|
-
});
|
|
835
|
-
};
|
|
836
|
-
|
|
837
|
-
const isBusinessDay = (date: Date): boolean => {
|
|
838
|
-
const day = date.getDay();
|
|
839
|
-
return config.businessDays.includes(day) && !isHoliday(date);
|
|
840
|
-
};
|
|
841
|
-
|
|
842
|
-
return {
|
|
843
|
-
validateDate: (date: Date) => {
|
|
844
|
-
// Check if within date range
|
|
845
|
-
if (date < minDate || date > maxDate) {
|
|
846
|
-
return false;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
// Check if business day
|
|
850
|
-
return isBusinessDay(date);
|
|
851
|
-
},
|
|
852
|
-
|
|
853
|
-
getValidationError: (date: Date) => {
|
|
854
|
-
if (date < minDate) {
|
|
855
|
-
return `Please select a date at least ${config.minAdvanceDays} days in advance`;
|
|
856
|
-
}
|
|
857
|
-
if (date > maxDate) {
|
|
858
|
-
return `Please select a date within ${config.maxAdvanceDays} days`;
|
|
859
|
-
}
|
|
860
|
-
if (!isBusinessDay(date)) {
|
|
861
|
-
return 'Please select a business day';
|
|
862
|
-
}
|
|
863
|
-
return null;
|
|
864
|
-
},
|
|
865
|
-
|
|
866
|
-
getDayCellClasses: (date, isSelected, isDisabled, isToday, isHoliday) => {
|
|
867
|
-
const classes: string[] = [];
|
|
868
|
-
|
|
869
|
-
if (!isBusinessDay(date)) {
|
|
870
|
-
classes.push('non-business-day');
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
if (isHoliday(date)) {
|
|
874
|
-
classes.push('holiday');
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
return classes;
|
|
878
|
-
},
|
|
879
|
-
|
|
880
|
-
getDayCellTooltip: (date, holidayLabel) => {
|
|
881
|
-
if (isHoliday(date)) {
|
|
882
|
-
return holidayLabel || 'Holiday';
|
|
883
|
-
}
|
|
884
|
-
if (!isBusinessDay(date)) {
|
|
885
|
-
return 'Not a business day';
|
|
886
|
-
}
|
|
887
|
-
return null;
|
|
888
|
-
}
|
|
889
|
-
};
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
// Usage
|
|
893
|
-
@Component({
|
|
894
|
-
template: `
|
|
895
|
-
<ngxsmk-datepicker
|
|
896
|
-
[hooks]="businessCalendarPlugin"
|
|
897
|
-
mode="single"
|
|
898
|
-
placeholder="Select a business day">
|
|
899
|
-
</ngxsmk-datepicker>
|
|
900
|
-
`
|
|
901
|
-
})
|
|
902
|
-
export class BookingComponent {
|
|
903
|
-
businessCalendarPlugin = createBusinessCalendarPlugin({
|
|
904
|
-
businessDays: [1, 2, 3, 4, 5], // Monday to Friday
|
|
905
|
-
holidays: [
|
|
906
|
-
new Date(2024, 0, 1), // New Year's Day
|
|
907
|
-
new Date(2024, 6, 4), // Independence Day
|
|
908
|
-
new Date(2024, 11, 25) // Christmas
|
|
909
|
-
],
|
|
910
|
-
minAdvanceDays: 1,
|
|
911
|
-
maxAdvanceDays: 90
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
```
|
|
915
|
-
|
|
916
|
-
## Summary
|
|
917
|
-
|
|
918
|
-
The plugin architecture in ngxsmk-datepicker provides:
|
|
919
|
-
|
|
920
|
-
- ✅ **Non-invasive extension** - Extend without modifying core code
|
|
921
|
-
- ✅ **Type safety** - Full TypeScript support
|
|
922
|
-
- ✅ **Composability** - Combine multiple plugins
|
|
923
|
-
- ✅ **Flexibility** - Optional and configurable
|
|
924
|
-
- ✅ **Performance** - Lightweight hook system
|
|
925
|
-
- ✅ **Maintainability** - Clear separation of concerns
|
|
926
|
-
|
|
927
|
-
For more examples and detailed hook documentation, see [Extension Points Guide](./extension-points.md).
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1
|
+
# Plugin Architecture
|
|
2
|
+
|
|
3
|
+
**Last updated:** July 2, 2026 - **Current stable:** v2.4.0
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Overview](#overview)
|
|
10
|
+
- [Architecture Principles](#architecture-principles)
|
|
11
|
+
- [Plugin Types](#plugin-types)
|
|
12
|
+
- [Creating Plugins](#creating-plugins)
|
|
13
|
+
- [Plugin Lifecycle](#plugin-lifecycle)
|
|
14
|
+
- [Advanced Patterns](#advanced-patterns)
|
|
15
|
+
- [Best Practices](#best-practices)
|
|
16
|
+
|
|
17
|
+
## Overview
|
|
18
|
+
|
|
19
|
+
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:
|
|
20
|
+
|
|
21
|
+
- **Intercept** component behavior before it executes
|
|
22
|
+
- **Modify** data and UI rendering
|
|
23
|
+
- **Extend** functionality with custom logic
|
|
24
|
+
- **Validate** user input with custom rules
|
|
25
|
+
- **React** to component events and state changes
|
|
26
|
+
|
|
27
|
+
### Key Concepts
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
31
|
+
│ Datepicker Component │
|
|
32
|
+
│ │
|
|
33
|
+
│ ┌──────────────────────────────────────────────────────┐ │
|
|
34
|
+
│ │ Core Component Logic │ │
|
|
35
|
+
│ │ │ │
|
|
36
|
+
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
|
|
37
|
+
│ │ │ Render │ │ Validate │ │ Events │ │ │
|
|
38
|
+
│ │ │ Hooks │ │ Hooks │ │ Hooks │ │ │
|
|
39
|
+
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
|
|
40
|
+
│ │ │ │ │ │ │
|
|
41
|
+
│ │ └─────────────┼─────────────┘ │ │
|
|
42
|
+
│ │ │ │ │
|
|
43
|
+
│ │ ┌──────▼──────┐ │ │
|
|
44
|
+
│ │ │ Plugin │ │ │
|
|
45
|
+
│ │ │ Registry │ │ │
|
|
46
|
+
│ │ └──────┬──────┘ │ │
|
|
47
|
+
│ │ │ │ │
|
|
48
|
+
│ └─────────────────────┼───────────────────────────────┘ │
|
|
49
|
+
│ │ │
|
|
50
|
+
└────────────────────────┼───────────────────────────────────┘
|
|
51
|
+
│
|
|
52
|
+
│
|
|
53
|
+
┌───────────────┼───────────────┐
|
|
54
|
+
│ │ │
|
|
55
|
+
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
|
|
56
|
+
│ Plugin │ │ Plugin │ │ Plugin │
|
|
57
|
+
│ A │ │ B │ │ C │
|
|
58
|
+
└─────────┘ └─────────┘ └─────────┘
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Architecture Principles
|
|
62
|
+
|
|
63
|
+
### 1. **Non-Invasive Extension**
|
|
64
|
+
|
|
65
|
+
Plugins extend functionality without modifying core code:
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// ✅ Good: Plugin extends behavior
|
|
69
|
+
const myPlugin: DatepickerHooks = {
|
|
70
|
+
validateDate: (date) => date.getDay() !== 0 // Disable Sundays
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// ❌ Bad: Modifying core component (not possible, but conceptually wrong)
|
|
74
|
+
// Don't try to override internal methods
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 2. **Composable Architecture**
|
|
78
|
+
|
|
79
|
+
Multiple plugins can work together:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// Combine multiple plugins
|
|
83
|
+
const weekendPlugin: DatepickerHooks = {
|
|
84
|
+
validateDate: (date) => date.getDay() !== 0 && date.getDay() !== 6
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const businessDaysPlugin: DatepickerHooks = {
|
|
88
|
+
getDayCellClasses: (date, isSelected, isDisabled, isToday, isHoliday) => {
|
|
89
|
+
const day = date.getDay();
|
|
90
|
+
if (day === 0 || day === 6) {
|
|
91
|
+
return ['weekend-day'];
|
|
92
|
+
}
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Use both together
|
|
98
|
+
const combinedHooks: DatepickerHooks = {
|
|
99
|
+
...weekendPlugin,
|
|
100
|
+
...businessDaysPlugin
|
|
101
|
+
};
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 3. **Type-Safe Extensions**
|
|
105
|
+
|
|
106
|
+
All plugins are fully typed with TypeScript:
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { DatepickerHooks } from 'ngxsmk-datepicker';
|
|
110
|
+
|
|
111
|
+
// TypeScript ensures type safety
|
|
112
|
+
const plugin: DatepickerHooks = {
|
|
113
|
+
validateDate: (date: Date, currentValue: DatepickerValue, mode: string) => {
|
|
114
|
+
// TypeScript knows the exact types
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 4. **Optional Execution**
|
|
121
|
+
|
|
122
|
+
Hooks are optional - the component works without them:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// Component works fine without plugins
|
|
126
|
+
<ngxsmk-datepicker mode="single"></ngxsmk-datepicker>
|
|
127
|
+
|
|
128
|
+
// Plugins are optional enhancements
|
|
129
|
+
<ngxsmk-datepicker
|
|
130
|
+
[hooks]="myPlugin"
|
|
131
|
+
mode="single">
|
|
132
|
+
</ngxsmk-datepicker>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Plugin Types
|
|
136
|
+
|
|
137
|
+
The plugin architecture consists of **5 main plugin types**, each serving a specific purpose:
|
|
138
|
+
|
|
139
|
+
### 1. **Rendering Plugins** (`DayCellRenderHook`)
|
|
140
|
+
|
|
141
|
+
Control how dates are rendered in the calendar:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
interface DayCellRenderHook {
|
|
145
|
+
getDayCellClasses?(date: Date, isSelected: boolean, isDisabled: boolean, isToday: boolean, isHoliday: boolean): string[];
|
|
146
|
+
getDayCellTooltip?(date: Date, holidayLabel: string | null): string | null;
|
|
147
|
+
formatDayNumber?(date: Date): string;
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Use Cases:**
|
|
152
|
+
- Custom styling for specific dates
|
|
153
|
+
- Conditional CSS classes
|
|
154
|
+
- Custom tooltips
|
|
155
|
+
- Date number formatting
|
|
156
|
+
|
|
157
|
+
**Example:**
|
|
158
|
+
```typescript
|
|
159
|
+
const stylingPlugin: DatepickerHooks = {
|
|
160
|
+
getDayCellClasses: (date, isSelected, isDisabled, isToday, isHoliday) => {
|
|
161
|
+
const classes: string[] = [];
|
|
162
|
+
|
|
163
|
+
// Add custom classes based on date properties
|
|
164
|
+
if (isToday) classes.push('custom-today');
|
|
165
|
+
if (isHoliday) classes.push('custom-holiday');
|
|
166
|
+
if (date.getDate() === 1) classes.push('first-of-month');
|
|
167
|
+
|
|
168
|
+
return classes;
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
getDayCellTooltip: (date, holidayLabel) => {
|
|
172
|
+
if (holidayLabel) {
|
|
173
|
+
return `🎉 ${holidayLabel}`;
|
|
174
|
+
}
|
|
175
|
+
return `Date: ${date.toLocaleDateString()}`;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### 2. **Validation Plugins** (`ValidationHook`)
|
|
181
|
+
|
|
182
|
+
Add custom validation rules:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
interface ValidationHook {
|
|
186
|
+
validateDate?(date: Date, currentValue: DatepickerValue, mode: string): boolean;
|
|
187
|
+
validateRange?(startDate: Date, endDate: Date): boolean;
|
|
188
|
+
getValidationError?(date: Date): string | null;
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Use Cases:**
|
|
193
|
+
- Business rule validation
|
|
194
|
+
- Date range constraints
|
|
195
|
+
- Custom disabled date logic
|
|
196
|
+
- Error message customization
|
|
197
|
+
|
|
198
|
+
**Example:**
|
|
199
|
+
```typescript
|
|
200
|
+
const validationPlugin: DatepickerHooks = {
|
|
201
|
+
validateDate: (date, currentValue, mode) => {
|
|
202
|
+
// Prevent past dates
|
|
203
|
+
const today = new Date();
|
|
204
|
+
today.setHours(0, 0, 0, 0);
|
|
205
|
+
if (date < today) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Prevent weekends
|
|
210
|
+
const day = date.getDay();
|
|
211
|
+
if (day === 0 || day === 6) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return true;
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
validateRange: (startDate, endDate) => {
|
|
219
|
+
// Ensure minimum 3-day range
|
|
220
|
+
const diffTime = endDate.getTime() - startDate.getTime();
|
|
221
|
+
const diffDays = diffTime / (1000 * 60 * 60 * 24);
|
|
222
|
+
return diffDays >= 3;
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
getValidationError: (date) => {
|
|
226
|
+
const today = new Date();
|
|
227
|
+
today.setHours(0, 0, 0, 0);
|
|
228
|
+
if (date < today) {
|
|
229
|
+
return 'Cannot select past dates';
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### 3. **Keyboard Shortcut Plugins** (`KeyboardShortcutHook`)
|
|
237
|
+
|
|
238
|
+
Add custom keyboard shortcuts:
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
interface KeyboardShortcutHook {
|
|
242
|
+
handleShortcut?(event: KeyboardEvent, context: KeyboardShortcutContext): boolean;
|
|
243
|
+
getShortcutHelp?(): KeyboardShortcutHelp[];
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Use Cases:**
|
|
248
|
+
- Custom navigation shortcuts
|
|
249
|
+
- Application-specific shortcuts
|
|
250
|
+
- Power user features
|
|
251
|
+
- Accessibility enhancements
|
|
252
|
+
|
|
253
|
+
**Example:**
|
|
254
|
+
```typescript
|
|
255
|
+
const shortcutPlugin: DatepickerHooks = {
|
|
256
|
+
handleShortcut: (event, context) => {
|
|
257
|
+
// Custom shortcut: Ctrl+1 for first day of month
|
|
258
|
+
if (event.ctrlKey && event.key === '1') {
|
|
259
|
+
// Navigate to first day
|
|
260
|
+
return true; // Handled
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Custom shortcut: Ctrl+L for last day of month
|
|
264
|
+
if (event.ctrlKey && event.key === 'l') {
|
|
265
|
+
// Navigate to last day
|
|
266
|
+
return true; // Handled
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return false; // Not handled, use default
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
getShortcutHelp: () => [
|
|
273
|
+
{ key: 'Ctrl+1', description: 'Go to first day of month' },
|
|
274
|
+
{ key: 'Ctrl+L', description: 'Go to last day of month' }
|
|
275
|
+
]
|
|
276
|
+
};
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### 4. **Formatting Plugins** (`DateFormatHook`)
|
|
280
|
+
|
|
281
|
+
Customize date formatting:
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
interface DateFormatHook {
|
|
285
|
+
formatDisplayValue?(value: DatepickerValue, mode: string): string;
|
|
286
|
+
formatAriaLabel?(date: Date): string;
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**Use Cases:**
|
|
291
|
+
- Custom date display formats
|
|
292
|
+
- Localized formatting
|
|
293
|
+
- Accessibility labels
|
|
294
|
+
- Brand-specific formatting
|
|
295
|
+
|
|
296
|
+
**Example:**
|
|
297
|
+
```typescript
|
|
298
|
+
const formattingPlugin: DatepickerHooks = {
|
|
299
|
+
formatDisplayValue: (value, mode) => {
|
|
300
|
+
if (mode === 'single' && value instanceof Date) {
|
|
301
|
+
// Custom format: "Monday, January 15, 2024"
|
|
302
|
+
return value.toLocaleDateString('en-US', {
|
|
303
|
+
weekday: 'long',
|
|
304
|
+
year: 'numeric',
|
|
305
|
+
month: 'long',
|
|
306
|
+
day: 'numeric'
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (mode === 'range' && typeof value === 'object' && 'start' in value) {
|
|
311
|
+
const range = value as { start: Date; end: Date };
|
|
312
|
+
return `${range.start.toLocaleDateString()} → ${range.end.toLocaleDateString()}`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return ''; // Use default
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
formatAriaLabel: (date) => {
|
|
319
|
+
return `Select ${date.toLocaleDateString('en-US', {
|
|
320
|
+
weekday: 'long',
|
|
321
|
+
month: 'long',
|
|
322
|
+
day: 'numeric',
|
|
323
|
+
year: 'numeric'
|
|
324
|
+
})}`;
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### 5. **Event Plugins** (`EventHook`)
|
|
330
|
+
|
|
331
|
+
React to component lifecycle events:
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
interface EventHook {
|
|
335
|
+
beforeDateSelect?(date: Date, currentValue: DatepickerValue): boolean;
|
|
336
|
+
afterDateSelect?(date: Date, newValue: DatepickerValue): void;
|
|
337
|
+
onCalendarOpen?(): void;
|
|
338
|
+
onCalendarClose?(): void;
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
**Use Cases:**
|
|
343
|
+
- Analytics tracking
|
|
344
|
+
- Side effects (API calls, logging)
|
|
345
|
+
- Pre-selection validation
|
|
346
|
+
- Post-selection actions
|
|
347
|
+
|
|
348
|
+
**Example:**
|
|
349
|
+
```typescript
|
|
350
|
+
const eventPlugin: DatepickerHooks = {
|
|
351
|
+
beforeDateSelect: (date, currentValue) => {
|
|
352
|
+
// Log selection attempt
|
|
353
|
+
console.log('Attempting to select:', date);
|
|
354
|
+
|
|
355
|
+
// Prevent selection on specific dates
|
|
356
|
+
if (date.getDate() === 13) {
|
|
357
|
+
return false; // Prevent selection
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return true; // Allow selection
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
afterDateSelect: (date, newValue) => {
|
|
364
|
+
// Track analytics
|
|
365
|
+
analytics.track('date_selected', {
|
|
366
|
+
date: date.toISOString(),
|
|
367
|
+
mode: 'single'
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Perform side effects
|
|
371
|
+
this.updateRelatedComponents(newValue);
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
onCalendarOpen: () => {
|
|
375
|
+
console.log('Calendar opened');
|
|
376
|
+
analytics.track('calendar_opened');
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
onCalendarClose: () => {
|
|
380
|
+
console.log('Calendar closed');
|
|
381
|
+
analytics.track('calendar_closed');
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## Creating Plugins
|
|
387
|
+
|
|
388
|
+
### Basic Plugin Structure
|
|
389
|
+
|
|
390
|
+
A plugin is simply an object that implements one or more hook interfaces:
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
import { DatepickerHooks } from 'ngxsmk-datepicker';
|
|
394
|
+
|
|
395
|
+
// Create a plugin
|
|
396
|
+
const myPlugin: DatepickerHooks = {
|
|
397
|
+
// Implement any hooks you need
|
|
398
|
+
validateDate: (date) => {
|
|
399
|
+
// Your logic here
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// Use the plugin
|
|
405
|
+
@Component({
|
|
406
|
+
template: `
|
|
407
|
+
<ngxsmk-datepicker
|
|
408
|
+
[hooks]="myPlugin"
|
|
409
|
+
mode="single">
|
|
410
|
+
</ngxsmk-datepicker>
|
|
411
|
+
`
|
|
412
|
+
})
|
|
413
|
+
export class MyComponent {
|
|
414
|
+
myPlugin = myPlugin;
|
|
415
|
+
}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### Reusable Plugin Classes
|
|
419
|
+
|
|
420
|
+
Create reusable plugin classes for better organization:
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
// plugins/weekend-blocker.plugin.ts
|
|
424
|
+
import { DatepickerHooks } from 'ngxsmk-datepicker';
|
|
425
|
+
|
|
426
|
+
export class WeekendBlockerPlugin implements DatepickerHooks {
|
|
427
|
+
validateDate(date: Date): boolean {
|
|
428
|
+
const day = date.getDay();
|
|
429
|
+
return day !== 0 && day !== 6; // Block weekends
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
getDayCellClasses(date: Date, isSelected: boolean, isDisabled: boolean, isToday: boolean, isHoliday: boolean): string[] {
|
|
433
|
+
const day = date.getDay();
|
|
434
|
+
if (day === 0 || day === 6) {
|
|
435
|
+
return ['weekend-disabled'];
|
|
436
|
+
}
|
|
437
|
+
return [];
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Usage
|
|
442
|
+
@Component({
|
|
443
|
+
template: `
|
|
444
|
+
<ngxsmk-datepicker
|
|
445
|
+
[hooks]="weekendPlugin"
|
|
446
|
+
mode="single">
|
|
447
|
+
</ngxsmk-datepicker>
|
|
448
|
+
`
|
|
449
|
+
})
|
|
450
|
+
export class MyComponent {
|
|
451
|
+
weekendPlugin = new WeekendBlockerPlugin();
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Plugin Factories
|
|
456
|
+
|
|
457
|
+
Create plugins with configuration:
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
// plugins/business-days.plugin.ts
|
|
461
|
+
import { DatepickerHooks } from 'ngxsmk-datepicker';
|
|
462
|
+
|
|
463
|
+
export interface BusinessDaysConfig {
|
|
464
|
+
allowedDays: number[]; // 0 = Sunday, 1 = Monday, etc.
|
|
465
|
+
customMessage?: string;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function createBusinessDaysPlugin(config: BusinessDaysConfig): DatepickerHooks {
|
|
469
|
+
return {
|
|
470
|
+
validateDate: (date: Date) => {
|
|
471
|
+
const day = date.getDay();
|
|
472
|
+
return config.allowedDays.includes(day);
|
|
473
|
+
},
|
|
474
|
+
|
|
475
|
+
getValidationError: (date: Date) => {
|
|
476
|
+
if (!config.allowedDays.includes(date.getDay())) {
|
|
477
|
+
return config.customMessage || 'Only business days are allowed';
|
|
478
|
+
}
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Usage
|
|
485
|
+
@Component({
|
|
486
|
+
template: `
|
|
487
|
+
<ngxsmk-datepicker
|
|
488
|
+
[hooks]="businessDaysPlugin"
|
|
489
|
+
mode="single">
|
|
490
|
+
</ngxsmk-datepicker>
|
|
491
|
+
`
|
|
492
|
+
})
|
|
493
|
+
export class MyComponent {
|
|
494
|
+
businessDaysPlugin = createBusinessDaysPlugin({
|
|
495
|
+
allowedDays: [1, 2, 3, 4, 5], // Monday to Friday
|
|
496
|
+
customMessage: 'Only weekdays are selectable'
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### Composing Multiple Plugins
|
|
502
|
+
|
|
503
|
+
Combine multiple plugins:
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
// Combine plugins using object spread
|
|
507
|
+
const combinedPlugin: DatepickerHooks = {
|
|
508
|
+
...weekendBlockerPlugin,
|
|
509
|
+
...businessDaysPlugin,
|
|
510
|
+
...customStylingPlugin
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// Or use a utility function
|
|
514
|
+
function combinePlugins(...plugins: DatepickerHooks[]): DatepickerHooks {
|
|
515
|
+
return Object.assign({}, ...plugins);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const combined = combinePlugins(
|
|
519
|
+
weekendBlockerPlugin,
|
|
520
|
+
businessDaysPlugin,
|
|
521
|
+
customStylingPlugin
|
|
522
|
+
);
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
## Plugin Lifecycle
|
|
526
|
+
|
|
527
|
+
Understanding when hooks are called helps you write effective plugins:
|
|
528
|
+
|
|
529
|
+
### 1. **Initialization Phase**
|
|
530
|
+
|
|
531
|
+
```
|
|
532
|
+
Component Init → Calendar Generation → Hook: getDayCellClasses (for each date)
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### 2. **User Interaction Phase**
|
|
536
|
+
|
|
537
|
+
```
|
|
538
|
+
User Clicks Date
|
|
539
|
+
↓
|
|
540
|
+
Hook: beforeDateSelect (can prevent selection)
|
|
541
|
+
↓
|
|
542
|
+
Hook: validateDate (can prevent selection)
|
|
543
|
+
↓
|
|
544
|
+
Date Selected (if allowed)
|
|
545
|
+
↓
|
|
546
|
+
Hook: afterDateSelect
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### 3. **Rendering Phase**
|
|
550
|
+
|
|
551
|
+
```
|
|
552
|
+
Calendar Render
|
|
553
|
+
↓
|
|
554
|
+
For each date:
|
|
555
|
+
- Hook: getDayCellClasses
|
|
556
|
+
- Hook: getDayCellTooltip
|
|
557
|
+
- Hook: formatDayNumber
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
### 4. **Keyboard Interaction Phase**
|
|
561
|
+
|
|
562
|
+
```
|
|
563
|
+
Key Press
|
|
564
|
+
↓
|
|
565
|
+
Hook: handleShortcut (can handle custom shortcuts)
|
|
566
|
+
↓
|
|
567
|
+
Default Shortcut Handling (if not handled)
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### 5. **Formatting Phase**
|
|
571
|
+
|
|
572
|
+
```
|
|
573
|
+
Value Change
|
|
574
|
+
↓
|
|
575
|
+
Hook: formatDisplayValue (for input display)
|
|
576
|
+
↓
|
|
577
|
+
Display Updated
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
## Advanced Patterns
|
|
581
|
+
|
|
582
|
+
### 1. **Plugin with State**
|
|
583
|
+
|
|
584
|
+
Plugins can maintain their own state:
|
|
585
|
+
|
|
586
|
+
```typescript
|
|
587
|
+
export class StatefulPlugin implements DatepickerHooks {
|
|
588
|
+
private selectedDates: Date[] = [];
|
|
589
|
+
|
|
590
|
+
validateDate(date: Date, currentValue: DatepickerValue): boolean {
|
|
591
|
+
// Limit to 5 selections
|
|
592
|
+
if (Array.isArray(currentValue)) {
|
|
593
|
+
return currentValue.length < 5;
|
|
594
|
+
}
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
afterDateSelect(date: Date, newValue: DatepickerValue): void {
|
|
599
|
+
// Track selections
|
|
600
|
+
if (Array.isArray(newValue)) {
|
|
601
|
+
this.selectedDates = newValue;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
### 2. **Plugin with Dependencies**
|
|
608
|
+
|
|
609
|
+
Plugins can depend on services:
|
|
610
|
+
|
|
611
|
+
```typescript
|
|
612
|
+
import { Injectable } from '@angular/core';
|
|
613
|
+
import { DatepickerHooks } from 'ngxsmk-datepicker';
|
|
614
|
+
import { AnalyticsService } from './analytics.service';
|
|
615
|
+
|
|
616
|
+
@Injectable()
|
|
617
|
+
export class AnalyticsPlugin implements DatepickerHooks {
|
|
618
|
+
constructor(private analytics: AnalyticsService) {}
|
|
619
|
+
|
|
620
|
+
afterDateSelect(date: Date, newValue: DatepickerValue): void {
|
|
621
|
+
this.analytics.track('date_selected', {
|
|
622
|
+
date: date.toISOString(),
|
|
623
|
+
value: newValue
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
onCalendarOpen(): void {
|
|
628
|
+
this.analytics.track('calendar_opened');
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
onCalendarClose(): void {
|
|
632
|
+
this.analytics.track('calendar_closed');
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
### 3. **Conditional Plugin Application**
|
|
638
|
+
|
|
639
|
+
Apply plugins conditionally:
|
|
640
|
+
|
|
641
|
+
```typescript
|
|
642
|
+
@Component({
|
|
643
|
+
template: `
|
|
644
|
+
<ngxsmk-datepicker
|
|
645
|
+
[hooks]="activePlugins"
|
|
646
|
+
mode="single">
|
|
647
|
+
</ngxsmk-datepicker>
|
|
648
|
+
`
|
|
649
|
+
})
|
|
650
|
+
export class MyComponent {
|
|
651
|
+
enableWeekendBlocking = true;
|
|
652
|
+
enableAnalytics = false;
|
|
653
|
+
|
|
654
|
+
get activePlugins(): DatepickerHooks {
|
|
655
|
+
const plugins: DatepickerHooks = {};
|
|
656
|
+
|
|
657
|
+
if (this.enableWeekendBlocking) {
|
|
658
|
+
Object.assign(plugins, weekendBlockerPlugin);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (this.enableAnalytics) {
|
|
662
|
+
Object.assign(plugins, analyticsPlugin);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return plugins;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
### 4. **Plugin Middleware Pattern**
|
|
671
|
+
|
|
672
|
+
Create middleware-style plugins:
|
|
673
|
+
|
|
674
|
+
```typescript
|
|
675
|
+
function createValidationMiddleware(
|
|
676
|
+
...validators: Array<(date: Date) => boolean>
|
|
677
|
+
): DatepickerHooks {
|
|
678
|
+
return {
|
|
679
|
+
validateDate: (date: Date) => {
|
|
680
|
+
// All validators must pass
|
|
681
|
+
return validators.every(validator => validator(date));
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Usage
|
|
687
|
+
const validationPlugin = createValidationMiddleware(
|
|
688
|
+
(date) => date.getDay() !== 0, // Not Sunday
|
|
689
|
+
(date) => date.getDay() !== 6, // Not Saturday
|
|
690
|
+
(date) => date >= new Date() // Not in past
|
|
691
|
+
);
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
## Best Practices
|
|
695
|
+
|
|
696
|
+
### 1. **Keep Plugins Focused**
|
|
697
|
+
|
|
698
|
+
Each plugin should have a single responsibility:
|
|
699
|
+
|
|
700
|
+
```typescript
|
|
701
|
+
// ✅ Good: Focused plugin
|
|
702
|
+
const weekendBlocker: DatepickerHooks = {
|
|
703
|
+
validateDate: (date) => date.getDay() !== 0 && date.getDay() !== 6
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
// ❌ Bad: Too many responsibilities
|
|
707
|
+
const megaPlugin: DatepickerHooks = {
|
|
708
|
+
validateDate: (date) => { /* ... */ },
|
|
709
|
+
getDayCellClasses: (date) => { /* ... */ },
|
|
710
|
+
formatDisplayValue: (value) => { /* ... */ },
|
|
711
|
+
handleShortcut: (event) => { /* ... */ },
|
|
712
|
+
// ... too many things
|
|
713
|
+
};
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
### 2. **Make Plugins Reusable**
|
|
717
|
+
|
|
718
|
+
Design plugins to be reusable across projects:
|
|
719
|
+
|
|
720
|
+
```typescript
|
|
721
|
+
// ✅ Good: Configurable and reusable
|
|
722
|
+
export function createBusinessDaysPlugin(config: BusinessDaysConfig): DatepickerHooks {
|
|
723
|
+
return {
|
|
724
|
+
validateDate: (date) => config.allowedDays.includes(date.getDay())
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// ❌ Bad: Hard-coded and not reusable
|
|
729
|
+
const myBusinessDays: DatepickerHooks = {
|
|
730
|
+
validateDate: (date) => [1, 2, 3, 4, 5].includes(date.getDay())
|
|
731
|
+
};
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
### 3. **Optimize Performance**
|
|
735
|
+
|
|
736
|
+
Keep hook functions lightweight:
|
|
737
|
+
|
|
738
|
+
```typescript
|
|
739
|
+
// ✅ Good: Lightweight and memoized
|
|
740
|
+
const memoizedValidation = new Map<string, boolean>();
|
|
741
|
+
|
|
742
|
+
const optimizedPlugin: DatepickerHooks = {
|
|
743
|
+
validateDate: (date) => {
|
|
744
|
+
const key = date.toISOString().split('T')[0];
|
|
745
|
+
if (memoizedValidation.has(key)) {
|
|
746
|
+
return memoizedValidation.get(key)!;
|
|
747
|
+
}
|
|
748
|
+
const result = /* expensive validation */;
|
|
749
|
+
memoizedValidation.set(key, result);
|
|
750
|
+
return result;
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
// ❌ Bad: Expensive operation on every call
|
|
755
|
+
const slowPlugin: DatepickerHooks = {
|
|
756
|
+
validateDate: (date) => {
|
|
757
|
+
// Expensive API call on every validation
|
|
758
|
+
return this.apiService.checkDate(date).toPromise();
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
### 4. **Provide Clear Error Messages**
|
|
764
|
+
|
|
765
|
+
Use `getValidationError` for user-friendly messages:
|
|
766
|
+
|
|
767
|
+
```typescript
|
|
768
|
+
// ✅ Good: Clear error messages
|
|
769
|
+
const userFriendlyPlugin: DatepickerHooks = {
|
|
770
|
+
validateDate: (date) => {
|
|
771
|
+
const today = new Date();
|
|
772
|
+
today.setHours(0, 0, 0, 0);
|
|
773
|
+
return date >= today;
|
|
774
|
+
},
|
|
775
|
+
|
|
776
|
+
getValidationError: (date) => {
|
|
777
|
+
const today = new Date();
|
|
778
|
+
today.setHours(0, 0, 0, 0);
|
|
779
|
+
if (date < today) {
|
|
780
|
+
return 'Please select a date from today onwards';
|
|
781
|
+
}
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
### 5. **Test Your Plugins**
|
|
788
|
+
|
|
789
|
+
Write tests for your plugins:
|
|
790
|
+
|
|
791
|
+
```typescript
|
|
792
|
+
describe('WeekendBlockerPlugin', () => {
|
|
793
|
+
it('should block weekends', () => {
|
|
794
|
+
const plugin = new WeekendBlockerPlugin();
|
|
795
|
+
const saturday = new Date(2024, 0, 6); // Saturday
|
|
796
|
+
const monday = new Date(2024, 0, 8); // Monday
|
|
797
|
+
|
|
798
|
+
expect(plugin.validateDate?.(saturday)).toBe(false);
|
|
799
|
+
expect(plugin.validateDate?.(monday)).toBe(true);
|
|
800
|
+
});
|
|
801
|
+
});
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
## Complete Example
|
|
805
|
+
|
|
806
|
+
Here's a complete example of a production-ready plugin:
|
|
807
|
+
|
|
808
|
+
```typescript
|
|
809
|
+
// plugins/business-calendar.plugin.ts
|
|
810
|
+
import { DatepickerHooks, DatepickerValue } from 'ngxsmk-datepicker';
|
|
811
|
+
|
|
812
|
+
export interface BusinessCalendarConfig {
|
|
813
|
+
businessDays: number[]; // [1,2,3,4,5] for Mon-Fri
|
|
814
|
+
holidays: Date[]; // Array of holiday dates
|
|
815
|
+
minAdvanceDays: number; // Minimum days in advance
|
|
816
|
+
maxAdvanceDays: number; // Maximum days in advance
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
export function createBusinessCalendarPlugin(
|
|
820
|
+
config: BusinessCalendarConfig
|
|
821
|
+
): DatepickerHooks {
|
|
822
|
+
const today = new Date();
|
|
823
|
+
today.setHours(0, 0, 0, 0);
|
|
824
|
+
|
|
825
|
+
const minDate = new Date(today);
|
|
826
|
+
minDate.setDate(today.getDate() + config.minAdvanceDays);
|
|
827
|
+
|
|
828
|
+
const maxDate = new Date(today);
|
|
829
|
+
maxDate.setDate(today.getDate() + config.maxAdvanceDays);
|
|
830
|
+
|
|
831
|
+
const isHoliday = (date: Date): boolean => {
|
|
832
|
+
return config.holidays.some(holiday => {
|
|
833
|
+
return holiday.toDateString() === date.toDateString();
|
|
834
|
+
});
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
const isBusinessDay = (date: Date): boolean => {
|
|
838
|
+
const day = date.getDay();
|
|
839
|
+
return config.businessDays.includes(day) && !isHoliday(date);
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
return {
|
|
843
|
+
validateDate: (date: Date) => {
|
|
844
|
+
// Check if within date range
|
|
845
|
+
if (date < minDate || date > maxDate) {
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Check if business day
|
|
850
|
+
return isBusinessDay(date);
|
|
851
|
+
},
|
|
852
|
+
|
|
853
|
+
getValidationError: (date: Date) => {
|
|
854
|
+
if (date < minDate) {
|
|
855
|
+
return `Please select a date at least ${config.minAdvanceDays} days in advance`;
|
|
856
|
+
}
|
|
857
|
+
if (date > maxDate) {
|
|
858
|
+
return `Please select a date within ${config.maxAdvanceDays} days`;
|
|
859
|
+
}
|
|
860
|
+
if (!isBusinessDay(date)) {
|
|
861
|
+
return 'Please select a business day';
|
|
862
|
+
}
|
|
863
|
+
return null;
|
|
864
|
+
},
|
|
865
|
+
|
|
866
|
+
getDayCellClasses: (date, isSelected, isDisabled, isToday, isHoliday) => {
|
|
867
|
+
const classes: string[] = [];
|
|
868
|
+
|
|
869
|
+
if (!isBusinessDay(date)) {
|
|
870
|
+
classes.push('non-business-day');
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (isHoliday(date)) {
|
|
874
|
+
classes.push('holiday');
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
return classes;
|
|
878
|
+
},
|
|
879
|
+
|
|
880
|
+
getDayCellTooltip: (date, holidayLabel) => {
|
|
881
|
+
if (isHoliday(date)) {
|
|
882
|
+
return holidayLabel || 'Holiday';
|
|
883
|
+
}
|
|
884
|
+
if (!isBusinessDay(date)) {
|
|
885
|
+
return 'Not a business day';
|
|
886
|
+
}
|
|
887
|
+
return null;
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Usage
|
|
893
|
+
@Component({
|
|
894
|
+
template: `
|
|
895
|
+
<ngxsmk-datepicker
|
|
896
|
+
[hooks]="businessCalendarPlugin"
|
|
897
|
+
mode="single"
|
|
898
|
+
placeholder="Select a business day">
|
|
899
|
+
</ngxsmk-datepicker>
|
|
900
|
+
`
|
|
901
|
+
})
|
|
902
|
+
export class BookingComponent {
|
|
903
|
+
businessCalendarPlugin = createBusinessCalendarPlugin({
|
|
904
|
+
businessDays: [1, 2, 3, 4, 5], // Monday to Friday
|
|
905
|
+
holidays: [
|
|
906
|
+
new Date(2024, 0, 1), // New Year's Day
|
|
907
|
+
new Date(2024, 6, 4), // Independence Day
|
|
908
|
+
new Date(2024, 11, 25) // Christmas
|
|
909
|
+
],
|
|
910
|
+
minAdvanceDays: 1,
|
|
911
|
+
maxAdvanceDays: 90
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
## Summary
|
|
917
|
+
|
|
918
|
+
The plugin architecture in ngxsmk-datepicker provides:
|
|
919
|
+
|
|
920
|
+
- ✅ **Non-invasive extension** - Extend without modifying core code
|
|
921
|
+
- ✅ **Type safety** - Full TypeScript support
|
|
922
|
+
- ✅ **Composability** - Combine multiple plugins
|
|
923
|
+
- ✅ **Flexibility** - Optional and configurable
|
|
924
|
+
- ✅ **Performance** - Lightweight hook system
|
|
925
|
+
- ✅ **Maintainability** - Clear separation of concerns
|
|
926
|
+
|
|
927
|
+
For more examples and detailed hook documentation, see [Extension Points Guide](./extension-points.md).
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
|