npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

column-fitter

v15.0.9

Published

This is an Angular Module containing Components/Services using Material

Readme

Column Fitter Component

Overview

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.

Core Capabilities

📱 Responsive Grid Layout System

  • Device Detection: Automatically detects device type using screen-observer service
  • Dynamic Column Adjustment: Updates column count based on current device (mobile, tablet, mini, desktop)
  • CSS Grid Integration: Uses modern CSS Grid with auto-fit and minmax for flexible layouts
  • Flexible Configuration: Support for both fixed columns and device-specific column settings
  • Responsive Behavior: Seamlessly adapts between different screen sizes
  • Performance Optimized: Uses RxJS distinctUntilChanged to prevent unnecessary updates

🔧 Features

Device Size Detection - Automatic detection via screen-observer integration
Dynamic Grid Updates - Real-time column count adjustment
CSS Grid Foundation - Modern CSS Grid with repeat() and auto-fit
Flexible Configuration - Fixed numbers or device-specific settings
Customizable Styling - Configurable gap, margins, padding, and colors
Performance Optimized - Efficient change detection and updates
Type-Safe Configuration - Strong typing with Column and DeviceSizes models
Demo Component - Interactive demo showcasing all features

Key Benefits

| Feature | Description | |---------|-------------| | Automatic Responsiveness | No manual media queries needed | | Device-Aware Layouts | Optimized layouts for each device type | | Modern CSS Grid | Leverages CSS Grid for superior performance | | Type-Safe Configuration | Full TypeScript support with device models | | Seamless Integration | Works with existing screen-observer implementations |


Demo Component (ColumnFitterDemoComponent)

The demo component showcases responsive grid layouts using a bookmarks list example.

Usage

To use the demo component in your application:

<app-column-fitter-demo></app-column-fitter-demo>

Demo Features

  • Bookmarks List: Displays a list of classic books in responsive grid
  • Device-Specific Columns:
    • Mobile: 1 column
    • Tablet: 4 columns
    • Mini: 2 columns
    • Desktop: Auto-fit with minmax
  • Real-time Updates: Grid layout updates as you resize the browser
  • Visual Feedback: Console logging of device changes

Summary

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.


Quick Start Guide

Installation & Setup (2 minutes)

1. Import Module

// app.module.ts
import { ColumnFitterModule } from 'column-fitter';

@NgModule({
  imports: [
    ColumnFitterModule
  ]
})
export class AppModule { }

2. Dependencies

The package requires the screen-observer package for device detection:

npm install screen-observer

Quick Examples

Example 1: Fixed Column Layout

import { Component } from '@angular/core';

@Component({
  selector: 'app-fixed-grid',
  template: `
    <app-column-fitter
      [columns]="4"
      [gap]="'1rem'"
      [padding]="'1rem'"
      [backgroundColor]="'#f5f5f5'">
      
      <div class="grid-item" *ngFor="let item of items">
        {{ item.name }}
      </div>
      
    </app-column-fitter>
  `
})
export class FixedGridComponent {
  items = [
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' },
    { id: 3, name: 'Item 3' },
    { id: 4, name: 'Item 4' },
    { id: 5, name: 'Item 5' },
    { id: 6, name: 'Item 6' }
  ];
}

Example 2: Device-Specific Columns

import { Component } from '@angular/core';
import { Column, DeviceSizes } from 'column-fitter';

@Component({
  selector: 'app-responsive-grid',
  template: `
    <app-column-fitter
      [columns]="responsiveColumns"
      [gap]="'1.5rem'"
      [margin]="'1rem'"
      [minWidth]="'250px'"
      [backgroundColor]="'#ffffff'"
      [padding]="'1rem'">
      
      <div class="product-card" *ngFor="let product of products">
        <h3>{{ product.name }}</h3>
        <p>{{ product.description }}</p>
        <span class="price">{{ product.price | currency }}</span>
      </div>
      
    </app-column-fitter>
  `,
  styles: [`
    .product-card {
      padding: 1rem;
      border: 1px solid #ddd;
      border-radius: 8px;
      background: white;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    }
    
    .price {
      font-weight: bold;
      color: #2196f3;
    }
  `]
})
export class ResponsiveGridComponent {
  responsiveColumns: Column[] = [
    { device: DeviceSizes.MOBILE, columns: 1 },
    { device: DeviceSizes.MINI, columns: 2 },
    { device: DeviceSizes.TABLET, columns: 3 },
    { device: DeviceSizes.DESKTOP, columns: 4 }
  ];
  
