ngx-edge-slider 2.2.2 → 2.2.5

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,653 +1,655 @@
1
- # WingmanColt Angular Slider
2
-
3
- A modern, fully reactive, plugin‑driven **Angular slider / carousel library** built for **Angular 18+**.
4
-
5
-
6
- Video :
7
- https://youtu.be/tGZ5qX70KU0
8
-
9
- Main Slider (3 rendere sliders on homepage):
10
- https://obscene.me/
11
-
12
- Main Slider + Thumb Slider :
13
- https://obscene.me/shop/product/the-devil
14
-
15
- WingmanColt is designed for **production‑grade UI systems** where flexibility, performance, and clean architecture matter. It supports **dragging**, **pagination**, **navigation**, **autoplay**, **responsive breakpoints**, and **synced thumbnail sliders**, all powered by a small, predictable core engine.
16
-
17
- ---
18
-
19
- ## ✨ Features
20
-
21
- - Angular **18+** compatible (Standalone components)
22
- - ⚡ **RxJS‑driven state** (predictable & debuggable)
23
- - 🧩 **Plugin architecture** (enable only what you need)
24
- - 🖱️ Pointerbased dragging (mouse + touch)
25
- - 📱 Responsive breakpoints (container‑aware)
26
- - 🧭 Navigation arrows
27
- - 🔘 Pagination (dots)
28
- - ▶️ Autoplay (configurable)
29
- - 🖼️ **Main + Thumbs slider syncing**
30
- - 📐 Vertical & horizontal modes
31
- - 🎯 Click‑to‑select slides
32
- - ♻️ Safe re‑initialization on data changes
33
-
34
- ---
35
-
36
- ## 📦 Installation
37
-
38
- ```bash
39
- npm install ngx-edge-slider
40
- ```
41
-
42
- > Angular **18 or newer** is required.
43
-
44
- ---
45
-
46
- ## 🚀 Quick Start
47
-
48
- ### 1️⃣ Import the module
49
-
50
- ```ts
51
- import { NgxEdgeSliderModule } from "ngx-edge-slider";
52
-
53
- @Component({
54
- standalone: true,
55
- imports: [NgxEdgeSliderModule],
56
- })
57
- export class AppComponent {}
58
- ```
59
-
60
- Or use as standalone
61
- ```ts
62
- @Component({
63
- selector: 'app-root',
64
- imports: [CommonModule, NgxEdgeSliderModule],
65
- templateUrl: './app.html',
66
- styleUrl: './app.scss',
67
- encapsulation: ViewEncapsulation.None,
68
- })
69
- ```
70
-
71
-
72
- ---
73
- ### Import Style
74
-
75
- ```css
76
- @import 'ngx-edge-slider/assets/ngx-simple-slider.scss';
77
- OR
78
- @import 'ngx-edge-slider/assets/ngx-simple-slider.css';
79
-
80
- OR USE CDN
81
-
82
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/WingmanColt/ngx-edge-slider@master/projects/ngx-edge-slider/assets/ngx-simple-slider.scss"/>
83
-
84
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/WingmanColt/ngx-edge-slider@master/projects/ngx-edge-slider/assets/ngx-simple-slider.css"/>
85
-
86
- ```
87
-
88
-
89
- ---
90
-
91
- ### 2️⃣ Basic Slider Usage
92
-
93
- ```html
94
- <app-simple-slider #MainSlider [config]="sliderConfig" [slideTemplate]="mainSlideTemplate" (slideChange)="onSlideChange($event)"></app-simple-slider>
95
-
96
- <ng-template #mainSlideTemplate let-slide="slide" let-index="index">
97
- <div class="slide-content">
98
- <img [src]="slide.image" alt="Slide {{ index + 1 }}" />
99
- </div>
100
- </ng-template>
101
- ```
102
-
103
- ```ts
104
- sliderConfig: SliderConfig = {
105
- slides: this.slides,
106
- slidesPerView: 1,
107
- plugins: {
108
- draggable: true,
109
- pagination: true,
110
- navigation: true,
111
- },
112
- };
113
- ```
114
-
115
- ---
116
-
117
- ## 🧱 Slide Templates
118
-
119
- WingmanColt is **template‑driven**. You fully control slide markup.
120
-
121
- ```html
122
- <app-simple-slider [config]="sliderConfig" [slideTemplate]="slideTpl"></app-simple-slider>
123
-
124
- <ng-template #slideTpl let-slide let-index="index">
125
- <img [src]="slide.image" />
126
- </ng-template>
127
- ```
128
-
129
- ---
130
-
131
- ## Full Component with MainSlider, ThumbSlider Pagination, Navigation
132
-
133
-
134
- ```html
135
- <div style="padding: 100px">
136
- <ng-container *ngIf="this.slides.length">
137
- <div class="slider-container" style="position: relative">
138
- <app-simple-slider
139
- #MainSlider
140
- [config]="sliderConfig"
141
- class="slider-main-product"
142
- [slideTemplate]="mainSlideTemplate"
143
- (slideChange)="onSlideChangeMain($event)"
144
- >
145
- </app-simple-slider>
146
-
147
- <ng-template #mainSlideTemplate let-slide="slide" let-index="index">
148
- <div class="slide-content">
149
- <img [src]="slide.image" alt="Slide {{ index + 1 }}" />
150
- </div>
151
- </ng-template>
152
-
153
- <!-- NAVIGATION -->
154
- <div class="slider-nav" [ngClass]="'nav--' + navPosition">
155
- <button
156
- type="button"
157
- class="nav-btn nav-btn--prev"
158
- [class.is-hidden]="!(canPrev$ | async)"
159
- (click)="onPrevClick($event)"
160
- aria-label="Previous"
161
- >
162
- <
163
- </button>
164
-
165
- <button
166
- type="button"
167
- class="nav-btn nav-btn--next"
168
- [class.is-hidden]="!(canNext$ | async)"
169
- (click)="onNextClick($event)"
170
- aria-label="Next"
171
- >
172
- >
173
- </button>
174
- </div>
175
- <ng-container *ngIf="pager$ | async as pager">
176
- <div class="slider-pagination" *ngIf="pager">
177
- <div class="thumb-dots-wrapper">
178
- <div
179
- *ngFor="let slideIndex of pager.visibleDots; let i = index"
180
- (click)="goToSlide(slideIndex)"
181
- >
182
- <span
183
- class="thumb-dot"
184
- [class.active]="slideIndex === pager.visibleDots[pager.activeDotIndex]"
185
- [class.inactive]="slideIndex !== pager.visibleDots[pager.activeDotIndex]"
186
- ></span>
187
- </div>
188
- </div>
189
- </div>
190
- </ng-container>
191
- </div>
192
- </ng-container>
193
-
194
- <!-- Thumbs Slider -->
195
- <ng-container *ngIf="sliderConfigThumbs?.slides?.length">
196
- <div class="thumbs-wrapper">
197
- <div
198
- class="thumb-nav thumb-nav--left"
199
- [class.activeArrow]="(thumbsState$ | async)?.canPrev"
200
- (click)="ThumbsSlider?.prev()"
201
- ></div>
202
- <app-simple-slider
203
- #ThumbsSlider
204
- [config]="sliderConfigThumbs"
205
- [slideTemplate]="ThumbsSlideTemplate"
206
- ></app-simple-slider>
207
-
208
- <ng-template #ThumbsSlideTemplate let-slide="slide" let-index="index">
209
- <div
210
- class="slide-content"
211
- [class.slide--current]="(thumbsState$ | async)?.selectedSlide === index"
212
- (click)="onThumbClick(index)"
213
- >
214
- <img [src]="slide.image" alt="Slide {{ index + 1 }}" />
215
- </div>
216
- </ng-template>
217
-
218
- <div
219
- class="thumb-nav thumb-nav--right"
220
- [class.activeArrow]="(thumbsState$ | async)?.canNext"
221
- (click)="ThumbsSlider?.next()"
222
- ></div>
223
- </div>
224
- </ng-container>
225
- </div>
226
-
227
- ```
228
-
229
- ---
230
-
231
- ---
232
-
233
- ## Full TS File
234
-
235
- ```ts
236
- import { CommonModule } from '@angular/common';
237
- import { Component, OnInit, signal, ViewChild, ViewEncapsulation } from '@angular/core';
238
- import {
239
- NavPosition,
240
- NgxEdgeSliderModule,
241
- Pager,
242
- SimpleSliderComponent,
243
- SliderConfig,
244
- } from 'ngx-edge-slider';
245
- import { Observable, take } from 'rxjs';
246
-
247
- @Component({
248
- selector: 'app-root',
249
- imports: [CommonModule, NgxEdgeSliderModule],
250
- templateUrl: './app.html',
251
- styleUrl: './app.scss',
252
- encapsulation: ViewEncapsulation.None,
253
- })
254
- export class App implements OnInit {
255
- slidesArray = [
256
- {
257
- image: 'https://obscene.me/assets/images/products/27/image-1/image-1.webp',
258
- caption: 'First Slide',
259
- },
260
- {
261
- image: 'https://obscene.me/assets/images/products/27/image-2/image-2.webp',
262
- caption: 'Second Slide',
263
- },
264
- {
265
- image: 'https://obscene.me/assets/images/products/27/image-3/image-3.webp',
266
- caption: 'Third Slide',
267
- },
268
- {
269
- image: 'https://obscene.me/assets/images/products/27/image-4/image-4.webp',
270
- caption: 'Fourth Slide',
271
- },
272
- {
273
- image: 'https://obscene.me/assets/images/products/27/image-3/image-3.webp',
274
- caption: 'Third Slide',
275
- },
276
- {
277
- image: 'https://obscene.me/assets/images/products/27/image-1/image-1.webp',
278
- caption: 'First Slide',
279
- },
280
- {
281
- image: 'https://obscene.me/assets/images/products/27/image-2/image-2.webp',
282
- caption: 'Second Slide',
283
- },
284
- ];
285
-
286
- private isSyncing = false;
287
- slides: any[] = [...this.slidesArray];
288
-
289
- sliderConfig!: SliderConfig;
290
- sliderConfigThumbs!: SliderConfig;
291
-
292
- navPosition: NavPosition = 'top-right'; // change this to switch layouts "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right" | "center-sides";
293
-
294
- @ViewChild('MainSlider') MainSlider!: SimpleSliderComponent;
295
- @ViewChild('ThumbsSlider') ThumbsSlider?: SimpleSliderComponent;
296
-
297
- constructor() {}
298
-
299
- ngOnInit(): void {
300
- this.sliderConfig = {
301
- slides: this.slides,
302
- slidesPerView: 1,
303
- slidesToSlide: 1,
304
- isThumbs: false,
305
- plugins: {
306
- draggable: true,
307
- pagination: true,
308
- navigation: true,
309
- autoplay: undefined,
310
- },
311
- };
312
-
313
- this.sliderConfigThumbs = {
314
- slides: this.slides,
315
- slidesPerView: 4, // desktop default
316
- slidesToSlide: 1,
317
- isThumbs: true,
318
- plugins: {
319
- pagination: true, // enable pagination
320
- navigation: true, // optional
321
- draggable: true, // optional
322
- autoplay: undefined,
323
- },
324
- showOn: { mobile: false, tablet: true, desktop: true },
325
- breakpoints: {
326
- mobile: { slidesPerView: 0 }, // not used because showOn.mobile=false
327
- tablet: { slidesPerView: 4 },
328
- desktop: { slidesPerView: 5 },
329
- },
330
- };
331
- }
332
-
333
- /** Sync sliders */
334
- onSlideChangeMain(index: number) {
335
- this.syncSliders(index);
336
- }
337
- onSlideChangeThumbs(index: number) {
338
- this.syncSliders(index);
339
- }
340
- onThumbClick(index: number) {
341
- this.syncSliders(index);
342
- }
343
-
344
- private syncSliders(index: number) {
345
- if (this.isSyncing) return;
346
- this.isSyncing = true;
347
-
348
- this.MainSlider?.goTo(index);
349
- this.ThumbsSlider?.goTo(index);
350
-
351
- this.isSyncing = false;
352
- }
353
-
354
- // GET SLIDER STATES
355
- get mainState$() {
356
- return this.MainSlider?.state$;
357
- }
358
- get thumbsState$() {
359
- return this.ThumbsSlider?.state$;
360
- }
361
-
362
- // NAVIGATION
363
- get canPrev$(): Observable<boolean> | null {
364
- if (!this.MainSlider || !this.MainSlider.canPrev$) {
365
- console.warn('Navigation plugin is not enabled for this slider.');
366
- return null;
367
- }
368
- return this.MainSlider.canPrev$;
369
- }
370
-
371
- get canNext$(): Observable<boolean> | null {
372
- if (!this.MainSlider || !this.MainSlider.canNext$) {
373
- console.warn('Navigation plugin is not enabled for this slider.');
374
- return null;
375
- }
376
- return this.MainSlider.canNext$;
377
- }
378
-
379
- onPrevClick(event?: MouseEvent) {
380
- event?.stopPropagation();
381
- event?.preventDefault();
382
-
383
- this.MainSlider?.canPrev$?.pipe(take(1)).subscribe((can) => {
384
- if (can) this.MainSlider.prev?.();
385
- });
386
- }
387
-
388
- onNextClick(event?: MouseEvent) {
389
- event?.stopPropagation();
390
- event?.preventDefault();
391
-
392
- this.MainSlider?.canNext$?.pipe(take(1)).subscribe((can) => {
393
- if (can) this.MainSlider.next?.();
394
- });
395
- }
396
-
397
- // PAGINATION
398
- get pager$(): Observable<Pager | null> | null {
399
- if (!this.MainSlider?.pager$) {
400
- console.warn('Pagination plugin is not enabled for this slider.');
401
- return null;
402
- }
403
- return this.MainSlider.pager$;
404
- }
405
-
406
- goToSlide(index: number) {
407
- this.MainSlider?.goToSlide?.(index);
408
- }
409
-
410
- protected readonly title = signal('slider-test');
411
- }
412
-
413
- ```
414
- ----
415
-
416
- ## 🔧 SliderConfig Reference
417
-
418
- ```ts
419
- export interface SliderConfig {
420
- slides: any[];
421
- slidesPerView: number;
422
- slidesToSlide?: number;
423
- loop?: 0 | 1 | 2;
424
- vertical?: boolean;
425
- changeToClickedSlide?: boolean;
426
- isThumbs?: boolean;
427
- gap?: number;
428
-
429
- breakpoints?: {
430
- mobile?: Partial<SliderConfig>;
431
- tablet?: Partial<SliderConfig>;
432
- desktop?: Partial<SliderConfig>;
433
- };
434
-
435
- plugins?: {
436
- draggable?: boolean;
437
- autoplay?: { delay?: number };
438
- navigation?: boolean;
439
- pagination?: boolean;
440
- };
441
-
442
- showOn?: {
443
- mobile?: boolean;
444
- tablet?: boolean;
445
- desktop?: boolean;
446
- };
447
- }
448
- ```
449
-
450
- ---
451
-
452
- ## 🔌 Plugins
453
-
454
- Plugins are **opt‑in**. Only enabled plugins are initialized.
455
-
456
- ### Draggable
457
-
458
- ```ts
459
- plugins: {
460
- draggable: true;
461
- }
462
- ```
463
-
464
- - Mouse + touch dragging
465
- - Pointer capture outside slider bounds
466
-
467
- ---
468
-
469
- ### Navigation
470
-
471
- ```ts
472
- plugins: {
473
- navigation: true;
474
- }
475
- ```
476
-
477
- ```html
478
- <button (click)="slider.prev()">Prev</button>
479
- <button (click)="slider.next()">Next</button>
480
- ```
481
-
482
- Reactive state:
483
-
484
- ```ts
485
- slider.canPrev$;
486
- slider.canNext$;
487
- ```
488
-
489
- ---
490
-
491
- ### Pagination
492
-
493
- ```ts
494
- plugins: {
495
- pagination: true;
496
- }
497
- ```
498
-
499
- ```ts
500
- slider.pager$; // Observable<Pager>
501
- ```
502
-
503
- Pager structure:
504
-
505
- ```ts
506
- interface Pager {
507
- currentPage: number;
508
- totalPages: number;
509
- visibleDots: number[];
510
- activeDotIndex: number;
511
- }
512
- ```
513
-
514
- ---
515
-
516
- ### Autoplay
517
-
518
- ```ts
519
- plugins: {
520
- autoplay: {
521
- delay: 3000;
522
- }
523
- }
524
- ```
525
-
526
- - Automatically pauses during dragging
527
- - Resumes safely
528
-
529
- ---
530
-
531
- ## 🖼️ Thumbnails Slider (Main + Thumbs)
532
-
533
- WingmanColt supports **fully synced sliders**.
534
-
535
- ```html
536
- <app-simple-slider #MainSlider [config]="mainConfig" (slideChange)="onMainChange($event)"></app-simple-slider>
537
-
538
- <app-simple-slider #ThumbsSlider [config]="thumbsConfig"></app-simple-slider>
539
- ```
540
-
541
- ```ts
542
- onMainChange(index: number) {
543
- this.MainSlider.goTo(index);
544
- this.ThumbsSlider.goTo(index);
545
- }
546
- ```
547
-
548
- Thumbs config example:
549
-
550
- ```ts
551
- thumbsConfig = {
552
- slides,
553
- slidesPerView: 5,
554
- isThumbs: true,
555
- plugins: { draggable: true, navigation: true },
556
- breakpoints: {
557
- tablet: { slidesPerView: 4 },
558
- desktop: { slidesPerView: 5 },
559
- },
560
- };
561
- ```
562
-
563
- ---
564
-
565
- ## 📐 Responsive Breakpoints
566
-
567
- Breakpoints are **container‑aware**, not just viewport‑based.
568
-
569
- ```ts
570
- breakpoints: {
571
- mobile: { slidesPerView: 1 },
572
- tablet: { slidesPerView: 2 },
573
- desktop: { slidesPerView: 4 }
574
- }
575
- ```
576
-
577
- Visibility control:
578
-
579
- ```ts
580
- showOn: {
581
- mobile: false,
582
- tablet: true,
583
- desktop: true
584
- }
585
- ```
586
-
587
- ---
588
-
589
- ## 🧠 Architecture Overview
590
-
591
- - **SliderEngine** core logic, movement, breakpoints
592
- - **SliderStore** – RxJS state container
593
- - **Plugins** – isolated feature modules
594
- - **SimpleSliderComponent** – UI wrapper
595
-
596
- This separation allows:
597
-
598
- - Easy feature expansion
599
- - Predictable state transitions
600
- - Minimal DOM coupling
601
-
602
- ---
603
-
604
- ## ♻️ Lifecycle & Reinitialization
605
-
606
- The slider safely re‑initializes when:
607
-
608
- - Slides array reference changes
609
- - Breakpoints change
610
- - Container size changes
611
-
612
- ```ts
613
- this.engine.destroy();
614
- this.engine.init(newConfig);
615
- ```
616
-
617
- ---
618
-
619
- ## 🛠️ Requirements
620
-
621
- - Angular **18+**
622
- - RxJS **7+**
623
- - Browser support for `ResizeObserver`
624
-
625
- ---
626
-
627
- ## 🧪 Status
628
-
629
- WingmanColt is **production‑ready**, actively evolving, and designed for real‑world applications.
630
-
631
- Planned enhancements:
632
-
633
- - ⏩ Loop modes
634
- - 🎞️ Animation presets
635
- - Accessibility helpers
636
- - 🔄 Virtual slides
637
-
638
- ---
639
-
640
- ## 📄 License
641
-
642
- MIT
643
-
644
- ---
645
-
646
- ## 👤 Author
647
-
648
- **WingmanColt**
649
- Angular & Full‑Stack Engineer
650
-
651
- ---
652
-
653
- If you find this library useful, ⭐️ the repository and feel free to contribute.
1
+ # WingmanColt Angular Slider
2
+
3
+ A modern, fully reactive, plugin‑driven **Angular slider / carousel library** built for **Angular 18+**.
4
+
5
+ Main Slider (3 rendere sliders on homepage):
6
+ https://obscene.me/
7
+
8
+ Main Slider + Thumb Slider :
9
+ https://obscene.me/shop/product/the-devil
10
+
11
+
12
+
13
+ Stackblitz:
14
+ https://stackblitz.com/edit/angular-rx83vkhm?file=src%2Fmain.ts
15
+
16
+
17
+ WingmanColt is designed for **production‑grade UI systems** where flexibility, performance, and clean architecture matter. It supports **dragging**, **pagination**, **navigation**, **autoplay**, **responsive breakpoints**, and **synced thumbnail sliders**, all powered by a small, predictable core engine.
18
+
19
+ ---
20
+
21
+ ## Features
22
+
23
+ - Angular **18+** compatible (Standalone components)
24
+ - **RxJSdriven state** (predictable & debuggable)
25
+ - 🧩 **Plugin architecture** (enable only what you need)
26
+ - 🖱️ Pointer‑based dragging (mouse + touch)
27
+ - 📱 Responsive breakpoints (container‑aware)
28
+ - 🧭 Navigation arrows
29
+ - 🔘 Pagination (dots)
30
+ - ▶️ Autoplay (configurable)
31
+ - 🖼️ **Main + Thumbs slider syncing**
32
+ - 📐 Vertical & horizontal modes
33
+ - 🎯 Click‑to‑select slides
34
+ - ♻️ Safe re‑initialization on data changes
35
+
36
+ ---
37
+
38
+ ## 📦 Installation
39
+
40
+ ```bash
41
+ npm install ngx-edge-slider
42
+ ```
43
+
44
+ > Angular **18 or newer** is required.
45
+
46
+ ---
47
+
48
+ ## 🚀 Quick Start
49
+
50
+ ### 1️⃣ Import the module
51
+
52
+ ```ts
53
+ import { NgxEdgeSliderModule } from "ngx-edge-slider";
54
+
55
+ @Component({
56
+ standalone: true,
57
+ imports: [NgxEdgeSliderModule],
58
+ })
59
+ export class AppComponent {}
60
+ ```
61
+
62
+ Or use as standalone
63
+ ```ts
64
+ @Component({
65
+ selector: 'app-root',
66
+ imports: [CommonModule, NgxEdgeSliderModule],
67
+ templateUrl: './app.html',
68
+ styleUrl: './app.scss',
69
+ encapsulation: ViewEncapsulation.None,
70
+ })
71
+ ```
72
+
73
+
74
+ ---
75
+ ### Import Style
76
+
77
+ ```css
78
+ @import 'ngx-edge-slider/assets/ngx-simple-slider.scss';
79
+ OR
80
+ @import 'ngx-edge-slider/assets/ngx-simple-slider.css';
81
+
82
+ OR USE CDN
83
+
84
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/WingmanColt/ngx-edge-slider@master/projects/ngx-edge-slider/assets/ngx-simple-slider.scss"/>
85
+
86
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/WingmanColt/ngx-edge-slider@master/projects/ngx-edge-slider/assets/ngx-simple-slider.css"/>
87
+
88
+ ```
89
+
90
+
91
+ ---
92
+
93
+ ### 2️⃣ Basic Slider Usage
94
+
95
+ ```html
96
+ <app-simple-slider #MainSlider [config]="sliderConfig" [slideTemplate]="mainSlideTemplate" (slideChange)="onSlideChange($event)"></app-simple-slider>
97
+
98
+ <ng-template #mainSlideTemplate let-slide="slide" let-index="index">
99
+ <div class="slide-content">
100
+ <img [src]="slide.image" alt="Slide {{ index + 1 }}" />
101
+ </div>
102
+ </ng-template>
103
+ ```
104
+
105
+ ```ts
106
+ sliderConfig: SliderConfig = {
107
+ slides: this.slides,
108
+ slidesPerView: 1,
109
+ plugins: {
110
+ draggable: true,
111
+ pagination: true,
112
+ navigation: true,
113
+ },
114
+ };
115
+ ```
116
+
117
+ ---
118
+
119
+ ## 🧱 Slider Templates
120
+
121
+ Slider is **template‑driven**. You fully control slide markup.
122
+
123
+ ```html
124
+ <app-simple-slider [config]="sliderConfig" [slideTemplate]="slideTpl"></app-simple-slider>
125
+
126
+ <ng-template #slideTpl let-slide let-index="index">
127
+ <img [src]="slide.image" />
128
+ </ng-template>
129
+ ```
130
+
131
+ ---
132
+
133
+ ## Full Component with MainSlider, ThumbSlider Pagination, Navigation
134
+
135
+
136
+ ```html
137
+
138
+ <ng-container *ngIf="this.slides.length">
139
+ <div class="slider-container" style="position: relative">
140
+ <app-simple-slider
141
+ #MainSlider
142
+ [config]="sliderConfig"
143
+ class="slider-main-product"
144
+ [slideTemplate]="mainSlideTemplate"
145
+ (slideChange)="onSlideChangeMain($event)"
146
+ >
147
+ </app-simple-slider>
148
+
149
+ <ng-template #mainSlideTemplate let-slide="slide" let-index="index">
150
+ <div class="slide-content">
151
+ <img [src]="slide.image" alt="Slide {{ index + 1 }}" />
152
+ </div>
153
+ </ng-template>
154
+
155
+ <!-- NAVIGATION -->
156
+ <div class="slider-nav" [ngClass]="'nav--' + navPosition">
157
+ <button
158
+ type="button"
159
+ class="nav-btn nav-btn--prev"
160
+ [class.is-hidden]="!(canPrev$ | async)"
161
+ (click)="onPrevClick($event)"
162
+ aria-label="Previous"
163
+ >
164
+ <
165
+ </button>
166
+
167
+ <button
168
+ type="button"
169
+ class="nav-btn nav-btn--next"
170
+ [class.is-hidden]="!(canNext$ | async)"
171
+ (click)="onNextClick($event)"
172
+ aria-label="Next"
173
+ >
174
+ >
175
+ </button>
176
+ </div>
177
+ <ng-container *ngIf="pager$ | async as pager">
178
+ <div class="slider-pagination" *ngIf="pager">
179
+ <div class="thumb-dots-wrapper">
180
+ <div
181
+ *ngFor="let slideIndex of pager.visibleDots; let i = index"
182
+ (click)="goToSlide(slideIndex)"
183
+ >
184
+ <span
185
+ class="thumb-dot"
186
+ [class.active]="slideIndex === pager.visibleDots[pager.activeDotIndex]"
187
+ [class.inactive]="slideIndex !== pager.visibleDots[pager.activeDotIndex]"
188
+ ></span>
189
+ </div>
190
+ </div>
191
+ </div>
192
+ </ng-container>
193
+ </div>
194
+ </ng-container>
195
+
196
+ <!-- Thumbs Slider -->
197
+ <ng-container *ngIf="sliderConfigThumbs?.slides?.length">
198
+ <div class="thumbs-wrapper">
199
+ <div
200
+ class="thumb-nav thumb-nav--left"
201
+ [class.activeArrow]="(thumbsState$ | async)?.canPrev"
202
+ (click)="ThumbsSlider?.prev()"
203
+ ></div>
204
+ <app-simple-slider
205
+ #ThumbsSlider
206
+ [config]="sliderConfigThumbs"
207
+ [slideTemplate]="ThumbsSlideTemplate"
208
+ ></app-simple-slider>
209
+
210
+ <ng-template #ThumbsSlideTemplate let-slide="slide" let-index="index">
211
+ <div
212
+ class="slide-content"
213
+ [class.slide--current]="(thumbsState$ | async)?.selectedSlide === index"
214
+ (click)="onThumbClick(index)"
215
+ >
216
+ <img [src]="slide.image" alt="Slide {{ index + 1 }}" />
217
+ </div>
218
+ </ng-template>
219
+
220
+ <div
221
+ class="thumb-nav thumb-nav--right"
222
+ [class.activeArrow]="(thumbsState$ | async)?.canNext"
223
+ (click)="ThumbsSlider?.next()"
224
+ ></div>
225
+ </div>
226
+ </ng-container>
227
+
228
+
229
+ ```
230
+
231
+ ---
232
+
233
+ ---
234
+
235
+ ## Full TS File
236
+
237
+ ```ts
238
+ import { CommonModule } from '@angular/common';
239
+ import { Component, OnInit, signal, ViewChild, ViewEncapsulation } from '@angular/core';
240
+ import {
241
+ NavPosition,
242
+ NgxEdgeSliderModule,
243
+ Pager,
244
+ SimpleSliderComponent,
245
+ SliderConfig,
246
+ } from 'ngx-edge-slider';
247
+ import { Observable, take } from 'rxjs';
248
+
249
+ @Component({
250
+ selector: 'app-root',
251
+ imports: [CommonModule, NgxEdgeSliderModule],
252
+ templateUrl: './app.html',
253
+ styleUrl: './app.scss',
254
+ encapsulation: ViewEncapsulation.None,
255
+ })
256
+ export class App implements OnInit {
257
+ slidesArray = [
258
+ {
259
+ image: 'https://obscene.me/assets/images/products/27/image-1/image-1.webp',
260
+ caption: 'First Slide',
261
+ },
262
+ {
263
+ image: 'https://obscene.me/assets/images/products/27/image-2/image-2.webp',
264
+ caption: 'Second Slide',
265
+ },
266
+ {
267
+ image: 'https://obscene.me/assets/images/products/27/image-3/image-3.webp',
268
+ caption: 'Third Slide',
269
+ },
270
+ {
271
+ image: 'https://obscene.me/assets/images/products/27/image-4/image-4.webp',
272
+ caption: 'Fourth Slide',
273
+ },
274
+ {
275
+ image: 'https://obscene.me/assets/images/products/27/image-3/image-3.webp',
276
+ caption: 'Third Slide',
277
+ },
278
+ {
279
+ image: 'https://obscene.me/assets/images/products/27/image-1/image-1.webp',
280
+ caption: 'First Slide',
281
+ },
282
+ {
283
+ image: 'https://obscene.me/assets/images/products/27/image-2/image-2.webp',
284
+ caption: 'Second Slide',
285
+ },
286
+ ];
287
+
288
+ private isSyncing = false;
289
+ slides: any[] = [...this.slidesArray];
290
+
291
+ sliderConfig!: SliderConfig;
292
+ sliderConfigThumbs!: SliderConfig;
293
+
294
+ navPosition: NavPosition = 'top-right'; // change this to switch layouts "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right" | "center-sides";
295
+
296
+ @ViewChild('MainSlider') MainSlider!: SimpleSliderComponent;
297
+ @ViewChild('ThumbsSlider') ThumbsSlider?: SimpleSliderComponent;
298
+
299
+ constructor() {}
300
+
301
+ ngOnInit(): void {
302
+ this.sliderConfig = {
303
+ slides: this.slides,
304
+ slidesPerView: 1,
305
+ slidesToSlide: 1,
306
+ isThumbs: false,
307
+ plugins: {
308
+ draggable: true,
309
+ pagination: true,
310
+ navigation: true,
311
+ autoplay: undefined,
312
+ },
313
+ };
314
+
315
+ this.sliderConfigThumbs = {
316
+ slides: this.slides,
317
+ slidesPerView: 4, // desktop default
318
+ slidesToSlide: 1,
319
+ isThumbs: true,
320
+ plugins: {
321
+ pagination: true, // enable pagination
322
+ navigation: true, // optional
323
+ draggable: true, // optional
324
+ autoplay: undefined,
325
+ },
326
+ showOn: { mobile: false, tablet: true, desktop: true },
327
+ breakpoints: {
328
+ mobile: { slidesPerView: 0 }, // not used because showOn.mobile=false
329
+ tablet: { slidesPerView: 4 },
330
+ desktop: { slidesPerView: 5 },
331
+ },
332
+ };
333
+ }
334
+
335
+ /** Sync sliders */
336
+ onSlideChangeMain(index: number) {
337
+ this.syncSliders(index);
338
+ }
339
+ onSlideChangeThumbs(index: number) {
340
+ this.syncSliders(index);
341
+ }
342
+ onThumbClick(index: number) {
343
+ this.syncSliders(index);
344
+ }
345
+
346
+ private syncSliders(index: number) {
347
+ if (this.isSyncing) return;
348
+ this.isSyncing = true;
349
+
350
+ this.MainSlider?.goTo(index);
351
+ this.ThumbsSlider?.goTo(index);
352
+
353
+ this.isSyncing = false;
354
+ }
355
+
356
+ // GET SLIDER STATES
357
+ get mainState$() {
358
+ return this.MainSlider?.state$;
359
+ }
360
+ get thumbsState$() {
361
+ return this.ThumbsSlider?.state$;
362
+ }
363
+
364
+ // NAVIGATION
365
+ get canPrev$(): Observable<boolean> | null {
366
+ if (!this.MainSlider || !this.MainSlider.canPrev$) {
367
+ console.warn('Navigation plugin is not enabled for this slider.');
368
+ return null;
369
+ }
370
+ return this.MainSlider.canPrev$;
371
+ }
372
+
373
+ get canNext$(): Observable<boolean> | null {
374
+ if (!this.MainSlider || !this.MainSlider.canNext$) {
375
+ console.warn('Navigation plugin is not enabled for this slider.');
376
+ return null;
377
+ }
378
+ return this.MainSlider.canNext$;
379
+ }
380
+
381
+ onPrevClick(event?: MouseEvent) {
382
+ event?.stopPropagation();
383
+ event?.preventDefault();
384
+
385
+ this.MainSlider?.canPrev$?.pipe(take(1)).subscribe((can) => {
386
+ if (can) this.MainSlider.prev?.();
387
+ });
388
+ }
389
+
390
+ onNextClick(event?: MouseEvent) {
391
+ event?.stopPropagation();
392
+ event?.preventDefault();
393
+
394
+ this.MainSlider?.canNext$?.pipe(take(1)).subscribe((can) => {
395
+ if (can) this.MainSlider.next?.();
396
+ });
397
+ }
398
+
399
+ // PAGINATION
400
+ get pager$(): Observable<Pager | null> | null {
401
+ if (!this.MainSlider?.pager$) {
402
+ console.warn('Pagination plugin is not enabled for this slider.');
403
+ return null;
404
+ }
405
+ return this.MainSlider.pager$;
406
+ }
407
+
408
+ goToSlide(index: number) {
409
+ this.MainSlider?.goToSlide?.(index);
410
+ }
411
+
412
+ protected readonly title = signal('slider-test');
413
+ }
414
+
415
+ ```
416
+ ----
417
+
418
+ ## 🔧 SliderConfig Reference
419
+
420
+ ```ts
421
+ export interface SliderConfig {
422
+ slides: any[];
423
+ slidesPerView: number;
424
+ slidesToSlide?: number;
425
+ loop?: 0 | 1 | 2;
426
+ vertical?: boolean;
427
+ changeToClickedSlide?: boolean;
428
+ isThumbs?: boolean;
429
+ gap?: number;
430
+
431
+ breakpoints?: {
432
+ mobile?: Partial<SliderConfig>;
433
+ tablet?: Partial<SliderConfig>;
434
+ desktop?: Partial<SliderConfig>;
435
+ };
436
+
437
+ plugins?: {
438
+ draggable?: boolean;
439
+ autoplay?: { delay?: number };
440
+ navigation?: boolean;
441
+ pagination?: boolean;
442
+ };
443
+
444
+ showOn?: {
445
+ mobile?: boolean;
446
+ tablet?: boolean;
447
+ desktop?: boolean;
448
+ };
449
+ }
450
+ ```
451
+
452
+ ---
453
+
454
+ ## 🔌 Plugins
455
+
456
+ Plugins are **opt‑in**. Only enabled plugins are initialized.
457
+
458
+ ### Draggable
459
+
460
+ ```ts
461
+ plugins: {
462
+ draggable: true;
463
+ }
464
+ ```
465
+
466
+ - Mouse + touch dragging
467
+ - Pointer capture outside slider bounds
468
+
469
+ ---
470
+
471
+ ### Navigation
472
+
473
+ ```ts
474
+ plugins: {
475
+ navigation: true;
476
+ }
477
+ ```
478
+
479
+ ```html
480
+ <button (click)="slider.prev()">Prev</button>
481
+ <button (click)="slider.next()">Next</button>
482
+ ```
483
+
484
+ Reactive state:
485
+
486
+ ```ts
487
+ slider.canPrev$;
488
+ slider.canNext$;
489
+ ```
490
+
491
+ ---
492
+
493
+ ### Pagination
494
+
495
+ ```ts
496
+ plugins: {
497
+ pagination: true;
498
+ }
499
+ ```
500
+
501
+ ```ts
502
+ slider.pager$; // Observable<Pager>
503
+ ```
504
+
505
+ Pager structure:
506
+
507
+ ```ts
508
+ interface Pager {
509
+ currentPage: number;
510
+ totalPages: number;
511
+ visibleDots: number[];
512
+ activeDotIndex: number;
513
+ }
514
+ ```
515
+
516
+ ---
517
+
518
+ ### Autoplay
519
+
520
+ ```ts
521
+ plugins: {
522
+ autoplay: {
523
+ delay: 3000;
524
+ }
525
+ }
526
+ ```
527
+
528
+ - Automatically pauses during dragging
529
+ - Resumes safely
530
+
531
+ ---
532
+
533
+ ## 🖼️ Thumbnails Slider (Main + Thumbs)
534
+
535
+ WingmanColt supports **fully synced sliders**.
536
+
537
+ ```html
538
+ <app-simple-slider #MainSlider [config]="mainConfig" (slideChange)="onMainChange($event)"></app-simple-slider>
539
+
540
+ <app-simple-slider #ThumbsSlider [config]="thumbsConfig"></app-simple-slider>
541
+ ```
542
+
543
+ ```ts
544
+ onMainChange(index: number) {
545
+ this.MainSlider.goTo(index);
546
+ this.ThumbsSlider.goTo(index);
547
+ }
548
+ ```
549
+
550
+ Thumbs config example:
551
+
552
+ ```ts
553
+ thumbsConfig = {
554
+ slides,
555
+ slidesPerView: 5,
556
+ isThumbs: true,
557
+ plugins: { draggable: true, navigation: true },
558
+ breakpoints: {
559
+ tablet: { slidesPerView: 4 },
560
+ desktop: { slidesPerView: 5 },
561
+ },
562
+ };
563
+ ```
564
+
565
+ ---
566
+
567
+ ## 📐 Responsive Breakpoints
568
+
569
+ Breakpoints are **container‑aware**, not just viewport‑based.
570
+
571
+ ```ts
572
+ breakpoints: {
573
+ mobile: { slidesPerView: 1 },
574
+ tablet: { slidesPerView: 2 },
575
+ desktop: { slidesPerView: 4 }
576
+ }
577
+ ```
578
+
579
+ Visibility control:
580
+
581
+ ```ts
582
+ showOn: {
583
+ mobile: false,
584
+ tablet: true,
585
+ desktop: true
586
+ }
587
+ ```
588
+
589
+ ---
590
+
591
+ ## 🧠 Architecture Overview
592
+
593
+ - **SliderEngine** – core logic, movement, breakpoints
594
+ - **SliderStore** – RxJS state container
595
+ - **Plugins** – isolated feature modules
596
+ - **SimpleSliderComponent** – UI wrapper
597
+
598
+ This separation allows:
599
+
600
+ - Easy feature expansion
601
+ - Predictable state transitions
602
+ - Minimal DOM coupling
603
+
604
+ ---
605
+
606
+ ## ♻️ Lifecycle & Reinitialization
607
+
608
+ The slider safely re‑initializes when:
609
+
610
+ - Slides array reference changes
611
+ - Breakpoints change
612
+ - Container size changes
613
+
614
+ ```ts
615
+ this.engine.destroy();
616
+ this.engine.init(newConfig);
617
+ ```
618
+
619
+ ---
620
+
621
+ ## 🛠️ Requirements
622
+
623
+ - Angular **18+**
624
+ - RxJS **7+**
625
+ - Browser support for `ResizeObserver`
626
+
627
+ ---
628
+
629
+ ## 🧪 Status
630
+
631
+ WingmanColt is **production‑ready**, actively evolving, and designed for real‑world applications.
632
+
633
+ Planned enhancements:
634
+
635
+ - Loop modes
636
+ - 🎞️ Animation presets
637
+ - ♿ Accessibility helpers
638
+ - 🔄 Virtual slides
639
+
640
+ ---
641
+
642
+ ## 📄 License
643
+
644
+ MIT
645
+
646
+ ---
647
+
648
+ ## 👤 Author
649
+
650
+ **WingmanColt**
651
+ Angular & Full‑Stack Engineer
652
+
653
+ ---
654
+
655
+ If you find this library useful, ⭐️ the repository and feel free to contribute.