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

chintanonweb - Sep 29 '23 - - Dev Community

Introduction

In modern web applications, presenting data in tables is a common practice. However, sometimes, users need the flexibility to customize their view, especially when dealing with large datasets. One common requirement is the ability to resize columns in a table. This can enhance user experience and make the application more user-friendly.

In this article, we will explore how to create resizable columns in Angular, a popular web application framework. We'll achieve this functionality by creating a custom Angular directive called columnResize. We'll walk through the process step by step, from creating the directive to integrating it into an Angular component.

Table of Contents

  1. Understanding the Problem
  2. Creating the Column Resize Directive
    • Initializing Directive Properties
    • Handling Mouse Events
    • Resizing Columns
    • Handling Edge Cases
  3. Using the Directive in an Angular Component
  4. Styling and Customization
  5. Frequently Asked Questions (FAQs)
  6. Conclusion

1. Understanding the Problem

Before diving into the code, let's understand the problem we're trying to solve. In a typical web application, when we display data in a table, each column has a fixed width. This fixed width can become problematic when dealing with different screen sizes or when users want to focus on specific columns.

To address this issue, we want to allow users to adjust the width of columns by dragging the column borders. This behavior should be intuitive, similar to how you would resize columns in spreadsheet applications like Microsoft Excel.

2. Creating the Column Resize Directive

Initializing Directive Properties

We'll start by creating our Angular directive, columnResize. This directive will be responsible for enabling column resizing. We'll use Angular's Renderer2 to manipulate the DOM elements. Here's how we initialize the directive's properties:

// column-resize.directive.ts

import { Directive, ElementRef, HostListener, Renderer2 } from '@angular/core';

@Directive({
  selector: '[columnResize]'
})
export class ColumnResizeDirective {
  private startX!: number;
  private isResizing = false;
  private initialWidth!: number;
  private columnIndex!: number;
  private table!: HTMLElement | null = null;