  products = [
    { id: 1, name: 'Product 1', description: 'Description 1', price: 29.99 },
    { id: 2, name: 'Product 2', description: 'Description 2', price: 39.99 },
    { id: 3, name: 'Product 3', description: 'Description 3', price: 49.99 },
    { id: 4, name: 'Product 4', description: 'Description 4', price: 59.99 },
    { id: 5, name: 'Product 5', description: 'Description 5', price: 69.99 },
    { id: 6, name: 'Product 6', description: 'Description 6', price: 79.99 }
  ];
}

Example 3: Gallery Layout

import { Component } from '@angular/core';
import { Column, DeviceSizes } from 'column-fitter';

@Component({
  selector: 'app-image-gallery',
  template: `
    <app-column-fitter
      [columns]="galleryColumns"
      [gap]="'0.5rem'"
      [padding]="'0.5rem'"
      [backgroundColor]="'#000'"
      [minWidth]="'200px'">
      
      <div class="gallery-item" *ngFor="let image of images">
        <img [src]="image.url" [alt]="image.alt" />
        <div class="overlay">
          <h4>{{ image.title }}</h4>
        </div>
      </div>
      
    </app-column-fitter>
  `,
  styles: [`
    .gallery-item {
      position: relative;
      overflow: hidden;
      border-radius: 4px;
      aspect-ratio: 1;
    }
    
    .gallery-item img {
      width: 100%;
      height: 100%;
      object-fit: cover;
      transition: transform 0.3s ease;
    }
    
    .gallery-item:hover img {
      transform: scale(1.1);
    }
    
    .overlay {
      position: absolute;
      bottom: 0;
      left: 0;
      right: 0;
      background: linear-gradient(transparent, rgba(0,0,0,0.8));
      color: white;
      padding: 1rem;
      transform: translateY(100%);
      transition: transform 0.3s ease;
    }
    
    .gallery-item:hover .overlay {
      transform: translateY(0);
    }
  `]
})
export class ImageGalleryComponent {
  galleryColumns: Column[] = [
    { device: DeviceSizes.MOBILE, columns: 2 },
    { device: DeviceSizes.MINI, columns: 3 },
    { device: DeviceSizes.TABLET, columns: 4 },
    { device: DeviceSizes.DESKTOP, columns: 6 }
  ];
  
  images = [
    { id: 1, url: 'https://picsum.photos/300/300?random=1', alt: 'Random 1', title: 'Image 1' },
    { id: 2, url: 'https://picsum.photos/300/300?random=2', alt: 'Random 2', title: 'Image 2' },
    { id: 3, url: 'https://picsum.photos/300/300?random=3', alt: 'Random 3', title: 'Image 3' },
    { id: 4, url: 'https://picsum.photos/300/300?random=4', alt: 'Random 4', title: 'Image 4' },
    { id: 5, url: 'https://picsum.photos/300/300?random=5', alt: 'Random 5', title: 'Image 5' },
    { id: 6, url: 'https://picsum.photos/300/300?random=6', alt: 'Random 6', title: 'Image 6' }
  ];
}

Example 4: Dashboard Cards

import { Component } from '@angular/core';
import { Column, DeviceSizes } from 'column-fitter';

@Component({
  selector: 'app-dashboard',
  template: `
    <div class="dashboard-container">
      <h2>Dashboard</h2>
      
      <app-column-fitter
        [columns]="dashboardColumns"
        [gap]="'1rem'"
        [padding'"
        [background]="'1remColor]="'#f8f9fa'"
        [minWidth]="'300px'">
        
        <div class="stat-card" *ngFor="let stat of statistics">
          <div class="stat-icon">
            <mat-icon>{{ stat.icon }}</mat-icon>
          </div>
          <div class="stat-content">
            <h3>{{ stat.value }}</h3>
            <p>{{ stat.label }}</p>
          </div>
        </div>
        
      </app-column-fitter>
    </div>
  `,
  styles: [`
    .dashboard-container {
      padding: 2rem;
    }
    
    .dashboard-container h2 {
      margin-bottom: 2rem;
      color: #333;
    }
    
    .stat-card {
      background: white;
      padding: 1.5rem;
      border-radius: 8px;
      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
      display: flex;
      align-items: center;
      gap: 1rem;
    }
    
    .stat-icon {
      width: 48px;
      height: 48px;
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      background: #e3f2fd;
      color: #1976d2;
    }
    
    .stat-content h3 {
      margin: 0;
      font-size: 1.5rem;
      font-weight: bold;
      color: #333;
    }
    
    .stat-content p {
      margin: 0;
      color: #666;
      font-size: 0.9rem;
    }
  `]
})
export class DashboardComponent {
  dashboardColumns: Column[] = [
    { device: DeviceSizes.MOBILE, columns: 1 },
    { device: DeviceSizes.TABLET, columns: 2 },
    { device: DeviceSizes.MINI, columns: 2 },
    { device: DeviceSizes.DESKTOP, columns: 4 }
  ];
  
