Mastering Resizable Columns in Angular Table: A Step-by-Step Guide for Developers

chintanonweb - Oct 15 - - Dev Community

How to Create Resizable Columns in Angular Table: A Step-by-Step Guide

Angular Material tables provide a sleek way to display data. However, users often want additional functionality like the ability to resize table columns for better control over data display. In this guide, we'll walk through the process of creating resizable columns in an Angular table using a custom directive. You'll learn how to set up the directive, style the resizer, and implement column resizing step-by-step.

Introduction

Adding resizable columns to an Angular Material table involves creating a custom directive that listens to mouse events, allowing users to click and drag a column to adjust its width. This gives users flexibility, especially when dealing with large datasets, improving the user experience.

In this tutorial, we will:

  • Create a custom column resize directive.
  • Handle mouse events to resize columns.
  • Apply styles for a smooth user experience.
  • Attach the directive to an Angular Material table.

Let’s dive into it.

Step 1: Setting Up the Angular Material Table

First, ensure that your Angular project has Angular Material installed. If not, run the following command to add Angular Material to your project:

ng add @angular/material
Enter fullscreen mode Exit fullscreen mode

Once Angular Material is installed, you can create a basic table using the following code.

HTML for the Table:

<div class="resizable-table">
  <table mat-table [dataSource]="dataSource">
    <ng-container *ngFor="let column of displayedColumns" [matColumnDef]="column">
      <th mat-header-cell *matHeaderCellDef appColumnResize>{{ column }}</th>
      <td mat-cell *matCellDef="let element">{{ element[column] }}</td>
    </ng-container>
    <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
    <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
  </table>
</div>
Enter fullscreen mode Exit fullscreen mode

Here, we use mat-table from Angular Material to display a simple table. The appColumnResize directive is applied to the th (header) elements to make columns resizable.

Data for the Table:

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

export interface PeriodicElement {
  name: string;
  position: number;
  weight: number;
  symbol: string;
}

const ELEMENT_DATA: PeriodicElement[] = [
  { position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H' },
  { position: 2, name: 'Helium', weight: 4.0026, symbol: 'He' },
  // ... add more data
];

@Component({
  selector: 'table-basic-example',
  styleUrls: ['table-basic-example.scss'],
  templateUrl: 'table-basic-example.html',
  encapsulation: ViewEncapsulation.None,
})
export class TableBasicExample {
  displayedColumns: string[] = ['position', 'name', 'weight', 'symbol'];
  dataSource = ELEMENT_DATA;
}
Enter fullscreen mode Exit fullscreen mode

The component includes data for the periodic elements, which we will display in the table.

Step 2: Creating the Column Resize Directive

Next, we’ll implement a custom Angular directive that enables resizing functionality for the table columns.

Directive Implementation:

import {
  Directive,
  ElementRef,
  Renderer2,
  NgZone,
  Input,
  OnInit,
  OnDestroy,
} from '@angular/core';
import { fromEvent, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

@Directive({
  selector: '[appColumnResize]',
})
export class ColumnResizeDirective implements OnInit, OnDestroy {
  @Input() resizableTable: HTMLElement | null = null;

  private startX!: number;
  private startWidth!: number;
  private isResizing = false;
  private column: HTMLElement;
  private resizer!: HTMLElement;
  private destroy$ = new Subject<void>();

  constructor(
    private el: ElementRef,
    private renderer: Renderer2,
    private zone: NgZone
  ) {
    this.column = this.el.nativeElement;
  }

  ngOnInit() {
    this.createResizer();
    this.initializeResizeListener();
  }

  private createResizer() {
    this.resizer = this.renderer.createElement('div');
    this.renderer.addClass(this.resizer, 'column-resizer');
    this.renderer.setStyle(this.resizer, 'position', 'absolute');
    this.renderer.setStyle(this.resizer, 'right', '0');
    this.renderer.setStyle(this.resizer, 'top', '0');
    this.renderer.setStyle(this.resizer, 'width', '5px');
    this.renderer.setStyle(this.resizer, 'cursor', 'col-resize');
    this.renderer.appendChild(this.column, this.resizer);
  }

  private initializeResizeListener() {
    this.zone.runOutsideAngular(() => {
      fromEvent(this.resizer, 'mousedown')
        .pipe(takeUntil(this.destroy$))
        .subscribe((event: MouseEvent) => this.onMouseDown(event));

      fromEvent(document, 'mousemove')
        .pipe(takeUntil(this.destroy$))
        .subscribe((event: MouseEvent) => this.onMouseMove(event));

      fromEvent(document, 'mouseup')
        .pipe(takeUntil(this.destroy$))
        .subscribe(() => this.onMouseUp());
    });
  }

  private onMouseDown(event: MouseEvent) {
    event.preventDefault();
    this.isResizing = true;
    this.startX = event.pageX;
    this.startWidth = this.column.offsetWidth;
  }

  private onMouseMove(event: MouseEvent) {
    if (!this.isResizing) return;
    const width = this.startWidth + (event.pageX - this.startX);
    this.renderer.setStyle(this.column, 'width', `${width}px`);
  }

  private onMouseUp() {
    if (!this.isResizing) return;
    this.isResizing = false;
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • createResizer(): Adds a resizer element (div) to the column header.
  • onMouseDown(): Triggered when the user clicks the resizer, recording the initial position.
  • onMouseMove(): Updates the column width as the user drags the resizer.
  • onMouseUp(): Ends the resizing when the user releases the mouse button.

Step 3: Styling the Resizer

We need to style the resizer so users know it's draggable. Add the following CSS to your styles:

.resizable-table {
  th {
    position: relative;

    .column-resizer {
      position: absolute;
      top: 0;
      right: 0;
      bottom: 0;
      width: 10px;
      cursor: col-resize;
      z-index: 1;

      &:hover {
        border-right: 2px solid red;
      }
    }

    &.resizing {
      user-select: none;
    }
  }

  &.resizing {
    cursor: col-resize;
    user-select: none;
  }
}
Enter fullscreen mode Exit fullscreen mode

This CSS positions the resizer correctly, adds a hover effect, and changes the cursor to indicate resizability.

Step 4: Testing the Table

Now that the directive and styles are in place, try resizing the columns. You should be able to click on the resizer, drag it left or right, and dynamically adjust the width of each column.

FAQs

Q: What happens if the resizable table is too wide?

A: The table will overflow and adjust based on the container's width. Ensure you add proper scroll behavior or container adjustments to handle large tables.

Q: Can I make specific columns non-resizable?

A: Yes, you can conditionally apply the appColumnResize directive to only specific columns by using Angular's built-in structural directives like *ngIf.

Q: Is this approach performance-friendly for large tables?

A: This solution works well for moderate-sized tables. However, for extremely large datasets, you may want to optimize further by using Angular's change detection strategy or a virtual scrolling mechanism.

Conclusion

By following this guide, you now have a fully functional resizable column feature for your Angular Material tables. This customization enhances the flexibility and usability of your tables, providing a better user experience. Happy coding!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .