column-fitter 15.0.7 → 15.0.9

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 CHANGED
@@ -1,24 +1,1046 @@
1
- # ColumnFitter
1
+ # Column Fitter Component
2
2
 
3
- This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.2.0.
3
+ ## Overview
4
4
 
5
- ## Code scaffolding
5
+ The `column-fitter` library provides a responsive grid layout system that automatically adjusts the number of columns based on the detected device size. It integrates with the `screen-observer` package to monitor device changes and dynamically updates CSS Grid layouts for optimal viewing across different screen sizes.
6
6
 
7
- Run `ng generate component component-name --project column-fitter` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project column-fitter`.
8
- > Note: Don't forget to add `--project column-fitter` or else it will be added to the default project in your `angular.json` file.
7
+ ### Core Capabilities
9
8
 
10
- ## Build
9
+ #### 📱 Responsive Grid Layout System
11
10
 
12
- Run `ng build column-fitter` to build the project. The build artifacts will be stored in the `dist/` directory.
11
+ - **Device Detection**: Automatically detects device type using screen-observer service
12
+ - **Dynamic Column Adjustment**: Updates column count based on current device (mobile, tablet, mini, desktop)
13
+ - **CSS Grid Integration**: Uses modern CSS Grid with auto-fit and minmax for flexible layouts
14
+ - **Flexible Configuration**: Support for both fixed columns and device-specific column settings
15
+ - **Responsive Behavior**: Seamlessly adapts between different screen sizes
16
+ - **Performance Optimized**: Uses RxJS distinctUntilChanged to prevent unnecessary updates
13
17
 
14
- ## Publishing
18
+ #### 🔧 Features
15
19
 
16
- After building your library with `ng build column-fitter`, go to the dist folder `cd dist/column-fitter` and run `npm publish`.
20
+ **Device Size Detection** - Automatic detection via screen-observer integration
21
+ ✅ **Dynamic Grid Updates** - Real-time column count adjustment
22
+ ✅ **CSS Grid Foundation** - Modern CSS Grid with repeat() and auto-fit
23
+ ✅ **Flexible Configuration** - Fixed numbers or device-specific settings
24
+ ✅ **Customizable Styling** - Configurable gap, margins, padding, and colors
25
+ ✅ **Performance Optimized** - Efficient change detection and updates
26
+ ✅ **Type-Safe Configuration** - Strong typing with Column and DeviceSizes models
27
+ ✅ **Demo Component** - Interactive demo showcasing all features
17
28
 
18
- ## Running unit tests
29
+ ### Key Benefits
19
30
 
20
- Run `ng test column-fitter` to execute the unit tests via [Karma](https://karma-runner.github.io).
31
+ | Feature | Description |
32
+ |---------|-------------|
33
+ | **Automatic Responsiveness** | No manual media queries needed |
34
+ | **Device-Aware Layouts** | Optimized layouts for each device type |
35
+ | **Modern CSS Grid** | Leverages CSS Grid for superior performance |
36
+ | **Type-Safe Configuration** | Full TypeScript support with device models |
37
+ | **Seamless Integration** | Works with existing screen-observer implementations |
21
38
 
22
- ## Further help
39
+ ---
23
40
 
24
- To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
41
+ ## Demo Component (`ColumnFitterDemoComponent`)
42
+
43
+ The demo component showcases responsive grid layouts using a bookmarks list example.
44
+
45
+ ### Usage
46
+
47
+ To use the demo component in your application:
48
+
49
+ ```html
50
+ <app-column-fitter-demo></app-column-fitter-demo>
51
+ ```
52
+
53
+ ### Demo Features
54
+
55
+ - **Bookmarks List**: Displays a list of classic books in responsive grid
56
+ - **Device-Specific Columns**:
57
+ - Mobile: 1 column
58
+ - Tablet: 4 columns
59
+ - Mini: 2 columns
60
+ - Desktop: Auto-fit with minmax
61
+ - **Real-time Updates**: Grid layout updates as you resize the browser
62
+ - **Visual Feedback**: Console logging of device changes
63
+
64
+ ---
65
+
66
+ ## Summary
67
+
68
+ The `column-fitter` library provides a modern, responsive grid system that automatically adapts column layouts based on device detection, making it perfect for creating responsive applications without manual media query management.
69
+
70
+ ---
71
+
72
+ ## Quick Start Guide
73
+
74
+ ### Installation & Setup (2 minutes)
75
+
76
+ #### 1. Import Module
77
+
78
+ ```typescript
79
+ // app.module.ts
80
+ import { ColumnFitterModule } from 'column-fitter';
81
+
82
+ @NgModule({
83
+ imports: [
84
+ ColumnFitterModule
85
+ ]
86
+ })
87
+ export class AppModule { }
88
+ ```
89
+
90
+ #### 2. Dependencies
91
+
92
+ The package requires the `screen-observer` package for device detection:
93
+
94
+ ```bash
95
+ npm install screen-observer
96
+ ```
97
+
98
+ ### Quick Examples
99
+
100
+ #### Example 1: Fixed Column Layout
101
+
102
+ ```typescript
103
+ import { Component } from '@angular/core';
104
+
105
+ @Component({
106
+ selector: 'app-fixed-grid',
107
+ template: `
108
+ <app-column-fitter
109
+ [columns]="4"
110
+ [gap]="'1rem'"
111
+ [padding]="'1rem'"
112
+ [backgroundColor]="'#f5f5f5'">
113
+
114
+ <div class="grid-item" *ngFor="let item of items">
115
+ {{ item.name }}
116
+ </div>
117
+
118
+ </app-column-fitter>
119
+ `
120
+ })
121
+ export class FixedGridComponent {
122
+ items = [
123
+ { id: 1, name: 'Item 1' },
124
+ { id: 2, name: 'Item 2' },
125
+ { id: 3, name: 'Item 3' },
126
+ { id: 4, name: 'Item 4' },
127
+ { id: 5, name: 'Item 5' },
128
+ { id: 6, name: 'Item 6' }
129
+ ];
130
+ }
131
+ ```
132
+
133
+ #### Example 2: Device-Specific Columns
134
+
135
+ ```typescript
136
+ import { Component } from '@angular/core';
137
+ import { Column, DeviceSizes } from 'column-fitter';
138
+
139
+ @Component({
140
+ selector: 'app-responsive-grid',
141
+ template: `
142
+ <app-column-fitter
143
+ [columns]="responsiveColumns"
144
+ [gap]="'1.5rem'"
145
+ [margin]="'1rem'"
146
+ [minWidth]="'250px'"
147
+ [backgroundColor]="'#ffffff'"
148
+ [padding]="'1rem'">
149
+
150
+ <div class="product-card" *ngFor="let product of products">
151
+ <h3>{{ product.name }}</h3>
152
+ <p>{{ product.description }}</p>
153
+ <span class="price">{{ product.price | currency }}</span>
154
+ </div>
155
+
156
+ </app-column-fitter>
157
+ `,
158
+ styles: [`
159
+ .product-card {
160
+ padding: 1rem;
161
+ border: 1px solid #ddd;
162
+ border-radius: 8px;
163
+ background: white;
164
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
165
+ }
166
+
167
+ .price {
168
+ font-weight: bold;
169
+ color: #2196f3;
170
+ }
171
+ `]
172
+ })
173
+ export class ResponsiveGridComponent {
174
+ responsiveColumns: Column[] = [
175
+ { device: DeviceSizes.MOBILE, columns: 1 },
176
+ { device: DeviceSizes.MINI, columns: 2 },
177
+ { device: DeviceSizes.TABLET, columns: 3 },
178
+ { device: DeviceSizes.DESKTOP, columns: 4 }
179
+ ];
180
+
181
+ products = [
182
+ { id: 1, name: 'Product 1', description: 'Description 1', price: 29.99 },
183
+ { id: 2, name: 'Product 2', description: 'Description 2', price: 39.99 },
184
+ { id: 3, name: 'Product 3', description: 'Description 3', price: 49.99 },
185
+ { id: 4, name: 'Product 4', description: 'Description 4', price: 59.99 },
186
+ { id: 5, name: 'Product 5', description: 'Description 5', price: 69.99 },
187
+ { id: 6, name: 'Product 6', description: 'Description 6', price: 79.99 }
188
+ ];
189
+ }
190
+ ```
191
+
192
+ #### Example 3: Gallery Layout
193
+
194
+ ```typescript
195
+ import { Component } from '@angular/core';
196
+ import { Column, DeviceSizes } from 'column-fitter';
197
+
198
+ @Component({
199
+ selector: 'app-image-gallery',
200
+ template: `
201
+ <app-column-fitter
202
+ [columns]="galleryColumns"
203
+ [gap]="'0.5rem'"
204
+ [padding]="'0.5rem'"
205
+ [backgroundColor]="'#000'"
206
+ [minWidth]="'200px'">
207
+
208
+ <div class="gallery-item" *ngFor="let image of images">
209
+ <img [src]="image.url" [alt]="image.alt" />
210
+ <div class="overlay">
211
+ <h4>{{ image.title }}</h4>
212
+ </div>
213
+ </div>
214
+
215
+ </app-column-fitter>
216
+ `,
217
+ styles: [`
218
+ .gallery-item {
219
+ position: relative;
220
+ overflow: hidden;
221
+ border-radius: 4px;
222
+ aspect-ratio: 1;
223
+ }
224
+
225
+ .gallery-item img {
226
+ width: 100%;
227
+ height: 100%;
228
+ object-fit: cover;
229
+ transition: transform 0.3s ease;
230
+ }
231
+
232
+ .gallery-item:hover img {
233
+ transform: scale(1.1);
234
+ }
235
+
236
+ .overlay {
237
+ position: absolute;
238
+ bottom: 0;
239
+ left: 0;
240
+ right: 0;
241
+ background: linear-gradient(transparent, rgba(0,0,0,0.8));
242
+ color: white;
243
+ padding: 1rem;
244
+ transform: translateY(100%);
245
+ transition: transform 0.3s ease;
246
+ }
247
+
248
+ .gallery-item:hover .overlay {
249
+ transform: translateY(0);
250
+ }
251
+ `]
252
+ })
253
+ export class ImageGalleryComponent {
254
+ galleryColumns: Column[] = [
255
+ { device: DeviceSizes.MOBILE, columns: 2 },
256
+ { device: DeviceSizes.MINI, columns: 3 },
257
+ { device: DeviceSizes.TABLET, columns: 4 },
258
+ { device: DeviceSizes.DESKTOP, columns: 6 }
259
+ ];
260
+
261
+ images = [
262
+ { id: 1, url: 'https://picsum.photos/300/300?random=1', alt: 'Random 1', title: 'Image 1' },
263
+ { id: 2, url: 'https://picsum.photos/300/300?random=2', alt: 'Random 2', title: 'Image 2' },
264
+ { id: 3, url: 'https://picsum.photos/300/300?random=3', alt: 'Random 3', title: 'Image 3' },
265
+ { id: 4, url: 'https://picsum.photos/300/300?random=4', alt: 'Random 4', title: 'Image 4' },
266
+ { id: 5, url: 'https://picsum.photos/300/300?random=5', alt: 'Random 5', title: 'Image 5' },
267
+ { id: 6, url: 'https://picsum.photos/300/300?random=6', alt: 'Random 6', title: 'Image 6' }
268
+ ];
269
+ }
270
+ ```
271
+
272
+ #### Example 4: Dashboard Cards
273
+
274
+ ```typescript
275
+ import { Component } from '@angular/core';
276
+ import { Column, DeviceSizes } from 'column-fitter';
277
+
278
+ @Component({
279
+ selector: 'app-dashboard',
280
+ template: `
281
+ <div class="dashboard-container">
282
+ <h2>Dashboard</h2>
283
+
284
+ <app-column-fitter
285
+ [columns]="dashboardColumns"
286
+ [gap]="'1rem'"
287
+ [padding'"
288
+ [background]="'1remColor]="'#f8f9fa'"
289
+ [minWidth]="'300px'">
290
+
291
+ <div class="stat-card" *ngFor="let stat of statistics">
292
+ <div class="stat-icon">
293
+ <mat-icon>{{ stat.icon }}</mat-icon>
294
+ </div>
295
+ <div class="stat-content">
296
+ <h3>{{ stat.value }}</h3>
297
+ <p>{{ stat.label }}</p>
298
+ </div>
299
+ </div>
300
+
301
+ </app-column-fitter>
302
+ </div>
303
+ `,
304
+ styles: [`
305
+ .dashboard-container {
306
+ padding: 2rem;
307
+ }
308
+
309
+ .dashboard-container h2 {
310
+ margin-bottom: 2rem;
311
+ color: #333;
312
+ }
313
+
314
+ .stat-card {
315
+ background: white;
316
+ padding: 1.5rem;
317
+ border-radius: 8px;
318
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
319
+ display: flex;
320
+ align-items: center;
321
+ gap: 1rem;
322
+ }
323
+
324
+ .stat-icon {
325
+ width: 48px;
326
+ height: 48px;
327
+ border-radius: 50%;
328
+ display: flex;
329
+ align-items: center;
330
+ justify-content: center;
331
+ background: #e3f2fd;
332
+ color: #1976d2;
333
+ }
334
+
335
+ .stat-content h3 {
336
+ margin: 0;
337
+ font-size: 1.5rem;
338
+ font-weight: bold;
339
+ color: #333;
340
+ }
341
+
342
+ .stat-content p {
343
+ margin: 0;
344
+ color: #666;
345
+ font-size: 0.9rem;
346
+ }
347
+ `]
348
+ })
349
+ export class DashboardComponent {
350
+ dashboardColumns: Column[] = [
351
+ { device: DeviceSizes.MOBILE, columns: 1 },
352
+ { device: DeviceSizes.TABLET, columns: 2 },
353
+ { device: DeviceSizes.MINI, columns: 2 },
354
+ { device: DeviceSizes.DESKTOP, columns: 4 }
355
+ ];
356
+
357
+ statistics = [
358
+ { id: 1, icon: 'people', value: '1,234', label: 'Total Users' },
359
+ { id: 2, icon: 'shopping_cart', value: '$12,345', label: 'Revenue' },
360
+ { id: 3, icon: 'trending_up', value: '98.5%', label: 'Growth Rate' },
361
+ { id: 4, icon: 'assignment', value: '567', label: 'Tasks Completed' },
362
+ { id: 5, icon: 'star', value: '4.8/5', label: 'Customer Rating' },
363
+ { id: 6, icon: 'notifications', value: '23', label: 'Pending Alerts' }
364
+ ];
365
+ }
366
+ ```
367
+
368
+ ---
369
+
370
+ ## Component API
371
+
372
+ ### Inputs
373
+
374
+ | Input | Type | Description | Default |
375
+ | :--- | :--- | :--- | :--- |
376
+ | `padding` | `string` | Padding for the grid container (CSS padding value) | `''` |
377
+ | `margin` | `string` | Margin for the grid container (CSS margin value) | `''` |
378
+ | `backgroundColor` | `string` | Background color for the grid container | `''` |
379
+ | `minWidth` | `string` | Minimum width for auto-fit columns (CSS length value) | `''` |
380
+ | `gap` | `string` | Gap between grid items (CSS gap value) | `'1rem'` |
381
+ | `columns` | `number \| Column[]` | Column configuration - fixed number or device-specific array | `0` |
382
+
383
+ ### Dynamic Properties
384
+
385
+ | Property | Type | Description |
386
+ |----------|------|-------------|
387
+ | `gridColumns` | `string` | Current CSS grid-template-columns value |
388
+ | `hasColumns` | `boolean` | Whether valid column configuration exists |
389
+ | `subscriptions` | `Subscription` | RxJS subscription management |
390
+
391
+ ---
392
+
393
+ ## Model Structures
394
+
395
+ ### DeviceSizes Enum
396
+
397
+ ```typescript
398
+ export enum DeviceSizes {
399
+ DESKTOP = 'desktop', // Desktop/large screens
400
+ TABLET = 'tablet', // Tablet devices
401
+ MINI = 'mini', // Small tablets/large phones
402
+ MOBILE = 'mobile' // Mobile phones
403
+ }
404
+ ```
405
+
406
+ ### Column Interface
407
+
408
+ ```typescript
409
+ export interface ColumnInterface {
410
+ device: DeviceSizes; // Target device type
411
+ columns: number; // Number of columns for this device
412
+ }
413
+ ```
414
+
415
+ ### Column Class
416
+
417
+ ```typescript
418
+ export class Column implements ColumnInterface {
419
+ constructor(
420
+ public device = DeviceSizes.DESKTOP,
421
+ public columns = 0,
422
+ ) {}
423
+
424
+ static adapt(item?: any): Column {
425
+ return new Column(
426
+ item?.device,
427
+ item?.columns
428
+ );
429
+ }
430
+ }
431
+ ```
432
+
433
+ ### Usage Examples
434
+
435
+ ```typescript
436
+ // Device-specific column configurations
437
+ const responsiveColumns: Column[] = [
438
+ new Column(DeviceSizes.MOBILE, 1), // 1 column on mobile
439
+ new Column(DeviceSizes.MINI, 2), // 2 columns on mini devices
440
+ new Column(DeviceSizes.TABLET, 3), // 3 columns on tablets
441
+ new Column(DeviceSizes.DESKTOP, 4) // 4 columns on desktop
442
+ ];
443
+
444
+ // Using adapt method
445
+ const adaptedColumns = [
446
+ Column.adapt({ device: DeviceSizes.MOBILE, columns: 1 }),
447
+ Column.adapt({ device: DeviceSizes.TABLET, columns: 3 })
448
+ ];
449
+
450
+ // Mixed configuration
451
+ const mixedColumns: (number | Column[])[] = [
452
+ 3, // Fixed 3 columns for all devices
453
+ // OR
454
+ [
455
+ { device: DeviceSizes.MOBILE, columns: 1 },
456
+ { device: DeviceSizes.TABLET, columns: 2 },
457
+ { device: DeviceSizes.DESKTOP, columns: 4 }
458
+ ]
459
+ ];
460
+ ```
461
+
462
+ ---
463
+
464
+ ## Grid Layout Logic
465
+
466
+ ### CSS Grid Generation
467
+
468
+ The component automatically generates CSS Grid templates based on the configuration:
469
+
470
+ #### Fixed Column Mode
471
+ ```typescript
472
+ // Input: columns = 3
473
+ // Output: gridColumns = 'repeat(3, 1fr)'
474
+
475
+ // Input: columns = 0 (disabled)
476
+ // Output: gridColumns = 'repeat(auto-fit, minmax(250px, 1fr))'
477
+ ```
478
+
479
+ #### Device-Specific Mode
480
+ ```typescript
481
+ // Device detection logic
482
+ if (device === 'desktop' && found(DeviceSizes.DESKTOP)) {
483
+ const cols = found(DeviceSizes.DESKTOP) as Column;
484
+ return `repeat(${cols.columns}, 1fr)`;
485
+ }
486
+
487
+ // Fallback for unmatched devices
488
+ return `repeat(auto-fit, minmax(${this.minWidth}, 1fr))`;
489
+ ```
490
+
491
+ ### Device Detection Flow
492
+
493
+ 1. **Screen Observer Integration**: Subscribes to `screenObserverService.device$`
494
+ 2. **Change Detection**: Uses RxJS `distinctUntilChanged()` to prevent unnecessary updates
495
+ 3. **Column Calculation**: Calls `getGridTemplateColumns()` with current device
496
+ 4. **Grid Update**: Updates `gridColumns` property with new CSS Grid value
497
+
498
+ ---
499
+
500
+ ## Module Configuration
501
+
502
+ ### ColumnFitterModule
503
+
504
+ **No Global Configuration Required**
505
+
506
+ The `ColumnFitterModule` does not provide a `forRoot()` method or global configuration options. All configuration is done at the component level through input properties.
507
+
508
+ #### Module Structure
509
+
510
+ ```typescript
511
+ @NgModule({
512
+ declarations: [
513
+ ColumnFitterComponent,
514
+ ColumnFitterDemoComponent
515
+ ],
516
+ imports: [
517
+ // Dependencies are imported by the consuming application
518
+ // screen-observer must be installed separately
519
+ ],
520
+ exports: [
521
+ ColumnFitterComponent,
522
+ ColumnFitterDemoComponent
523
+ ]
524
+ })
525
+ export class ColumnFitterModule { }
526
+ ```
527
+
528
+ #### Dependencies
529
+
530
+ - **screen-observer**: Device detection service (must be installed separately)
531
+ - **@angular/core**: Core Angular functionality
532
+ - **rxjs**: Reactive programming utilities for device change detection
533
+
534
+ ---
535
+
536
+ ## Styling and Customization
537
+
538
+ ### CSS Grid Styling
539
+
540
+ The component uses CSS Grid with the following base styles:
541
+
542
+ ```scss
543
+ :host {
544
+ display: block;
545
+ }
546
+
547
+ .grid-container {
548
+ display: grid;
549
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
550
+ gap: 1rem;
551
+ }
552
+ ```
553
+
554
+ ### Custom Styling Examples
555
+
556
+ #### Custom Grid Appearance
557
+
558
+ ```scss
559
+ // Enhanced grid styling
560
+ :host ::ng-deep .grid-container {
561
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
562
+ border-radius: 12px;
563
+ padding: 2rem;
564
+
565
+ .grid-item {
566
+ background: white;
567
+ border-radius: 8px;
568
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
569
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
570
+
571
+ &:hover {
572
+ transform: translateY(-2px);
573
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
574
+ }
575
+ }
576
+ }
577
+ ```
578
+
579
+ #### Responsive Gap Adjustment
580
+
581
+ ```scss
582
+ // Dynamic gaps based on device
583
+ :host ::ng-deep .grid-container {
584
+ gap: var(--grid-gap, 1rem);
585
+
586
+ @media (max-width: 768px) {
587
+ --grid-gap: 0.5rem;
588
+ }
589
+
590
+ @media (min-width: 1200px) {
591
+ --grid-gap: 1.5rem;
592
+ }
593
+ }
594
+ ```
595
+
596
+ ### Advanced Layout Patterns
597
+
598
+ #### Masonry-Style Layout
599
+
600
+ ```scss
601
+ // CSS Grid with masonry-like behavior
602
+ .masonry-grid {
603
+ display: grid;
604
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
605
+ grid-auto-rows: 200px;
606
+ gap: 1rem;
607
+
608
+ .masonry-item {
609
+ grid-row-end: span var(--row-span, 1);
610
+
611
+ &.large {
612
+ --row-span: 2;
613
+ }
614
+
615
+ &.wide {
616
+ grid-column-end: span 2;
617
+ }
618
+ }
619
+ }
620
+ ```
621
+
622
+ #### Card-Based Layout
623
+
624
+ ```scss
625
+ // Card layout with consistent height
626
+ .card-grid {
627
+ display: grid;
628
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
629
+ gap: 1.5rem;
630
+ align-items: stretch;
631
+
632
+ .card {
633
+ display: flex;
634
+ flex-direction: column;
635
+ height: 100%;
636
+ background: white;
637
+ border-radius: 8px;
638
+ overflow: hidden;
639
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
640
+
641
+ .card-image {
642
+ height: 200px;
643
+ overflow: hidden;
644
+
645
+ img {
646
+ width: 100%;
647
+ height: 100%;
648
+ object-fit: cover;
649
+ }
650
+ }
651
+
652
+ .card-content {
653
+ padding: 1.5rem;
654
+ flex: 1;
655
+ display: flex;
656
+ flex-direction: column;
657
+ }
658
+ }
659
+ }
660
+ ```
661
+
662
+ ---
663
+
664
+ ## Integration Examples
665
+
666
+ ### With Angular Material
667
+
668
+ ```typescript
669
+ import { Component } from '@angular/core';
670
+ import { Column, DeviceSizes } from 'column-fitter';
671
+
672
+ @Component({
673
+ selector: 'app-material-grid',
674
+ template: `
675
+ <app-column-fitter
676
+ [columns]="materialColumns"
677
+ [gap]="'1rem'"
678
+ [padding]="'1rem'">
679
+
680
+ <mat-card class="grid-card" *ngFor="let item of materialItems">
681
+ <mat-card-header>
682
+ <mat-card-title>{{ item.title }}</mat-card-title>
683
+ <mat-card-subtitle>{{ item.subtitle }}</mat-card-subtitle>
684
+ </mat-card-header>
685
+
686
+ <img mat-card-image [src]="item.image" [alt]="item.title">
687
+
688
+ <mat-card-content>
689
+ <p>{{ item.description }}</p>
690
+ </mat-card-content>
691
+
692
+ <mat-card-actions>
693
+ <button mat-button>LIKE</button>
694
+ <button mat-button>SHARE</button>
695
+ </mat-card-actions>
696
+ </mat-card>
697
+
698
+ </app-column-fitter>
699
+ `
700
+ })
701
+ export class MaterialGridComponent {
702
+ materialColumns: Column[] = [
703
+ { device: DeviceSizes.MOBILE, columns: 1 },
704
+ { device: DeviceSizes.TABLET, columns: 2 },
705
+ { device: DeviceSizes.DESKTOP, columns: 3 }
706
+ ];
707
+
708
+ materialItems = [
709
+ {
710
+ title: 'Card 1',
711
+ subtitle: 'Subtitle 1',
712
+ description: 'Description for card 1',
713
+ image: 'https://picsum.photos/400/200?random=1'
714
+ },
715
+ // ... more items
716
+ ];
717
+ }
718
+ ```
719
+
720
+ ### With Dynamic Content
721
+
722
+ ```typescript
723
+ import { Component } from '@angular/core';
724
+ import { Column, DeviceSizes } from 'column-fitter';
725
+
726
+ @Component({
727
+ selector: 'app-dynamic-content',
728
+ template: `
729
+ <div class="controls">
730
+ <button (click)="addItem()">Add Item</button>
731
+ <button (click)="removeItem()">Remove Item</button>
732
+ <select [(ngModel)]="selectedLayout" (change)="changeLayout()">
733
+ <option value="mobile1">Mobile: 1 Col</option>
734
+ <option value="tablet3">Tablet: 3 Col</option>
735
+ <option value="desktop4">Desktop: 4 Col</option>
736
+ </select>
737
+ </div>
738
+
739
+ <app-column-fitter
740
+ [columns]="currentColumns"
741
+ [gap]="'1rem'"
742
+ [padding]="'1rem'">
743
+
744
+ <div class="dynamic-item" *ngFor="let item of dynamicItems; trackBy: trackById">
745
+ <h3>{{ item.title }}</h3>
746
+ <p>{{ item.content }}</p>
747
+ <small>ID: {{ item.id }}</small>
748
+ </div>
749
+
750
+ </app-column-fitter>
751
+ `
752
+ })
753
+ export class DynamicContentComponent {
754
+ currentColumns: Column[] = [
755
+ { device: DeviceSizes.MOBILE, columns: 1 },
756
+ { device: DeviceSizes.TABLET, columns: 3 },
757
+ { device: DeviceSizes.DESKTOP, columns: 4 }
758
+ ];
759
+
760
+ selectedLayout = 'tablet3';
761
+ dynamicItems = [
762
+ { id: 1, title: 'Item 1', content: 'Content 1' },
763
+ { id: 2, title: 'Item 2', content: 'Content 2' },
764
+ { id: 3, title: 'Item 3', content: 'Content 3' }
765
+ ];
766
+
767
+ addItem() {
768
+ const newItem = {
769
+ id: Date.now(),
770
+ title: `Item ${this.dynamicItems.length + 1}`,
771
+ content: `Content ${this.dynamicItems.length + 1}`
772
+ };
773
+ this.dynamicItems = [...this.dynamicItems, newItem];
774
+ }
775
+
776
+ removeItem() {
777
+ if (this.dynamicItems.length > 0) {
778
+ this.dynamicItems = this.dynamicItems.slice(0, -1);
779
+ }
780
+ }
781
+
782
+ changeLayout() {
783
+ switch (this.selectedLayout) {
784
+ case 'mobile1':
785
+ this.currentColumns = [
786
+ { device: DeviceSizes.MOBILE, columns: 1 },
787
+ { device: DeviceSizes.TABLET, columns: 1 },
788
+ { device: DeviceSizes.DESKTOP, columns: 1 }
789
+ ];
790
+ break;
791
+ case 'tablet3':
792
+ this.currentColumns = [
793
+ { device: DeviceSizes.MOBILE, columns: 1 },
794
+ { device: DeviceSizes.TABLET, columns: 3 },
795
+ { device: DeviceSizes.DESKTOP, columns: 3 }
796
+ ];
797
+ break;
798
+ case 'desktop4':
799
+ this.currentColumns = [
800
+ { device: DeviceSizes.MOBILE, columns: 1 },
801
+ { device: DeviceSizes.TABLET, columns: 2 },
802
+ { device: DeviceSizes.DESKTOP, columns: 4 }
803
+ ];
804
+ break;
805
+ }
806
+ }
807
+
808
+ trackById(index: number, item: any): any {
809
+ return item.id;
810
+ }
811
+ }
812
+ ```
813
+
814
+ ---
815
+
816
+ ## Performance Optimization
817
+
818
+ ### Change Detection
819
+
820
+ The component uses several performance optimizations:
821
+
822
+ 1. **distinctUntilChanged()**: Prevents duplicate device updates
823
+ 2. **Efficient Grid Calculation**: Only recalculates when device actually changes
824
+ 3. **Subscription Management**: Properly cleans up RxJS subscriptions
825
+
826
+ ### Memory Management
827
+
828
+ ```typescript
829
+ ngOnDestroy() {
830
+ // Clean up subscriptions to prevent memory leaks
831
+ this.subscriptions.unsubscribe();
832
+ }
833
+ ```
834
+
835
+ ### Large Dataset Handling
836
+
837
+ ```typescript
838
+ // Use trackBy for large lists
839
+ @Component({
840
+ template: `
841
+ <app-column-fitter [columns]="columns">
842
+ <div *ngFor="let item of largeDataset; trackBy: trackById">
843
+ {{ item.name }}
844
+ </div>
845
+ </app-column-fitter>
846
+ `
847
+ })
848
+ export class LargeDatasetComponent {
849
+ trackById(index: number, item: any): any {
850
+ return item.id; // Use unique identifier
851
+ }
852
+ }
853
+ ```
854
+
855
+ ---
856
+
857
+ ## Testing
858
+
859
+ ### Unit Testing Example
860
+
861
+ ```typescript
862
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
863
+ import { ColumnFitterComponent } from './column-fitter.component';
864
+ import { Column, DeviceSizes } from './models/column.model';
865
+ import { ScreenObserverService } from 'screen-observer';
866
+
867
+ describe('ColumnFitterComponent', () => {
868
+ let component: ColumnFitterComponent;
869
+ let fixture: ComponentFixture<ColumnFitterComponent>;
870
+ let mockScreenObserverService: jasmine.SpyObj<ScreenObserverService>;
871
+
872
+ beforeEach(async () => {
873
+ const screenObserverSpy = jasmine.createSpyObj('ScreenObserverService', ['device$']);
874
+
875
+ await TestBed.configureTestingModule({
876
+ declarations: [ ColumnFitterComponent ],
877
+ providers: [
878
+ { provide: ScreenObserverService, useValue: screenObserverSpy }
879
+ ]
880
+ }).compileComponents();
881
+
882
+ fixture = TestBed.createComponent(ColumnFitterComponent);
883
+ component = fixture.componentInstance;
884
+ mockScreenObserverService = TestBed.inject(ScreenObserverService) as jasmine.SpyObj<ScreenObserverService>;
885
+ });
886
+
887
+ it('should create', () => {
888
+ expect(component).toBeTruthy();
889
+ });
890
+
891
+ it('should set fixed columns correctly', () => {
892
+ component.columns = 3;
893
+ fixture.detectChanges();
894
+
895
+ const result = component.getGridTemplateColumns('desktop');
896
+ expect(result).toBe('repeat(3, 1fr)');
897
+ });
898
+
899
+ it('should handle device-specific columns', () => {
900
+ component.columns = [
901
+ { device: DeviceSizes.MOBILE, columns: 1 },
902
+ { device: DeviceSizes.TABLET, columns: 3 },
903
+ { device: DeviceSizes.DESKTOP, columns: 4 }
904
+ ];
905
+
906
+ fixture.detectChanges();
907
+
908
+ expect(component.getGridTemplateColumns('mobile')).toBe('repeat(1, 1fr)');
909
+ expect(component.getGridTemplateColumns('tablet')).toBe('repeat(3, 1fr)');
910
+ expect(component.getGridTemplateColumns('desktop')).toBe('repeat(4, 1fr)');
911
+ });
912
+
913
+ it('should fallback to auto-fit when no matching device', () => {
914
+ component.columns = [
915
+ { device: DeviceSizes.MOBILE, columns: 1 }
916
+ ];
917
+ component.minWidth = '200px';
918
+
919
+ fixture.detectChanges();
920
+
921
+ const result = component.getGridTemplateColumns('desktop');
922
+ expect(result).toBe('repeat(auto-fit, minmax(200px, 1fr))');
923
+ });
924
+
925
+ it('should handle disabled state (columns = 0)', () => {
926
+ component.columns = 0;
927
+ component.minWidth = '250px';
928
+
929
+ fixture.detectChanges();
930
+
931
+ const result = component.getGridTemplateColumns('desktop');
932
+ expect(result).toBe('repeat(auto-fit, minmax(250px, 1fr))');
933
+ });
934
+ });
935
+ ```
936
+
937
+ ---
938
+
939
+ ## Troubleshooting
940
+
941
+ ### Common Issues
942
+
943
+ 1. **No columns showing**: Ensure screen-observer package is installed and configured
944
+ 2. **Layout not updating**: Check that device$ observable is emitting values
945
+ 3. **Styling issues**: Verify CSS Grid is supported in target browsers
946
+ 4. **Performance issues**: Consider using OnPush change detection for large datasets
947
+
948
+ ### Debug Mode
949
+
950
+ ```typescript
951
+ @Component({
952
+ template: `
953
+ <div class="debug-info">
954
+ Current Device: {{ currentDevice }}<br>
955
+ Grid Columns: {{ gridColumns }}<br>
956
+ Has Columns: {{ hasColumns }}<br>
957
+ Columns Config: {{ columns | json }}
958
+ </div>
959
+
960
+ <app-column-fitter
961
+ [columns]="columns"
962
+ [gap]="gap"
963
+ [minWidth]="minWidth">
964
+ <!-- Content -->
965
+ </app-column-fitter>
966
+ `
967
+ })
968
+ export class DebugColumnFitterComponent {
969
+ currentDevice = '';
970
+ gridColumns = '';
971
+ hasColumns = false;
972
+ columns: any = [];
973
+ gap = '1rem';
974
+ minWidth = '250px';
975
+
976
+ constructor() {
977
+ // Add debugging logic
978
+ }
979
+ }
980
+ ```
981
+
982
+ ### Performance Monitoring
983
+
984
+ ```typescript
985
+ ngOnInit() {
986
+ const start = performance.now();
987
+
988
+ this.subscriptions.add(
989
+ this.screenObserverService.device$.subscribe((screen: string) => {
990
+ const updateStart = performance.now();
991
+ this.gridColumns = this.getGridTemplateColumns(screen);
992
+ const updateEnd = performance.now();
993
+
994
+ console.log(`Grid update took ${updateEnd - updateStart}ms for device: ${screen}`);
995
+ })
996
+ );
997
+
998
+ const initEnd = performance.now();
999
+ console.log(`ColumnFitter initialization took ${initEnd - start}ms`);
1000
+ }
1001
+ ```
1002
+
1003
+ ---
1004
+
1005
+ ## Browser Support
1006
+
1007
+ ### CSS Grid Support
1008
+
1009
+ The component requires CSS Grid support, which is available in:
1010
+
1011
+ - **Chrome**: 57+ (March 2017)
1012
+ - **Firefox**: 52+ (March 2017)
1013
+ - **Safari**: 10.1+ (March 2017)
1014
+ - **Edge**: 16+ (October 2017)
1015
+
1016
+ ### Fallback for Older Browsers
1017
+
1018
+ ```scss
1019
+ // CSS Grid fallback using Flexbox
1020
+ .grid-container {
1021
+ display: flex;
1022
+ flex-wrap: wrap;
1023
+ margin: -0.5rem;
1024
+
1025
+ .grid-item {
1026
+ flex: 1 1 250px; /* Minimum width of 250px */
1027
+ margin: 0.5rem;
1028
+
1029
+ @supports (display: grid) {
1030
+ display: grid;
1031
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
1032
+ margin: 0;
1033
+ }
1034
+ }
1035
+ }
1036
+ ```
1037
+
1038
+ ### Progressive Enhancement
1039
+
1040
+ ```typescript
1041
+ // JavaScript fallback for older browsers
1042
+ if (!CSS.supports('display', 'grid')) {
1043
+ // Apply Flexbox fallback
1044
+ this.applyFlexboxFallback();
1045
+ }
1046
+ ```