  statistics = [
    { id: 1, icon: 'people', value: '1,234', label: 'Total Users' },
    { id: 2, icon: 'shopping_cart', value: '$12,345', label: 'Revenue' },
    { id: 3, icon: 'trending_up', value: '98.5%', label: 'Growth Rate' },
    { id: 4, icon: 'assignment', value: '567', label: 'Tasks Completed' },
    { id: 5, icon: 'star', value: '4.8/5', label: 'Customer Rating' },
    { id: 6, icon: 'notifications', value: '23', label: 'Pending Alerts' }
  ];
}

Component API

Inputs

| Input | Type | Description | Default | | :--- | :--- | :--- | :--- | | padding | string | Padding for the grid container (CSS padding value) | '' | | margin | string | Margin for the grid container (CSS margin value) | '' | | backgroundColor | string | Background color for the grid container | '' | | minWidth | string | Minimum width for auto-fit columns (CSS length value) | '' | | gap | string | Gap between grid items (CSS gap value) | '1rem' | | columns | number \| Column[] | Column configuration - fixed number or device-specific array | 0 |

Dynamic Properties

| Property | Type | Description | |----------|------|-------------| | gridColumns | string | Current CSS grid-template-columns value | | hasColumns | boolean | Whether valid column configuration exists | | subscriptions | Subscription | RxJS subscription management |


Model Structures

DeviceSizes Enum

export enum DeviceSizes {
  DESKTOP = 'desktop',    // Desktop/large screens
  TABLET = 'tablet',      // Tablet devices
  MINI = 'mini',          // Small tablets/large phones
  MOBILE = 'mobile'       // Mobile phones
}

Column Interface

export interface ColumnInterface {
  device: DeviceSizes;    // Target device type
  columns: number;        // Number of columns for this device
}

Column Class

export class Column implements ColumnInterface {
  constructor(
    public device = DeviceSizes.DESKTOP,
    public columns = 0,
  ) {}

  static adapt(item?: any): Column {
    return new Column(
      item?.device,
      item?.columns
    );
  }
}

Usage Examples

// Device-specific column configurations
const responsiveColumns: Column[] = [
  new Column(DeviceSizes.MOBILE, 1),    // 1 column on mobile
  new Column(DeviceSizes.MINI, 2),      // 2 columns on mini devices
  new Column(DeviceSizes.TABLET, 3),    // 3 columns on tablets
  new Column(DeviceSizes.DESKTOP, 4)    // 4 columns on desktop
];

// Using adapt method
const adaptedColumns = [
  Column.adapt({ device: DeviceSizes.MOBILE, columns: 1 }),
  Column.adapt({ device: DeviceSizes.TABLET, columns: 3 })
];

// Mixed configuration
const mixedColumns: (number | Column[])[] = [
  3, // Fixed 3 columns for all devices
  // OR
  [
    { device: DeviceSizes.MOBILE, columns: 1 },
    { device: DeviceSizes.TABLET, columns: 2 },
    { device: DeviceSizes.DESKTOP, columns: 4 }
  ]
];

Grid Layout Logic

CSS Grid Generation

The component automatically generates CSS Grid templates based on the configuration:

Fixed Column Mode

// Input: columns = 3
// Output: gridColumns = 'repeat(3, 1fr)'

// Input: columns = 0 (disabled)
// Output: gridColumns = 'repeat(auto-fit, minmax(250px, 1fr))'

Device-Specific Mode

// Device detection logic
if (device === 'desktop' && found(DeviceSizes.DESKTOP)) {
  const cols = found(DeviceSizes.DESKTOP) as Column;
  return `repeat(${cols.columns}, 1fr)`;
}

