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
- Understanding the Problem
-
Creating the Column Resize Directive
- Initializing Directive Properties
- Handling Mouse Events
- Resizing Columns
- Handling Edge Cases
- Using the Directive in an Angular Component
- Styling and Customization
- Frequently Asked Questions (FAQs)
- 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) {}
// ...
}
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;
// ...
}
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';
}
});
}
};
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');
}
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();
}
}
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 {}
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>
/* 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