How to Create Resizable Columns in Angular Table: A Step-by-Step Guide
Tags: angular, javascript, tutorial, beginners
Published: 2023-09-29 07:50:06

## 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:

```typescript
// 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) {}
  // ...
}
```

### 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.

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

### 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.

```typescript
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';
      }
    });
  }
};
```

### 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.

```typescript
// 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');
}
```

Here is the full code 
```typescript
// 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();
  }
}
```

## 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:

```typescript
// 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 {}
```

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:

```html
/* 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>
```

```css
/* 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);
        }
      }
```

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