// Fallback for unmatched devices
return `repeat(auto-fit, minmax(${this.minWidth}, 1fr))`;

Device Detection Flow

  1. Screen Observer Integration: Subscribes to screenObserverService.device$
  2. Change Detection: Uses RxJS distinctUntilChanged() to prevent unnecessary updates
  3. Column Calculation: Calls getGridTemplateColumns() with current device
  4. Grid Update: Updates gridColumns property with new CSS Grid value

Module Configuration

ColumnFitterModule

No Global Configuration Required

The ColumnFitterModule does not provide a forRoot() method or global configuration options. All configuration is done at the component level through input properties.

Module Structure

@NgModule({
  declarations: [
    ColumnFitterComponent,
    ColumnFitterDemoComponent
  ],
  imports: [
    // Dependencies are imported by the consuming application
    // screen-observer must be installed separately
  ],
  exports: [
    ColumnFitterComponent,
    ColumnFitterDemoComponent
  ]
})
export class ColumnFitterModule { }

Dependencies

  • screen-observer: Device detection service (must be installed separately)
  • @angular/core: Core Angular functionality
  • rxjs: Reactive programming utilities for device change detection

Styling and Customization

CSS Grid Styling

The component uses CSS Grid with the following base styles:

:host {
  display: block;
}

.grid-container {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 1rem;
}

Custom Styling Examples

Custom Grid Appearance

// Enhanced grid styling
:host ::ng-deep .grid-container {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  border-radius: 12px;
  padding: 2rem;
  
  .grid-item {
    background: white;
    border-radius: 8px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    transition: transform 0.2s ease, box-shadow 0.2s ease;
    
    &:hover {
      transform: translateY(-2px);
      box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
    }
  }
}

Responsive Gap Adjustment

// Dynamic gaps based on device
:host ::ng-deep .grid-container {
  gap: var(--grid-gap, 1rem);
  
  @media (max-width: 768px) {
    --grid-gap: 0.5rem;
  }
  
  @media (min-width: 1200px) {
    --grid-gap: 1.5rem;
  }
}

Advanced Layout Patterns

Masonry-Style Layout

// CSS Grid with masonry-like behavior
.masonry-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  grid-auto-rows: 200px;
  gap: 1rem;
  
  .masonry-item {
    grid-row-end: span var(--row-span, 1);
    
    &.large {
      --row-span: 2;
    }
    
    &.wide {
      grid-column-end: span 2;
    }
  }
}

Card-Based Layout

// Card layout with consistent height
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 1.5rem;
  align-items: stretch;
  
  .card {
    display: flex;
    flex-direction: column;
    height: 100%;
    background: white;
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 2px 8px rgba(0,0,0,0.1);
    
    .card-image {
      height: 200px;
      overflow: hidden;
      
      img {
        width: 100%;
        height: 100%;
        object-fit: cover;
      }
    }
    
    .card-content {
      padding: 1.5rem;
      flex: 1;
      display: flex;
      flex-direction: column;
    }
  }
}

Integration Examples

With Angular Material

import { Component } from '@angular/core';
import { Column, DeviceSizes } from 'column-fitter';

@Component({
  selector: 'app-material-grid',
  template: `
    <app-column-fitter
      [columns]="materialColumns"
      [gap]="'1rem'"
      [padding]="'1rem'">
      
      <mat-card class="grid-card" *ngFor="let item of materialItems">
        <mat-card-header>
          <mat-card-title>{{ item.title }}</mat-card-title>
          <mat-card-subtitle>{{ item.subtitle }}</mat-card-subtitle>
        </mat-card-header>
        
        <img mat-card-image [src]="item.image" [alt]="item.title">
        
        <mat-card-content>
          <p>{{ item.description }}</p>
        </mat-card-content>
        
        <mat-card-actions>
          <button mat-button>LIKE</button>
          <button mat-button>SHARE</button>
        </mat-card-actions>
      </mat-card>
      
    </app-column-fitter>
  `
})
export class MaterialGridComponent {
  materialColumns: Column[] = [
    { device: DeviceSizes.MOBILE, columns: 1 },
    { device: DeviceSizes.TABLET, columns: 2 },
    { device: DeviceSizes.DESKTOP, columns: 3 }
  ];
  
  materialItems = [
    {
      title: 'Card 1',
      subtitle: 'Subtitle 1',
      description: 'Description for card 1',
      image: 'https://picsum.photos/400/200?random=1'
    },
    // ... more items
  ];
}