  constructor(private el: ElementRef, private renderer: Renderer2) {}
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Handling Mouse Events

To enable column resizing, we need to handle mouse events like mousedown, mousemove, and mouseup. When a user clicks on a column border (mousedown), we'll start tracking the mouse movement and calculate the new column width.

@HostListener('mousedown', ['$event'])
onMouseDown(event: MouseEvent) {
  event.preventDefault();
  this.startX = event.pageX;
  this.isResizing = true;
  this.initialWidth = this.el.nativeElement.offsetWidth;
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Resizing Columns

Inside the onMouseMove event listener, we calculate the new width of the column and update its style. Additionally, we update the corresponding header and cell widths to maintain consistency.

const onMouseMove = (moveEvent: MouseEvent) => {
  if (this.isResizing) {
    const deltaX = moveEvent.pageX - this.startX;
    const newWidth = this.initialWidth + deltaX;

    // Update the width of the current column
    this.renderer.setStyle(this.el.nativeElement, 'width', newWidth + 'px');

    // Update the width of the corresponding header and cell in each row
    columns[this.columnIndex].style.width = newWidth + 'px';
    const rows = this.table.querySelectorAll('tr');
    rows.forEach((row) => {
      const cells = row.querySelectorAll('td');
      if (cells[this.columnIndex]) {
        cells[this.columnIndex].style.width = newWidth + 'px';
      }
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

Handling Edge Cases

We also handle edge cases, such as when the table has a fixed width. In this case, we adjust the table's width accordingly to prevent horizontal scrolling issues.

// Adjust the width of the table if it has a fixed width
const tableWidth = this.table.offsetWidth;
if (tableWidth > 0) {
  this.renderer.setStyle(this.table, 'width', tableWidth + deltaX + 'px');
}
Enter fullscreen mode Exit fullscreen mode

Here is the full code

// column-resize.directive.ts

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

@Directive({
  selector: '[columnResize]'
})
export class ColumnResizeDirective implements OnInit, OnDestroy {

  private startX!: number;
  private isResizing = false;
  private initialWidth!: number;
  private columnIndex!: number;
  private table: HTMLElement | null = null;
  private tableWidth: number | null = null;

  private onDestroy$ = new Subject<void>();

  constructor(private elementRef: ElementRef, private renderer: Renderer2) { }

  ngOnInit(): void {
    const nativeElement = this.elementRef?.nativeElement as HTMLElement;
    const mousedown = fromEvent<MouseEvent>(nativeElement, 'mousedown');
    mousedown.pipe(takeUntil(this.onDestroy$), distinctUntilChanged())
      .subscribe(event => this.onMouseDown(event));
    // prevent click event
    fromEvent<MouseEvent>(nativeElement, 'click').pipe(takeUntil(this.onDestroy$), distinctUntilChanged())
      .subscribe(event => event.stopPropagation());
  }

  private onMouseDown(event: MouseEvent) {
    event.preventDefault();
    this.startX = event?.pageX;
    this.isResizing = true;
    this.initialWidth = this.elementRef?.nativeElement?.offsetWidth;

    // Find the index of the current column
    const row = this.elementRef?.nativeElement?.parentElement;
    const cells = Array.from(row?.children);
    this.columnIndex = cells.indexOf(this.elementRef?.nativeElement);

    this.renderer.addClass(this.elementRef?.nativeElement, 'resizing-col');
    // this.renderer.addClass(document.body, 'resizing');

    this.table = this.findParentTable(this.elementRef.nativeElement);

    if (this.table) {
      this.renderer.addClass(this.table, 'table-resizing');
      const columns = this.table.querySelectorAll('th');
      const tableWidth = this.table?.offsetWidth;

      const onMouseMove = (moveEvent: MouseEvent) => {
        if (this.isResizing) {
          const deltaX = moveEvent?.pageX - this.startX;
          const newWidth = this.initialWidth + deltaX;
          this.tableWidth = !this.tableWidth && this.table?.offsetWidth ? this.table?.offsetWidth / 2 : this.tableWidth;
          // Restrict the column width to a minimum of 40 and a maximum 50% of table or 350 pixels
          if (newWidth >= 40 && newWidth <= (this.tableWidth || 350)) {

            // Update the width of the current column
            this.renderer.setStyle(this.elementRef?.nativeElement, 'width', `${newWidth}px`);

            // Update the width of the corresponding header and cell in each row
            columns[this.columnIndex].style.width = `${newWidth}px`;
            const rows = this.table?.querySelectorAll('tr');
            rows?.forEach((row) => {
              const cells = row.querySelectorAll('td');
              if (cells[this.columnIndex]) {
                cells[this.columnIndex].style.width = `${newWidth}px`;
              }
            });

            // Adjust the width of the table if it has a fixed width
            if (tableWidth > 0) {
              this.renderer.setStyle(this.table, 'width', tableWidth + deltaX + 'px');
            }
          }
        }
      };

      const onMouseUp = () => {
        this.isResizing = false;
        this.renderer.removeClass(this.elementRef?.nativeElement, 'resizing-col');
        if (this.table) this.renderer.removeClass(this.table, 'table-resizing');
        document.removeEventListener('mousemove', onMouseMove);
        document.removeEventListener('mouseup', onMouseUp);
      };

      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('mouseup', onMouseUp);
    }
  }

  private findParentTable(element: HTMLElement): HTMLElement | null {
    while (element) {
      if (element.tagName === 'TABLE') {
        return element;
      }
      if (element?.parentElement) element = element?.parentElement;
    }
    return null;
  }

  ngOnDestroy(): void {
    // we've destroyed the component, so update the subject
    this.onDestroy$.next();
    this.onDestroy$.complete();
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Using the Directive in an Angular Component

With the columnResize directive in place, you can use it in your Angular components. Here's an example of how to integrate it into a component:

// table-resize.component.ts

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

@Component({
  selector: 'app-table-resize',
  template: `
    <table>
      <thead>
        <tr>
          <th columnResize>Column 1</th>
          <th columnResize>Column 2</th>
          <th columnResize>Column 3</th>
          <!-- Add more columns as needed -->
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>Data 1</td>
          <td>Data 2</td>
          <td>Data 3</td>
          <!-- Add more cells as needed -->
        </tr>
        <!-- Add more rows as needed -->
      </tbody>
    </table>
  `,
  styleUrls: ['./table-resize.component.css']
})
export class TableResizeComponent {}
Enter fullscreen mode Exit fullscreen mode

In this example, we've added the columnResize directive to the th elements of the table header. You can use it in any table where you want to enable column resizing.

4. Styling and Customization

To enhance the user experience, you can add CSS styles to indicate that columns are resizable when the user hovers over the column borders. Additionally, you can customize the appearance of the resizing handle. Here's a simple CSS example:

/* table.component.html */
<div class="dc-table" [class.header-s]="headerSize=='s'" [class.table-paginator]="!paginator">
  <ng-content selector=".header-row"></ng-content>
  <ng-content selector=".table"></ng-content>
</div>
Enter fullscreen mode Exit fullscreen mode
/* Add this to your component's CSS or global styles */
app-table {
  .dc-table {
    border-radius: 8px;
    outline: 1px solid var(--grayscale-c5);
    border-spacing: inherit;
    overflow: hidden;

    table.mat-table {
      min-width: 100%;
    }
   }
}

.resizable-table {
  overflow: auto;

  app-table {
    .dc-table {
      overflow: auto;
    }
  }
}
      th[columnResize] {
        &:hover {
          background: var(--grayscale-c3) !important;
          position: relative;

          &::after {
            border-right: 2px solid var(--primary-c6);
            content: '';
            position: absolute;
            top: 0;
            right: 0px;
            bottom: 0;
            width: 10px;
            cursor: var(--col-resize-svg);
          }
        }

        &.resizing-col {
          background: var(--grayscale-c3) !important;
          border-right: 2px solid var(--primary-c6);
        }
      }
Enter fullscreen mode Exit fullscreen mode

Feel free to adjust the styles to match your application's design.

5. Frequently Asked Questions (FAQs)

Q1: Can I use this directive in tables with horizontal scrolling?

Yes, the columnResize directive is designed to work with both regular tables and horizontally scrollable tables. It adjusts the table's width as needed to prevent issues with horizontal scrolling.

Q2: How can I prevent resizing certain columns?

If you want to prevent resizing for specific columns, you can conditionally apply the columnResize directive based on your application's logic. For example, you can use *ngIf to conditionally include the directive on certain th elements.

6. Conclusion

In this article, we've learned how to create resizable columns in Angular using a custom directive called columnResize. By following the step-by-step guide, you can enhance the user experience of your web application when dealing with tables

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