ngxsmk-datepicker 2.3.1 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,930 +1,930 @@
1
- # Plugin Architecture
2
-
3
- **Last updated:** June 3, 2026 - **Current stable:** v2.3.1
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
+