With Dynamic Content

import { Component } from '@angular/core';
import { Column, DeviceSizes } from 'column-fitter';

@Component({
  selector: 'app-dynamic-content',
  template: `
    <div class="controls">
      <button (click)="addItem()">Add Item</button>
      <button (click)="removeItem()">Remove Item</button>
      <select [(ngModel)]="selectedLayout" (change)="changeLayout()">
        <option value="mobile1">Mobile: 1 Col</option>
        <option value="tablet3">Tablet: 3 Col</option>
        <option value="desktop4">Desktop: 4 Col</option>
      </select>
    </div>
    
    <app-column-fitter
      [columns]="currentColumns"
      [gap]="'1rem'"
      [padding]="'1rem'">
      
      <div class="dynamic-item" *ngFor="let item of dynamicItems; trackBy: trackById">
        <h3>{{ item.title }}</h3>
        <p>{{ item.content }}</p>
        <small>ID: {{ item.id }}</small>
      </div>
      
    </app-column-fitter>
  `
})
export class DynamicContentComponent {
  currentColumns: Column[] = [
    { device: DeviceSizes.MOBILE, columns: 1 },
    { device: DeviceSizes.TABLET, columns: 3 },
    { device: DeviceSizes.DESKTOP, columns: 4 }
  ];
  
  selectedLayout = 'tablet3';
  dynamicItems = [
    { id: 1, title: 'Item 1', content: 'Content 1' },
    { id: 2, title: 'Item 2', content: 'Content 2' },
    { id: 3, title: 'Item 3', content: 'Content 3' }
  ];
  
  addItem() {
    const newItem = {
      id: Date.now(),
      title: `Item ${this.dynamicItems.length + 1}`,
      content: `Content ${this.dynamicItems.length + 1}`
    };
    this.dynamicItems = [...this.dynamicItems, newItem];
  }
  
  removeItem() {
    if (this.dynamicItems.length > 0) {
      this.dynamicItems = this.dynamicItems.slice(0, -1);
    }
  }
  
  changeLayout() {
    switch (this.selectedLayout) {
      case 'mobile1':
        this.currentColumns = [
          { device: DeviceSizes.MOBILE, columns: 1 },
          { device: DeviceSizes.TABLET, columns: 1 },
          { device: DeviceSizes.DESKTOP, columns: 1 }
        ];
        break;
      case 'tablet3':
        this.currentColumns = [
          { device: DeviceSizes.MOBILE, columns: 1 },
          { device: DeviceSizes.TABLET, columns: 3 },
          { device: DeviceSizes.DESKTOP, columns: 3 }
        ];
        break;
      case 'desktop4':
        this.currentColumns = [
          { device: DeviceSizes.MOBILE, columns: 1 },
          { device: DeviceSizes.TABLET, columns: 2 },
          { device: DeviceSizes.DESKTOP, columns: 4 }
        ];
        break;
    }
  }
  
  trackById(index: number, item: any): any {
    return item.id;
  }
}

Performance Optimization

Change Detection

The component uses several performance optimizations:

  1. distinctUntilChanged(): Prevents duplicate device updates
  2. Efficient Grid Calculation: Only recalculates when device actually changes
  3. Subscription Management: Properly cleans up RxJS subscriptions

Memory Management

ngOnDestroy() {
  // Clean up subscriptions to prevent memory leaks
  this.subscriptions.unsubscribe();
}

Large Dataset Handling

// Use trackBy for large lists
@Component({
  template: `
    <app-column-fitter [columns]="columns">
      <div *ngFor="let item of largeDataset; trackBy: trackById">
        {{ item.name }}
      </div>
    </app-column-fitter>
  `
})
export class LargeDatasetComponent {
  trackById(index: number, item: any): any {
    return item.id; // Use unique identifier
  }
}

Testing

Unit Testing Example

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ColumnFitterComponent } from './column-fitter.component';
import { Column, DeviceSizes } from './models/column.model';
import { ScreenObserverService } from 'screen-observer';

