Skip to content

Instantly share code, notes, and snippets.

@MirzaLeka
Last active March 24, 2024 19:43
Show Gist options
  • Save MirzaLeka/d695e9062ec079fa8f37bff469d51d84 to your computer and use it in GitHub Desktop.
Save MirzaLeka/d695e9062ec079fa8f37bff469d51d84 to your computer and use it in GitHub Desktop.

Angular 17 Material Resizable Table (Standalone components)

Credit:

Creating component

resizable-table.component.html

<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" cdkDropListGroup>
    <ng-container *ngFor="let column of columns; let i = index" [matColumnDef]="column.field">
      <th mat-header-cell *matHeaderCellDef>
        {{ column.field }}
        <span class="resize-handle" (mousedown)="onResizeColumn($event, i)"></span>
      </th>
      <td mat-cell *matCellDef="let row">
          
        <!--  Formatting numbers using Pipes -->

        <ng-container *ngIf="isNumber(row[column.field]); else textData">
          {{ row[column.field] | number : '1.2-2' }}
        </ng-container>
        <ng-template #textData>
          {{ row[column.field] }}
        </ng-template>

      </td>

    </ng-container>
    <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
    <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
  </table>

resizable-table.component.css

table {
    width: 100%;
  }

  table th {
    position: relative;
  }

  table th:hover .resize-handle {
    opacity: 1;
    transition: .3s ease-in-out;
  }

  .resize-handle {
    display: inline-block;
    border-right: 2px solid lightgray;
    position: absolute;
    top: 0;
    right: 0;
    height: 100%;
    cursor: col-resize;
    opacity: 0;
  }

  .resize-handle:hover {
    width: 20px;
  }

resizable-table.component.ts (works with Angular SSR)

import { CommonModule } from '@angular/common';
import { AfterViewInit, Component, ElementRef, HostListener, Inject, OnInit, Renderer2, ViewChild } from '@angular/core';
import { MatTable, MatTableModule } from '@angular/material/table';
import { PLATFORM_ID } from "@angular/core";
import { isPlatformBrowser } from '@angular/common';

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'},
 {position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li'},
 {position: 4, name: 'Beryllium', weight: 9.0122, symbol: 'Be'},
 {position: 5, name: 'Boron', weight: 10.811, symbol: 'B'},
 {position: 6, name: 'Carbon', weight: 12.0107, symbol: 'C'},
 {position: 7, name: 'Nitrogen', weight: 14.0067, symbol: 'N'},
 {position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O'},
 {position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F'},
 {position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne'},
];

@Component({
 selector: 'app-resizable-table',
 standalone: true,
 imports: [MatTableModule, CommonModule],
 templateUrl: './resizable-table.component.html',
 styleUrl: './resizable-table.component.scss'
})
export class ResizableTableComponent implements OnInit, AfterViewInit {
 title = 'Material Table column Resize';
 @ViewChild(MatTable, {read: ElementRef} ) private matTableRef!: ElementRef;

 columns: any[] = [
   { field: 'position', width: 20,  },
   { field: 'name', width: 20, },
   { field: 'weight', width: 20, },
   { field: 'symbol', width: 20, }
 ];
 displayedColumns: string[] = [];
 dataSource = ELEMENT_DATA;

 pressed = false;
 currentResizeIndex!: number;
 startX!: number;
 startWidth!: number;
 isResizingRight!: boolean;
 resizableMousemove!: () => void;
 resizableMouseup!: () => void;

 constructor(
   private renderer: Renderer2,
   // validate platformID for SSR
   @Inject(PLATFORM_ID) private platformId: any,
 ) { }

 ngOnInit() {
   this.setDisplayedColumns();
 }

 ngAfterViewInit() {
   this.setTableResize(this.matTableRef.nativeElement.clientWidth);
 }
 
 isNumber(value: any): boolean {
   return typeof value === 'number';
 }


 setTableResize(tableWidth: number) {
   let totWidth = 0;
   this.columns.forEach(( column) => {
     totWidth += column.width;
   });
   const scale = (tableWidth - 5) / totWidth;
   this.columns.forEach(( column) => {
     column.width *= scale;
     this.setColumnWidth(column);
   });
 }

 setDisplayedColumns() {
   this.columns.forEach(( column, index) => {
     column.index = index;
     this.displayedColumns[index] = column.field;
   });
 }

 onResizeColumn(event: any, index: number) {
   console.log(event.target.parentElement);
   this.checkResizing(event, index);
   this.currentResizeIndex = index;
   this.pressed = true;
   this.startX = event.pageX;
   this.startWidth = event.target.parentElement.clientWidth;
   event.preventDefault();
   this.mouseMove(index);
 }

 private checkResizing(event: any, index: any) {
   const cellData = this.getCellData(index);
   if ( ( index === 0 ) || ( Math.abs(event.pageX - cellData.right) < cellData.width / 2 &&  index !== this.columns.length - 1 ) ) {
     this.isResizingRight = true;
   } else {
     this.isResizingRight = false;
   }
 }

 private getCellData(index: number) {
   const headerRow = this.matTableRef.nativeElement.children[0].querySelector('tr');
   const cell = headerRow.children[index];
   return cell.getBoundingClientRect();
 }

 mouseMove(index: number) {
   this.resizableMousemove = this.renderer.listen('document', 'mousemove', (event) => {
     if (this.pressed && event.buttons ) {
       const dx = (this.isResizingRight) ? (event.pageX - this.startX) : (-event.pageX + this.startX);
       const width = this.startWidth + dx;
       if ( this.currentResizeIndex === index && width > 50 ) {
         this.setColumnWidthChanges(index, width);
       }
     }
   });
   this.resizableMouseup = this.renderer.listen('document', 'mouseup', (event) => {
     if (this.pressed) {
       this.pressed = false;
       this.currentResizeIndex = -1;
       this.resizableMousemove();
       this.resizableMouseup();
     }
   });
 }

 setColumnWidthChanges(index: number, width: number) {
   const orgWidth = this.columns[index].width;
   const dx = width - orgWidth;
   if ( dx !== 0 ) {
     const j = ( this.isResizingRight ) ? index + 1 : index - 1;
     const newWidth = this.columns[j].width - dx;
     if ( newWidth > 50 ) {
         this.columns[index].width = width;
         this.setColumnWidth(this.columns[index]);
         this.columns[j].width = newWidth;
         this.setColumnWidth(this.columns[j]);
       }
   }
 }

 setColumnWidth(column: any) {
   // makes it resolve "document not defined" error that occurs when rendering apps in SSR
   if(!isPlatformBrowser(this.platformId)) return;

   const columnEls = Array.from( document.getElementsByClassName('mat-column-' + column.field) );
   columnEls.forEach(( el: any ) => {
     el.style.width = column.width + 'px';
   });
 }

 @HostListener('window:resize', ['$event'])
 onResize(event: any) {
   this.setTableResize(this.matTableRef.nativeElement.clientWidth);
 }
}

Injecting into App Component

app.component.ts

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ResizableTableComponent],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss'
})
export class AppComponent {

}

app.component.html

<app-resizable-table/>
@MirzaLeka
Copy link
Author

Expandable rows:

Angular (forked) - StackBlitz
https://stackblitz.com/edit/angular-35wzca?file=main.ts

@MirzaLeka
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment