From 75c8aa8767d1bb5e2e5f29ea0434a5876a2c5a21 Mon Sep 17 00:00:00 2001 From: Karl Seamon Date: Tue, 10 Dec 2024 03:55:22 -0500 Subject: [PATCH] feat(cdk-experimental/column-resize): Support column size persistance hooks (#30136) --- .../column-resize/column-resize.ts | 8 +- .../column-resize/column-size-store.ts | 5 +- .../column-resize/resizable.ts | 29 ++++- .../column-resize/column-resize.spec.ts | 109 +++++++++++++++++- .../column-resize/public-api.ts | 2 +- 5 files changed, 142 insertions(+), 11 deletions(-) diff --git a/src/cdk-experimental/column-resize/column-resize.ts b/src/cdk-experimental/column-resize/column-resize.ts index bf83a770315d..5837a4da7fbb 100644 --- a/src/cdk-experimental/column-resize/column-resize.ts +++ b/src/cdk-experimental/column-resize/column-resize.ts @@ -44,7 +44,6 @@ export const COLUMN_RESIZE_OPTIONS = new InjectionToken( */ @Directive() export abstract class ColumnResize implements AfterViewInit, OnDestroy { - private _idGenerator = inject(_IdGenerator); protected readonly destroyed = new Subject(); /* Publicly accessible interface for triggering and being notified of resizes. */ @@ -58,7 +57,7 @@ export abstract class ColumnResize implements AfterViewInit, OnDestroy { protected abstract readonly notifier: ColumnResizeNotifierSource; /** Unique ID for this table instance. */ - protected readonly selectorId = this._idGenerator.getId('cdk-column-resize-'); + protected readonly selectorId = inject(_IdGenerator).getId('cdk-column-resize-'); /** The id attribute of the table, if specified. */ id?: string; @@ -88,6 +87,11 @@ export abstract class ColumnResize implements AfterViewInit, OnDestroy { return this.selectorId; } + /** Gets the ID for this table used for column size persistance. */ + getTableId(): string { + return String(this.elementRef.nativeElement.id); + } + /** Called when a column in the table is resized. Applies a css class to the table element. */ setResized() { this.elementRef.nativeElement!.classList.add(WITH_RESIZED_COLUMN_CLASS); diff --git a/src/cdk-experimental/column-resize/column-size-store.ts b/src/cdk-experimental/column-resize/column-size-store.ts index f28e3ea9fb3d..6fe13e55f732 100644 --- a/src/cdk-experimental/column-resize/column-size-store.ts +++ b/src/cdk-experimental/column-resize/column-size-store.ts @@ -7,6 +7,7 @@ */ import {Injectable} from '@angular/core'; +import {Observable} from 'rxjs'; /** * Can be provided by the host application to enable persistence of column resize state. @@ -14,8 +15,8 @@ import {Injectable} from '@angular/core'; @Injectable() export abstract class ColumnSizeStore { /** Returns the persisted size of the specified column in the specified table. */ - abstract getSize(tableId: string, columnId: string): number; + abstract getSize(tableId: string, columnId: string): Observable | null; /** Persists the size of the specified column in the specified table. */ - abstract setSize(tableId: string, columnId: string): void; + abstract setSize(tableId: string, columnId: string, sizePx: number): void; } diff --git a/src/cdk-experimental/column-resize/resizable.ts b/src/cdk-experimental/column-resize/resizable.ts index 94ff2c766f77..06ce875f0eae 100644 --- a/src/cdk-experimental/column-resize/resizable.ts +++ b/src/cdk-experimental/column-resize/resizable.ts @@ -10,6 +10,7 @@ import { AfterViewInit, Directive, ElementRef, + inject, Injector, NgZone, OnDestroy, @@ -22,7 +23,7 @@ import {ComponentPortal} from '@angular/cdk/portal'; import {Overlay, OverlayRef} from '@angular/cdk/overlay'; import {CdkColumnDef, _CoalescedStyleScheduler} from '@angular/cdk/table'; import {merge, Subject} from 'rxjs'; -import {filter, takeUntil} from 'rxjs/operators'; +import {distinctUntilChanged, filter, take, takeUntil} from 'rxjs/operators'; import {_closest} from '@angular/cdk-experimental/popover-edit'; @@ -30,6 +31,7 @@ import {HEADER_ROW_SELECTOR} from './selectors'; import {ResizeOverlayHandle} from './overlay-handle'; import {ColumnResize} from './column-resize'; import {ColumnSizeAction, ColumnResizeNotifierSource} from './column-resize-notifier'; +import {ColumnSizeStore} from './column-size-store'; import {HeaderRowEventDispatcher} from './event-dispatcher'; import {ResizeRef} from './resize-ref'; import {ResizeStrategy} from './resize-strategy'; @@ -66,6 +68,8 @@ export abstract class Resizable protected abstract readonly viewContainerRef: ViewContainerRef; protected abstract readonly changeDetectorRef: ChangeDetectorRef; + protected readonly columnSizeStore = inject(ColumnSizeStore, {optional: true}); + private _viewInitialized = false; private _isDestroyed = false; @@ -105,6 +109,15 @@ export abstract class Resizable this._viewInitialized = true; this._applyMinWidthPx(); this._applyMaxWidthPx(); + this.columnSizeStore + ?.getSize(this.columnResize.getTableId(), this.columnDef.name) + ?.pipe(take(1), takeUntil(this.destroyed)) + .subscribe(size => { + if (size == null) { + return; + } + this._applySize(size); + }); }); } @@ -195,6 +208,20 @@ export abstract class Resizable .subscribe(columnSize => { this._cleanUpAfterResize(columnSize); }); + + this.resizeNotifier.resizeCompleted + .pipe( + filter(sizeUpdate => sizeUpdate.columnId === this.columnDef.name), + distinctUntilChanged((a, b) => a.size === b.size), + takeUntil(this.destroyed), + ) + .subscribe(sizeUpdate => { + this.columnSizeStore?.setSize( + this.columnResize.getTableId(), + this.columnDef.name, + sizeUpdate.size, + ); + }); } private _completeResizeOperation(): void { diff --git a/src/material-experimental/column-resize/column-resize.spec.ts b/src/material-experimental/column-resize/column-resize.spec.ts index 554d36c80874..a6434c3b30ee 100644 --- a/src/material-experimental/column-resize/column-resize.spec.ts +++ b/src/material-experimental/column-resize/column-resize.spec.ts @@ -1,13 +1,20 @@ import {BidiModule} from '@angular/cdk/bidi'; import {DataSource} from '@angular/cdk/collections'; import {ESCAPE} from '@angular/cdk/keycodes'; -import {ChangeDetectionStrategy, Component, Directive, ElementRef, ViewChild} from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + Directive, + ElementRef, + Injectable, + ViewChild, +} from '@angular/core'; import {ComponentFixture, TestBed, fakeAsync, flush} from '@angular/core/testing'; import {MatTableModule} from '@angular/material/table'; -import {BehaviorSubject} from 'rxjs'; +import {BehaviorSubject, Observable, ReplaySubject} from 'rxjs'; import {dispatchKeyboardEvent} from '../../cdk/testing/private'; -import {ColumnSize} from '@angular/cdk-experimental/column-resize'; +import {ColumnSize, ColumnSizeStore} from '@angular/cdk-experimental/column-resize'; import {AbstractMatColumnResize} from './column-resize-directives/common'; import { MatColumnResize, @@ -52,7 +59,7 @@ function getTableTemplate(defaultEnabled: boolean) { }
- @@ -109,7 +116,7 @@ function getFlexTemplate(defaultEnabled: boolean) { }
- + { })); }); } + + describe('ColumnSizeStore (persistance)', () => { + let component: BaseTestComponent; + let fixture: ComponentFixture; + let columnSizeStore: FakeColumnSizeStore; + + beforeEach(fakeAsync(() => { + jasmine.addMatchers(approximateMatcher); + + TestBed.configureTestingModule({ + imports: [BidiModule, MatTableModule, MatColumnResizeModule], + providers: [ + FakeColumnSizeStore, + {provide: ColumnSizeStore, useExisting: FakeColumnSizeStore}, + ], + declarations: [MatResizeOnPushTest], + }); + fixture = TestBed.createComponent(MatResizeOnPushTest); + component = fixture.componentInstance; + columnSizeStore = TestBed.inject(FakeColumnSizeStore); + fixture.detectChanges(); + flush(); + })); + + it('applies the persisted size', fakeAsync(() => { + (expect(component.getColumnWidth(1)).not as any).isApproximately(300); + + columnSizeStore.emitSize('theTable', 'name', 300); + + flush(); + + (expect(component.getColumnWidth(1)) as any).isApproximately(300); + })); + + it('persists the user-triggered size update', fakeAsync(() => { + const initialColumnWidth = component.getColumnWidth(1); + + component.triggerHoverState(); + fixture.detectChanges(); + + component.resizeColumnWithMouse(1, 5); + fixture.detectChanges(); + flush(); + + component.completeResizeWithMouseInProgress(1); + flush(); + + component.endHoverState(); + fixture.detectChanges(); + + expect(columnSizeStore.setSizeCalls.length).toBe(1); + const {tableId, columnId, sizePx} = columnSizeStore.setSizeCalls[0]; + expect(tableId).toBe('theTable'); + expect(columnId).toBe('name'); + (expect(sizePx) as any).isApproximately(initialColumnWidth + 5); + })); + }); }); function createElementData() { @@ -639,3 +703,38 @@ function createElementData() { {position: 5, name: 'Boron', weight: 10.811, symbol: 'B'}, ]; } + +@Injectable() +class FakeColumnSizeStore extends ColumnSizeStore { + readonly emitStore = new Map>(); + readonly setSizeCalls: {tableId: string; columnId: string; sizePx: number}[] = []; + + /** Returns an observable that will emit values from emitSize(). */ + override getSize(tableId: string, columnId: string): Observable | null { + return this._getOrAdd(tableId, columnId); + } + + /** + * Adds an entry to setSizeCalls. + * Note: Does not affect values returned from getSize. + */ + override setSize(tableId: string, columnId: string, sizePx: number): void { + this.setSizeCalls.push({tableId, columnId, sizePx}); + } + + /** Call this in test code to simulate persisted column sizes. */ + emitSize(tableId: string, columnId: string, sizePx: number) { + const stored = this._getOrAdd(tableId, columnId); + stored.next(sizePx); + } + + private _getOrAdd(tableId: string, columnId: string): ReplaySubject { + const key = `tableId----columnId`; + let stored = this.emitStore.get(key); + if (!stored) { + stored = new ReplaySubject(1); + this.emitStore.set(key, stored); + } + return stored; + } +} diff --git a/src/material-experimental/column-resize/public-api.ts b/src/material-experimental/column-resize/public-api.ts index 292bb3e81938..61e14e2e3d77 100644 --- a/src/material-experimental/column-resize/public-api.ts +++ b/src/material-experimental/column-resize/public-api.ts @@ -16,4 +16,4 @@ export * from './resizable-directives/resizable'; export * from './resize-strategy'; export * from './overlay-handle'; export type {ColumnResizeOptions} from '@angular/cdk-experimental/column-resize'; -export {COLUMN_RESIZE_OPTIONS} from '@angular/cdk-experimental/column-resize'; +export {COLUMN_RESIZE_OPTIONS, ColumnSizeStore} from '@angular/cdk-experimental/column-resize';