describe('ColumnFitterComponent', () => {
  let component: ColumnFitterComponent;
  let fixture: ComponentFixture<ColumnFitterComponent>;
  let mockScreenObserverService: jasmine.SpyObj<ScreenObserverService>;

  beforeEach(async () => {
    const screenObserverSpy = jasmine.createSpyObj('ScreenObserverService', ['device$']);
    
    await TestBed.configureTestingModule({
      declarations: [ ColumnFitterComponent ],
      providers: [
        { provide: ScreenObserverService, useValue: screenObserverSpy }
      ]
    }).compileComponents();

    fixture = TestBed.createComponent(ColumnFitterComponent);
    component = fixture.componentInstance;
    mockScreenObserverService = TestBed.inject(ScreenObserverService) as jasmine.SpyObj<ScreenObserverService>;
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should set fixed columns correctly', () => {
    component.columns = 3;
    fixture.detectChanges();
    
    const result = component.getGridTemplateColumns('desktop');
    expect(result).toBe('repeat(3, 1fr)');
  });

  it('should handle device-specific columns', () => {
    component.columns = [
      { device: DeviceSizes.MOBILE, columns: 1 },
      { device: DeviceSizes.TABLET, columns: 3 },
      { device: DeviceSizes.DESKTOP, columns: 4 }
    ];
    
    fixture.detectChanges();
    
    expect(component.getGridTemplateColumns('mobile')).toBe('repeat(1, 1fr)');
    expect(component.getGridTemplateColumns('tablet')).toBe('repeat(3, 1fr)');
    expect(component.getGridTemplateColumns('desktop')).toBe('repeat(4, 1fr)');
  });

  it('should fallback to auto-fit when no matching device', () => {
    component.columns = [
      { device: DeviceSizes.MOBILE, columns: 1 }
    ];
    component.minWidth = '200px';
    
    fixture.detectChanges();
    
    const result = component.getGridTemplateColumns('desktop');
    expect(result).toBe('repeat(auto-fit, minmax(200px, 1fr))');
  });

  it('should handle disabled state (columns = 0)', () => {
    component.columns = 0;
    component.minWidth = '250px';
    
    fixture.detectChanges();
    
    const result = component.getGridTemplateColumns('desktop');
    expect(result).toBe('repeat(auto-fit, minmax(250px, 1fr))');
  });
});

Troubleshooting

Common Issues

  1. No columns showing: Ensure screen-observer package is installed and configured
  2. Layout not updating: Check that device$ observable is emitting values
  3. Styling issues: Verify CSS Grid is supported in target browsers
  4. Performance issues: Consider using OnPush change detection for large datasets

Debug Mode

@Component({
  template: `
    <div class="debug-info">
      Current Device: {{ currentDevice }}<br>
      Grid Columns: {{ gridColumns }}<br>
      Has Columns: {{ hasColumns }}<br>
      Columns Config: {{ columns | json }}
    </div>
    
    <app-column-fitter
      [columns]="columns"
      [gap]="gap"
      [minWidth]="minWidth">
      <!-- Content -->
    </app-column-fitter>
  `
})
export class DebugColumnFitterComponent {
  currentDevice = '';
  gridColumns = '';
  hasColumns = false;
  columns: any = [];
  gap = '1rem';
  minWidth = '250px';
  
  constructor() {
    // Add debugging logic
  }
}

Performance Monitoring

ngOnInit() {
  const start = performance.now();
  
  this.subscriptions.add(
    this.screenObserverService.device$.subscribe((screen: string) => {
      const updateStart = performance.now();
      this.gridColumns = this.getGridTemplateColumns(screen);
      const updateEnd = performance.now();
      
      console.log(`Grid update took ${updateEnd - updateStart}ms for device: ${screen}`);
    })
  );
  
  const initEnd = performance.now();
  console.log(`ColumnFitter initialization took ${initEnd - start}ms`);
}

Browser Support

CSS Grid Support

The component requires CSS Grid support, which is available in:

  • Chrome: 57+ (March 2017)
  • Firefox: 52+ (March 2017)
  • Safari: 10.1+ (March 2017)
  • Edge: 16+ (October 2017)

Fallback for Older Browsers

// CSS Grid fallback using Flexbox
.grid-container {
  display: flex;
  flex-wrap: wrap;
  margin: -0.5rem;
  
  .grid-item {
    flex: 1 1 250px; /* Minimum width of 250px */
    margin: 0.5rem;
    
    @supports (display: grid) {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
      margin: 0;
    }
  }
}

Progressive Enhancement

// JavaScript fallback for older browsers
if (!CSS.supports('display', 'grid')) {
  // Apply Flexbox fallback
  this.applyFlexboxFallback();
}