From 95ea26149e22714990f41c954792c67324b93d3c Mon Sep 17 00:00:00 2001 From: Shlomi Assaf Date: Wed, 16 Dec 2020 00:59:33 +0200 Subject: [PATCH] refactor(ngrid): move code from main component to logical units --- .../column-resize/column-resize.component.ts | 2 +- libs/ngrid/src/lib/ext/grid-ext-api.ts | 13 +- libs/ngrid/src/lib/grid/api-factory.ts | 33 ++- .../lib/grid/cell/header-cell.component.ts | 2 +- .../lib/grid/column/directives/column-def.ts | 4 +- .../lib/grid/column/management/column-api.ts | 6 +- .../grid/column/management/column-store.ts | 21 +- .../column/width-logic/column-width-calc.ts | 143 +++++++++++ .../width-logic/dynamic-column-width.ts | 6 + .../column-size-observer-group.ts | 15 +- .../column-size-observer.ts | 6 +- libs/ngrid/src/lib/grid/logicap/README.md | 8 + .../src/lib/grid/logicap/bind-registry.ts | 42 ++++ libs/ngrid/src/lib/grid/logicap/index.ts | 18 ++ .../src/lib/grid/logicap/no-data-view.ts | 41 ++++ .../src/lib/grid/logicap/pagination-view.ts | 34 +++ libs/ngrid/src/lib/grid/ngrid.component.ts | 232 +----------------- 17 files changed, 369 insertions(+), 257 deletions(-) create mode 100644 libs/ngrid/src/lib/grid/column/width-logic/column-width-calc.ts create mode 100644 libs/ngrid/src/lib/grid/logicap/README.md create mode 100644 libs/ngrid/src/lib/grid/logicap/bind-registry.ts create mode 100644 libs/ngrid/src/lib/grid/logicap/index.ts create mode 100644 libs/ngrid/src/lib/grid/logicap/no-data-view.ts create mode 100644 libs/ngrid/src/lib/grid/logicap/pagination-view.ts diff --git a/libs/ngrid/drag/src/lib/column-resize/column-resize.component.ts b/libs/ngrid/drag/src/lib/column-resize/column-resize.component.ts index 1ec5ff239..d64458209 100644 --- a/libs/ngrid/drag/src/lib/column-resize/column-resize.component.ts +++ b/libs/ngrid/drag/src/lib/column-resize/column-resize.component.ts @@ -201,7 +201,7 @@ export class PblNgridDragResizeComponent implements AfterViewInit, OnDestroy { if (this._lastWidth !== newWidth) { this._lastWidth = newWidth; this.column.updateWidth(`${newWidth}px`); - this.grid.resetColumnsWidth(); + this._extApi.widthCalc.resetColumnsWidth(); // `this.column.updateWidth` will update the grid width cell only, which will trigger a resize that will update all other cells // `this.grid.resetColumnsWidth()` will re-adjust all other grid width cells, and if their size changes they will trigger the resize event... } diff --git a/libs/ngrid/src/lib/ext/grid-ext-api.ts b/libs/ngrid/src/lib/ext/grid-ext-api.ts index 2c2ef4d08..dc3a4fb29 100644 --- a/libs/ngrid/src/lib/ext/grid-ext-api.ts +++ b/libs/ngrid/src/lib/ext/grid-ext-api.ts @@ -2,17 +2,18 @@ import { Observable } from 'rxjs'; import { InjectionToken } from '@angular/core'; import { Direction } from '@angular/cdk/bidi'; -import { PblNgridConfigService, PblNgridEvents } from '@pebula/ngrid/core'; +import { PblNgridConfigService, PblNgridEvents, PblNgridRegistryService } from '@pebula/ngrid/core'; import { PblCdkTableComponent } from '../grid/pbl-cdk-table/pbl-cdk-table.component'; import { ContextApi } from '../grid/context/api'; import { PblNgridComponent } from '../grid/ngrid.component'; import { ColumnApi, PblColumnStore } from '../grid/column/management'; -import { DynamicColumnWidthLogic } from '../grid/column/width-logic/dynamic-column-width'; +import { PblNgridColumnWidthCalc } from '../grid/column/width-logic/column-width-calc'; import { PblCdkVirtualScrollViewportComponent } from '../grid/features/virtual-scroll/virtual-scroll-viewport.component' import { NotifyPropChangeMethod, OnPropChangedEvent } from './types'; import { PblNgridMetaRowService } from '../grid/meta-rows/meta-row.service'; import { RowsApi, PblRowsApi } from '../grid/row'; import { PblNgridPluginContext, PblNgridPluginController } from './plugin-control'; +import { Logicaps } from '../grid/logicap/index'; export const EXT_API_TOKEN = new InjectionToken('PBL_NGRID_EXTERNAL_API'); @@ -20,6 +21,11 @@ export interface PblNgridExtensionApi { grid: PblNgridComponent; element: HTMLElement; config: PblNgridConfigService; + /** + * The registry instance bound to the current instance. + * This registry instance lifespan is similar to the grid's component, it will get destroyed when the grid gets destroyed. + */ + registry: PblNgridRegistryService; propChanged: Observable; cdkTable: PblCdkTableComponent; columnStore: PblColumnStore; @@ -29,9 +35,9 @@ export interface PblNgridExtensionApi { events: Observable; metaRowService: PblNgridMetaRowService; pluginCtrl: PblNgridPluginController; + widthCalc: PblNgridColumnWidthCalc; onConstructed(fn: () => void): void; onInit(fn: () => void): void; - dynamicColumnWidthFactory(dir?: Direction): DynamicColumnWidthLogic; getDirection(): Direction; directionChange(): Observable; } @@ -43,4 +49,5 @@ export interface PblNgridInternalExtensionApi extends PblNgridExtension setViewport(viewport: PblCdkVirtualScrollViewportComponent): void; setCdkTable(cdkTable: PblCdkTableComponent): void; notifyPropChanged: NotifyPropChangeMethod; + logicaps: Logicaps; } diff --git a/libs/ngrid/src/lib/grid/api-factory.ts b/libs/ngrid/src/lib/grid/api-factory.ts index 65b7f8e8f..3b3944b3f 100644 --- a/libs/ngrid/src/lib/grid/api-factory.ts +++ b/libs/ngrid/src/lib/grid/api-factory.ts @@ -2,20 +2,21 @@ import { Observable, of, Subject, EMPTY } from 'rxjs'; import { ChangeDetectorRef, ElementRef, Injector, IterableDiffers, NgZone, ViewContainerRef } from '@angular/core'; import { Direction, Directionality } from '@angular/cdk/bidi'; -import { PblNgridConfigService, PblNgridEvents, ON_DESTROY, ON_CONSTRUCTED } from '@pebula/ngrid/core'; +import { PblNgridConfigService, PblNgridEvents, ON_DESTROY, ON_CONSTRUCTED, PblNgridRegistryService } from '@pebula/ngrid/core'; import { PblNgridInternalExtensionApi } from '../ext/grid-ext-api'; +import { PblNgridPluginContext } from '../ext/plugin-control'; +import { OnPropChangedEvent } from '../ext/types'; import { ColumnApi, PblColumnStore } from './column/management'; +import { PblNgridColumnWidthCalc } from './column/width-logic/column-width-calc'; import { PblNgridComponent } from './ngrid.component'; import { PblCdkTableComponent } from './pbl-cdk-table/pbl-cdk-table.component'; import { PblRowsApi } from './row/rows-api'; import { PblNgridCellFactoryResolver } from './row/cell-factory.service'; -import { DynamicColumnWidthLogic, DYNAMIC_PADDING_BOX_MODEL_SPACE_STRATEGY } from './column/width-logic/dynamic-column-width'; import { ContextApi } from './context/api'; import { PblNgridMetaRowService } from './meta-rows/meta-row.service'; -import { PblNgridPluginContext } from '../ext/plugin-control'; -import { OnPropChangedEvent } from '../ext/types'; import { PblCdkVirtualScrollViewportComponent } from './features/virtual-scroll/virtual-scroll-viewport.component'; import { bindGridToDataSource } from './bind-grid-to-datasource'; +import { logicap, Logicaps } from './logicap/index'; export interface RequiredAngularTokens { ngZone: NgZone; @@ -24,6 +25,7 @@ export interface RequiredAngularTokens { cdRef: ChangeDetectorRef; elRef: ElementRef; config: PblNgridConfigService; + registry: PblNgridRegistryService; dir?: Directionality; } @@ -33,6 +35,7 @@ export function createApis(grid: PblNgridComponent, tokens: RequiredAngula class InternalExtensionApi implements PblNgridInternalExtensionApi { readonly config: PblNgridConfigService; + readonly registry: PblNgridRegistryService; readonly element: HTMLElement; readonly propChanged: Observable; readonly columnStore: PblColumnStore; @@ -41,6 +44,8 @@ class InternalExtensionApi implements PblNgridInternalExtensionApi { readonly rowsApi: PblRowsApi; readonly events: Observable; readonly plugin: PblNgridPluginContext; + readonly widthCalc: PblNgridColumnWidthCalc; + readonly logicaps: Logicaps; get cdkTable() { return this._cdkTable; } get contextApi() { return this._contextApi || (this._contextApi = new ContextApi(this)); } @@ -58,6 +63,7 @@ class InternalExtensionApi implements PblNgridInternalExtensionApi { this.propChanged = this._propChanged = new Subject(); this.config = tokens.config; + this.registry = tokens.registry; this.element = tokens.elRef.nativeElement; if (tokens.dir) { this.dir = tokens.dir; @@ -67,7 +73,8 @@ class InternalExtensionApi implements PblNgridInternalExtensionApi { this._create = init; this.plugin = plugin; this.events = plugin.events; - this.columnStore = new PblColumnStore(grid, tokens.injector.get(IterableDiffers)); + this.columnStore = new PblColumnStore(this, tokens.injector.get(IterableDiffers)); + this.widthCalc = new PblNgridColumnWidthCalc(this); const cellFactory = tokens.injector.get(PblNgridCellFactoryResolver); this.rowsApi = new PblRowsApi(this, tokens.ngZone, cellFactory); @@ -75,10 +82,22 @@ class InternalExtensionApi implements PblNgridInternalExtensionApi { this.columnApi = ColumnApi.create(this); this.metaRowService = new PblNgridMetaRowService(this); this._contextApi = new ContextApi(this); + this.logicaps = logicap(this); bindGridToDataSource(this); this.events.pipe(ON_DESTROY).subscribe( e => this._propChanged.complete() ); + + this.widthCalc + .onWidthCalc + .subscribe( rowWidth => { + this._cdkTable.minWidth = rowWidth.minimumRowWidth; + + tokens.ngZone.run( () => { + this.rowsApi.syncRows('header'); + this.plugin.emitEvent({ source: 'grid', kind: 'onResizeRow' }); + }); + }); } getDirection() { @@ -113,10 +132,6 @@ class InternalExtensionApi implements PblNgridInternalExtensionApi { this._viewPort = viewport; } - dynamicColumnWidthFactory(dir?: Direction): DynamicColumnWidthLogic { - return new DynamicColumnWidthLogic(DYNAMIC_PADDING_BOX_MODEL_SPACE_STRATEGY, dir ?? this.dir?.value); - } - notifyPropChanged(source, key, prev, curr) { if (prev !== curr) { this._propChanged.next({source, key, prev, curr} as any); diff --git a/libs/ngrid/src/lib/grid/cell/header-cell.component.ts b/libs/ngrid/src/lib/grid/cell/header-cell.component.ts index e405dc272..1e8bea278 100644 --- a/libs/ngrid/src/lib/grid/cell/header-cell.component.ts +++ b/libs/ngrid/src/lib/grid/cell/header-cell.component.ts @@ -82,7 +82,7 @@ export class PblNgridHeaderCellComponent extends PblN predicate = event => (!gridWidthRow && event.reason !== 'update') || (gridWidthRow && event.reason !== 'resize'); view = !gridWidthRow ? this.initMainHeaderColumnView(column) : undefined; if (gridWidthRow && !this.resizeObserver) { - this.resizeObserver = new PblColumnSizeObserver(this.el, this.extApi.grid); + this.resizeObserver = new PblColumnSizeObserver(this.el, this.extApi); } this.columnDef.widthChange diff --git a/libs/ngrid/src/lib/grid/column/directives/column-def.ts b/libs/ngrid/src/lib/grid/column/directives/column-def.ts index 4f7f0ab73..b59ec80ab 100644 --- a/libs/ngrid/src/lib/grid/column/directives/column-def.ts +++ b/libs/ngrid/src/lib/grid/column/directives/column-def.ts @@ -91,8 +91,8 @@ export class PblNgridColumnDef extends CdkColumnDef i super(); this.grid = extApi.grid; - const s = extApi.dynamicColumnWidthFactory().strategy; - this.widthBreakout = c => widthBreakout(s, c); + const { strategy } = extApi.widthCalc.dynamicColumnWidth; + this.widthBreakout = c => widthBreakout(strategy, c); } /** diff --git a/libs/ngrid/src/lib/grid/column/management/column-api.ts b/libs/ngrid/src/lib/grid/column/management/column-api.ts index 72d3c77da..b4dc3bb75 100644 --- a/libs/ngrid/src/lib/grid/column/management/column-api.ts +++ b/libs/ngrid/src/lib/grid/column/management/column-api.ts @@ -164,7 +164,7 @@ export class ColumnApi { * For each visible column in the table, resize the width to a proportional width relative to the total width provided. */ autoSizeToFit(totalWidth: number, options: AutoSizeToFitOptions = {}): void { - const wLogic = this.extApi.dynamicColumnWidthFactory(); + const wLogic = this.extApi.widthCalc.dynamicColumnWidth; const { visibleColumns } = this; const columnBehavior: AutoSizeToFitOptions['columnBehavior'] = options.columnBehavior || ( () => options ) as any; @@ -230,8 +230,8 @@ export class ColumnApi { } // we now reset the column widths, this will calculate a new `defaultWidth` and set it in all columns but the relevant ones are column from (3) // It will also mark all columnDefs for check - this.grid.resetColumnsWidth(); - this.grid.resizeColumns(); + this.extApi.widthCalc.resetColumnsWidth(); + this.extApi.widthCalc.calcColumnWidth(); } /** diff --git a/libs/ngrid/src/lib/grid/column/management/column-store.ts b/libs/ngrid/src/lib/grid/column/management/column-store.ts index 81ec65d84..9a4b4d1e7 100644 --- a/libs/ngrid/src/lib/grid/column/management/column-store.ts +++ b/libs/ngrid/src/lib/grid/column/management/column-store.ts @@ -1,6 +1,9 @@ import { Subject, Observable } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; import { isDevMode, IterableDiffer, IterableDiffers } from '@angular/core'; -import { PblNgridColumnDefinitionSet, PblColumnSet, PblMetaRowDefinitions } from '@pebula/ngrid/core'; +import { ON_DESTROY, PblNgridColumnDefinitionSet, PblColumnSet, PblMetaRowDefinitions } from '@pebula/ngrid/core'; + +import { PblNgridInternalExtensionApi } from '../../../ext/grid-ext-api'; import { PblNgridComponent } from '../../ngrid.component'; import { findCellDef } from '../../cell/cell-def/utils'; import { @@ -9,12 +12,12 @@ import { isPblColumn, PblColumn, PblMetaColumn, PblNgridColumnSet, } from '../model'; +import { GridRowType } from '../../row/types'; +import { PblNgridBaseRowComponent } from '../../row/base-row.component'; import { StaticColumnWidthLogic } from '../width-logic/static-column-width'; import { resetColumnWidths } from '../../utils/width'; import { PblMetaColumnStore, PblRowColumnsChangeEvent, PblRowTypeToColumnTypeMap } from './types'; import { HiddenColumns } from './hidden-columns'; -import { GridRowType } from '../../row/types'; -import { PblNgridBaseRowComponent } from '../../row/base-row.component'; import { MetaRowsStore } from './meta-rows-store'; export class PblColumnStore { @@ -40,8 +43,10 @@ export class PblColumnStore { private differ: IterableDiffer; private _visibleChanged$ = new Subject>(); private metaRowsStore: MetaRowsStore; + private grid: PblNgridComponent; - constructor(private readonly grid: PblNgridComponent, private readonly differs: IterableDiffers) { + constructor(private readonly extApi: PblNgridInternalExtensionApi, private readonly differs: IterableDiffers) { + this.grid = extApi.grid; this.metaRowsStore = new MetaRowsStore(differs); this.resetIds(); this.resetColumns(); @@ -471,13 +476,13 @@ export class PblColumnStore { private afterColumnPositionChange(): void { // TODO: This shouldn't be here, it should be the responsibility of the caller to clear the context // Because now there is not option to control it. - this.grid.contextApi.clear(true); + this.extApi.contextApi.clear(true); this.updateGroups(); - this.grid.resetColumnsWidth(); + this.extApi.widthCalc.resetColumnsWidth(); // now, any newly added column cells must first spin up to get a size // and most importantly have their ngAfterViewInit fired so the resize column will update the sizeInfo of the column! - this.grid.rowsApi.syncRows('header', true); - this.grid.resizeColumns(); + this.extApi.rowsApi.syncRows('header', true); + this.extApi.widthCalc.calcColumnWidth(); } } diff --git a/libs/ngrid/src/lib/grid/column/width-logic/column-width-calc.ts b/libs/ngrid/src/lib/grid/column/width-logic/column-width-calc.ts new file mode 100644 index 000000000..782b03776 --- /dev/null +++ b/libs/ngrid/src/lib/grid/column/width-logic/column-width-calc.ts @@ -0,0 +1,143 @@ +import { animationFrameScheduler, fromEventPattern, Subject } from 'rxjs'; +import { debounceTime, skip, takeUntil } from 'rxjs/operators'; +import { ON_DESTROY } from '@pebula/ngrid/core'; +import { PblNgridInternalExtensionApi } from '../../../ext/grid-ext-api'; +import { resetColumnWidths } from '../../utils/width'; +import { PblColumn } from '../model/column'; +import { PblColumnStore } from '../management/column-store'; +import { DynamicColumnWidthLogic, DYNAMIC_PADDING_BOX_MODEL_SPACE_STRATEGY } from './dynamic-column-width'; + +export class PblNgridColumnWidthCalc { + + readonly dynamicColumnWidth: DynamicColumnWidthLogic + readonly onWidthCalc = new Subject(); + private readonly columnStore: PblColumnStore + + constructor(private readonly extApi: PblNgridInternalExtensionApi) { + this.columnStore = extApi.columnStore; + this.dynamicColumnWidth = new DynamicColumnWidthLogic(DYNAMIC_PADDING_BOX_MODEL_SPACE_STRATEGY, extApi.getDirection()); + extApi.directionChange() + .pipe(takeUntil(extApi.events.pipe(ON_DESTROY))) + .subscribe( dir => this.dynamicColumnWidth.dir = dir ); + + extApi.events.pipe(ON_DESTROY).subscribe(() => this.onWidthCalc.complete() ); + + extApi.onInit(() => this.listenToResize() ); + } + + /** + * Updates the column sizes for all columns in the grid based on the column definition metadata for each column. + * The final width represent a static width, it is the value as set in the definition (except column without width, where the calculated global width is set). + */ + resetColumnsWidth(): void { + resetColumnWidths(this.columnStore.getStaticWidth(), this.columnStore.visibleColumns, this.columnStore.metaColumns); + } + + calcColumnWidth(columns?: PblColumn[]): void { + if (!columns) { + columns = this.columnStore.visibleColumns; + } + + // protect from per-mature resize. + // Will happen on additional header/header-group rows AND ALSO when vScrollNone is set + // This will cause size not to populate because it takes time to render the rows, since it's not virtual and happens immediately. + // TODO: find a better protection. + if (!columns[0].sizeInfo) { + return; + } + + // stores and calculates width for columns added to it. Aggregate's the total width of all added columns. + const rowWidth = this.dynamicColumnWidth; + rowWidth.reset(); + this.syncColumnGroupsSize(); + + // if this is a grid without groups + if (rowWidth.minimumRowWidth === 0) { + rowWidth.addGroup(columns.map( c => c.sizeInfo )); + } + + // if the max lock state has changed we need to update re-calculate the static width's again. + if (rowWidth.maxWidthLockChanged) { + this.resetColumnsWidth(); + this.calcColumnWidth(columns); + return; + } + + this.onWidthCalc.next(rowWidth); + } + + /** + * Update the size of all group columns in the grid based on the size of their visible children (not hidden). + * @param dynamicWidthLogic - Optional logic container, if not set a new one is created. + */ + private syncColumnGroupsSize(): void { + // From all meta columns (header/footer/headerGroup) we filter only `headerGroup` columns. + // For each we calculate it's width from all of the columns that the headerGroup "groups". + // We use the same strategy and the same RowWidthDynamicAggregator instance which will prevent duplicate calculations. + // Note that we might have multiple header groups, i.e. same columns on multiple groups with different row index. + for (const g of this.columnStore.getAllHeaderGroup()) { + // We go over all columns because g.columns does not represent the current owned columns of the group + // it is static, representing the initial state. + // Only columns hold their group owners. + // TODO: find way to improve iteration + const colSizeInfos = this.columnStore.visibleColumns.filter( c => !c.hidden && c.isInGroup(g)).map( c => c.sizeInfo ); + if (colSizeInfos.length > 0) { + const groupWidth = this.dynamicColumnWidth.addGroup(colSizeInfos); + g.minWidth = groupWidth; + g.updateWidth(`${groupWidth}px`); + } else { + g.minWidth = undefined; + g.updateWidth(`0px`); + } + } + } + + private listenToResize(): void { + const { element } = this.extApi; + let resizeObserver: ResizeObserver; + const ro$ = fromEventPattern<[ResizeObserverEntry[], ResizeObserver]>( + handler => { + if (!resizeObserver) { + resizeObserver = new ResizeObserver(handler); + resizeObserver.observe(element); + } + }, + handler => { + if (resizeObserver) { + resizeObserver.unobserve(element); + resizeObserver.disconnect(); + resizeObserver = undefined; + } + } + ); + + // Skip the first emission + // Debounce all resizes until the next complete animation frame without a resize + // finally maps to the entries collection + // SKIP: We should skip the first emission (`skip(1)`) before we debounce, since its called upon calling "observe" on the resizeObserver. + // The problem is that some grid might require this because they do change size. + // An example is a grid in a mat-tab that is hidden, the grid will hit the resize one when we focus the tab + // which will require a resize handling because it's initial size is 0 + // To workaround this, we only skip elements not yet added to the DOM, which means they will not trigger a resize event. + let skipValue = document.body.contains(element) ? 1 : 0; + + ro$ + .pipe( + skip(skipValue), + debounceTime(0, animationFrameScheduler), + takeUntil(this.extApi.events.pipe(ON_DESTROY)), + ) + .subscribe( (args: [ResizeObserverEntry[], ResizeObserver]) => { + if (skipValue === 0) { + skipValue = 1; + this.extApi.columnStore.visibleColumns.forEach( c => c.sizeInfo.updateSize() ); + } + this.onResize(args[0]); + }); + } + + private onResize(entries: ResizeObserverEntry[]): void { + this.extApi.viewport?.checkViewportSize(); + this.calcColumnWidth(); + } +} diff --git a/libs/ngrid/src/lib/grid/column/width-logic/dynamic-column-width.ts b/libs/ngrid/src/lib/grid/column/width-logic/dynamic-column-width.ts index 1d010cc8a..729076c24 100644 --- a/libs/ngrid/src/lib/grid/column/width-logic/dynamic-column-width.ts +++ b/libs/ngrid/src/lib/grid/column/width-logic/dynamic-column-width.ts @@ -39,6 +39,12 @@ export class DynamicColumnWidthLogic { constructor(public readonly strategy: BoxModelSpaceStrategy, public dir?: Direction) { } + reset() { + this.maxWidthLockChanged = false; + this._minimumRowWidth = 0; + this.cols.clear(); + } + /** * Returns a breakout of the width of the column, breaking it into the width of the content and the rest of the width */ diff --git a/libs/ngrid/src/lib/grid/features/column-size-observer/column-size-observer-group.ts b/libs/ngrid/src/lib/grid/features/column-size-observer/column-size-observer-group.ts index 655a2488a..394613e5f 100644 --- a/libs/ngrid/src/lib/grid/features/column-size-observer/column-size-observer-group.ts +++ b/libs/ngrid/src/lib/grid/features/column-size-observer/column-size-observer-group.ts @@ -1,3 +1,4 @@ +import { PblNgridInternalExtensionApi } from '../../../ext/grid-ext-api'; import { PblNgridComponent } from '../../ngrid.component'; import { PblColumn } from '../../column/model/column'; import { PblColumnSizeObserver } from './column-size-observer'; @@ -13,18 +14,18 @@ export class PblNgridColumnSizeObserverGroup { private ro: ResizeObserver; private columns: PblColumnSizeObserver[] = []; - constructor(private grid: PblNgridComponent) { + constructor(private extApi: PblNgridInternalExtensionApi) { this.entries = new WeakMap(); this.ro = new ResizeObserver( entries => { requestAnimationFrame(() => this.onResize(entries) ); }); } - static get(table: PblNgridComponent): PblNgridColumnSizeObserverGroup { - let controller = PBL_NGRID_MAP.get(table); + static get(extApi: PblNgridInternalExtensionApi): PblNgridColumnSizeObserverGroup { + let controller = PBL_NGRID_MAP.get(extApi.grid); if (!controller) { - controller = new PblNgridColumnSizeObserverGroup(table); - PBL_NGRID_MAP.set(table, controller); + controller = new PblNgridColumnSizeObserverGroup(extApi); + PBL_NGRID_MAP.set(extApi.grid, controller); } return controller; } @@ -52,7 +53,7 @@ export class PblNgridColumnSizeObserverGroup { } if (this.columns.length === 0) { this.ro.disconnect(); - PBL_NGRID_MAP.delete(this.grid); + PBL_NGRID_MAP.delete(this.extApi.grid); } } @@ -71,7 +72,7 @@ export class PblNgridColumnSizeObserverGroup { c.updateSize(); } if (!isDragging) { - this.grid.resizeColumns(this.columns.map( c => c.column )); + this.extApi.widthCalc.calcColumnWidth(this.columns.map( c => c.column )); } } } diff --git a/libs/ngrid/src/lib/grid/features/column-size-observer/column-size-observer.ts b/libs/ngrid/src/lib/grid/features/column-size-observer/column-size-observer.ts index b6687272d..92843a4a9 100644 --- a/libs/ngrid/src/lib/grid/features/column-size-observer/column-size-observer.ts +++ b/libs/ngrid/src/lib/grid/features/column-size-observer/column-size-observer.ts @@ -1,4 +1,4 @@ -import { PblNgridComponent } from '../../ngrid.component'; +import { PblNgridInternalExtensionApi } from '../../../ext/grid-ext-api'; import { ColumnSizeInfo } from '../../column/model/column-size-info'; import { PblColumn } from '../../column/model/column'; import { PblNgridColumnSizeObserverGroup } from './column-size-observer-group'; @@ -17,9 +17,9 @@ import { PblNgridColumnSizeObserverGroup } from './column-size-observer-group'; export class PblColumnSizeObserver extends ColumnSizeInfo { private controller: PblNgridColumnSizeObserverGroup; - constructor(element: HTMLElement, grid: PblNgridComponent) { + constructor(element: HTMLElement, extApi: PblNgridInternalExtensionApi) { super(element); - this.controller = PblNgridColumnSizeObserverGroup.get(grid); + this.controller = PblNgridColumnSizeObserverGroup.get(extApi); } protected attachColumn(column: PblColumn): void { diff --git a/libs/ngrid/src/lib/grid/logicap/README.md b/libs/ngrid/src/lib/grid/logicap/README.md new file mode 100644 index 000000000..748303649 --- /dev/null +++ b/libs/ngrid/src/lib/grid/logicap/README.md @@ -0,0 +1,8 @@ +# logicap + +A **logicap** is a "Logical Capsule" that contains some logical functionality with internal state. +Think of it like a method on a class that has access to properties on the class. + +All **logicap's** are factory functions returning the method with the internal state. + +The **logicap's** are used to reduce code in the main grid component. diff --git a/libs/ngrid/src/lib/grid/logicap/bind-registry.ts b/libs/ngrid/src/lib/grid/logicap/bind-registry.ts new file mode 100644 index 000000000..535cb3594 --- /dev/null +++ b/libs/ngrid/src/lib/grid/logicap/bind-registry.ts @@ -0,0 +1,42 @@ +import { PblNgridInternalExtensionApi } from '../../ext/grid-ext-api'; + +/** + * Listens to registry changes and update the grid + * Must run when the grid in at content init + */ +export function bindRegistryLogicap(extApi: PblNgridInternalExtensionApi) { + return () => { + // no need to unsubscribe, the reg service is per grid instance and it will destroy when this grid destroy. + // Also, at this point initial changes from templates provided in the content are already inside so they will not trigger + // the order here is very important, because component top of this grid will fire life cycle hooks AFTER this component + // so if we have a top level component registering a template on top it will not show unless we listen. + extApi.registry.changes + .subscribe( changes => { + let gridCell = false; + let headerFooterCell = false; + for (const c of changes) { + switch (c.type) { + case 'tableCell': + gridCell = true; + break; + case 'headerCell': + case 'footerCell': + headerFooterCell = true; + break; + case 'noData': + extApi.logicaps.noData(); + break; + case 'paginator': + extApi.logicaps.pagination(); + break; + } + } + if (gridCell) { + extApi.columnStore.attachCustomCellTemplates(); + } + if (headerFooterCell) { + extApi.columnStore.attachCustomHeaderCellTemplates(); + } + }); + } +} diff --git a/libs/ngrid/src/lib/grid/logicap/index.ts b/libs/ngrid/src/lib/grid/logicap/index.ts new file mode 100644 index 000000000..ae589ce75 --- /dev/null +++ b/libs/ngrid/src/lib/grid/logicap/index.ts @@ -0,0 +1,18 @@ +import { PblNgridInternalExtensionApi } from '../../ext/grid-ext-api'; +import { noDataViewLogicap } from './no-data-view'; +import { bindRegistryLogicap } from './bind-registry'; +import { paginationViewLogicap } from './pagination-view'; + +export interface Logicaps { + bindRegistry: ReturnType; + noData: ReturnType; + pagination: ReturnType; +} + +export function logicap(extApi: PblNgridInternalExtensionApi): Logicaps { + return { + bindRegistry: bindRegistryLogicap(extApi), + noData: noDataViewLogicap(extApi), + pagination: paginationViewLogicap(extApi), + }; +} diff --git a/libs/ngrid/src/lib/grid/logicap/no-data-view.ts b/libs/ngrid/src/lib/grid/logicap/no-data-view.ts new file mode 100644 index 000000000..48e0c936b --- /dev/null +++ b/libs/ngrid/src/lib/grid/logicap/no-data-view.ts @@ -0,0 +1,41 @@ +import { EmbeddedViewRef } from '@angular/core'; +import { PblNgridInternalExtensionApi } from '../../ext/grid-ext-api'; + +interface NoDataViewLogicap { + (force?: boolean): void; + viewActive?: boolean; +} + +export function noDataViewLogicap(extApi: PblNgridInternalExtensionApi): NoDataViewLogicap { + let noDateEmbeddedVRef: EmbeddedViewRef; + + const logicap: NoDataViewLogicap = (force?: boolean) => { + if (noDateEmbeddedVRef) { + extApi.grid.removeView(noDateEmbeddedVRef, 'beforeContent'); + noDateEmbeddedVRef = undefined; + logicap.viewActive = false; + } + + if (force === false) { + return; + } + + + const noData = extApi.grid.ds && extApi.grid.ds.renderLength === 0; + if (noData) { + extApi.grid.addClass('pbl-ngrid-empty'); + } else { + extApi.grid.removeClass('pbl-ngrid-empty'); + } + + if (noData || force === true) { + const noDataTemplate = extApi.registry.getSingle('noData'); + if (noDataTemplate) { + noDateEmbeddedVRef = extApi.grid.createView('beforeContent', noDataTemplate.tRef, { $implicit: extApi.grid }, 0); + logicap.viewActive = true; + } + } + }; + + return logicap; +} diff --git a/libs/ngrid/src/lib/grid/logicap/pagination-view.ts b/libs/ngrid/src/lib/grid/logicap/pagination-view.ts new file mode 100644 index 000000000..da84edd99 --- /dev/null +++ b/libs/ngrid/src/lib/grid/logicap/pagination-view.ts @@ -0,0 +1,34 @@ +import { EmbeddedViewRef } from '@angular/core'; +import { unrx } from '@pebula/ngrid/core'; +import { PblNgridInternalExtensionApi } from '../../ext/grid-ext-api'; + +export function paginationViewLogicap(extApi: PblNgridInternalExtensionApi) { + const paginationKillKey = 'pblPaginationKillKey'; + let paginatorEmbeddedVRef: EmbeddedViewRef; + + return () => { + const ds = extApi.grid.ds; + const usePagination = ds && extApi.grid.usePagination; + + if (usePagination) { + ds.pagination = extApi.grid.usePagination; + if (ds.paginator) { + ds.paginator.noCacheMode = extApi.grid.noCachePaginator; + } + } + + if (extApi.grid.isInit) { + unrx.kill(extApi.grid, paginationKillKey); + if (paginatorEmbeddedVRef) { + extApi.grid.removeView(paginatorEmbeddedVRef, 'beforeContent'); + paginatorEmbeddedVRef = undefined; + } + if (usePagination) { + const paginatorTemplate = extApi.registry.getSingle('paginator'); + if (paginatorTemplate) { + paginatorEmbeddedVRef = extApi.grid.createView('beforeContent', paginatorTemplate.tRef, { $implicit: extApi.grid }); + } + } + } + } +} diff --git a/libs/ngrid/src/lib/grid/ngrid.component.ts b/libs/ngrid/src/lib/grid/ngrid.component.ts index 4d7963d9f..07d98b4e5 100644 --- a/libs/ngrid/src/lib/grid/ngrid.component.ts +++ b/libs/ngrid/src/lib/grid/ngrid.component.ts @@ -1,5 +1,5 @@ -import { asapScheduler, animationFrameScheduler, fromEventPattern } from 'rxjs'; -import { filter, take, tap, observeOn, switchMap, map, mapTo, startWith, pairwise, debounceTime, skip } from 'rxjs/operators'; +import { asapScheduler, animationFrameScheduler } from 'rxjs'; +import { filter, take, tap, observeOn, switchMap, map, mapTo, startWith, pairwise } from 'rxjs/operators'; import { AfterViewInit, Component, @@ -42,12 +42,10 @@ import { import { EXT_API_TOKEN, PblNgridExtensionApi, PblNgridInternalExtensionApi } from '../ext/grid-ext-api'; import { PblNgridPluginController, PblNgridPluginContext } from '../ext/plugin-control'; -import { resetColumnWidths } from './utils/width'; import { PblCdkTableComponent } from './pbl-cdk-table/pbl-cdk-table.component'; import { PblColumn, PblNgridColumnSet, } from './column/model'; import { PblColumnStore, ColumnApi, AutoSizeToFitOptions } from './column/management'; import { PblNgridCellContext, PblNgridMetaCellContext, PblNgridContextApi, PblNgridRowContext } from './context/index'; -import { DynamicColumnWidthLogic } from './column/width-logic/dynamic-column-width'; import { PblCdkVirtualScrollViewportComponent } from './features/virtual-scroll/virtual-scroll-viewport.component'; import { PblNgridMetaRowService } from './meta-rows/meta-row.service'; @@ -181,7 +179,7 @@ export class PblNgridComponent implements AfterContentInit, AfterViewIn } if ( value !== this._pagination ) { this._pagination = value; - this.setupPaginator(); + this._extApi.logicaps.pagination(); } } @@ -303,8 +301,6 @@ export class PblNgridComponent implements AfterContentInit, AfterViewIn get innerTableMinWidth() { return this._cdkTable?.minWidth } private _store: PblColumnStore; - private _noDateEmbeddedVRef: EmbeddedViewRef; - private _paginatorEmbeddedVRef: EmbeddedViewRef; private _pagination: PblNgridPaginatorKind | false; private _noCachePaginator = false; private _plugin: PblNgridPluginContext; @@ -318,10 +314,12 @@ export class PblNgridComponent implements AfterContentInit, AfterViewIn private ngZone: NgZone, private cdr: ChangeDetectorRef, private config: PblNgridConfigService, + // TODO: Make private in v 4 + /** @deprecated Will be removed in version 4 */ public registry: PblNgridRegistryService, @Attribute('id') public readonly id: string, @Optional() dir?: Directionality) { - this._extApi = createApis(this, { config, ngZone, injector, vcRef, elRef, cdRef: cdr, dir }); + this._extApi = createApis(this, { config, registry, ngZone, injector, vcRef, elRef, cdRef: cdr, dir }); dir?.change .pipe( @@ -347,37 +345,7 @@ export class PblNgridComponent implements AfterContentInit, AfterViewIn } ngAfterContentInit(): void { - // no need to unsubscribe, the reg service is per grid instance and it will destroy when this grid destroy. - // Also, at this point initial changes from templates provided in the content are already inside so they will not trigger - // the order here is very important, because component top of this grid will fire life cycle hooks AFTER this component - // so if we have a top level component registering a template on top it will not show unless we listen. - this.registry.changes.subscribe( changes => { - let gridCell = false; - let headerFooterCell = false; - for (const c of changes) { - switch (c.type) { - case 'tableCell': - gridCell = true; - break; - case 'headerCell': - case 'footerCell': - headerFooterCell = true; - break; - case 'noData': - this.setupNoData(); - break; - case 'paginator': - this.setupPaginator(); - break; - } - } - if (gridCell) { - this._store.attachCustomCellTemplates(); - } - if (headerFooterCell) { - this._store.attachCustomHeaderCellTemplates(); - } - }); + this._extApi.logicaps.bindRegistry(); } ngAfterViewInit(): void { @@ -386,8 +354,7 @@ export class PblNgridComponent implements AfterContentInit, AfterViewIn Object.defineProperty(this, 'isInit', { value: true }); this._plugin.emitEvent({ source: 'grid', kind: 'onInit' }); - this.setupPaginator(); - this.listenToResize(); + this._extApi.logicaps.pagination(); this.contextApi.focusChanged .subscribe( event => { @@ -530,8 +497,8 @@ export class PblNgridComponent implements AfterContentInit, AfterViewIn this._dataSource = value; this._cdkTable.dataSource = value as any; - this.setupPaginator(); - this.setupNoData(false); + this._extApi.logicaps.pagination(); + this._extApi.logicaps.noData(false); if (prev?.hostGrid === this) { prev._detachEmitter(); @@ -596,9 +563,9 @@ export class PblNgridComponent implements AfterContentInit, AfterViewIn startWith(null), pairwise(), tap( ([prev, curr]) => { - const noDataShowing = !!this._noDateEmbeddedVRef; + const noDataShowing = !!this._extApi.logicaps.noData.viewActive; if ( (curr > 0 && noDataShowing) || (curr === 0 && !noDataShowing) ) { - this.setupNoData(); + this._extApi.logicaps.noData(); } }), observeOn(animationFrameScheduler), // ww want to give the browser time to remove/add rows @@ -653,81 +620,6 @@ export class PblNgridComponent implements AfterContentInit, AfterViewIn this._plugin.emitEvent({ source: 'grid', kind: 'onInvalidateHeaders' }); } - /** - * Updates the column sizes for all columns in the grid based on the column definition metadata for each column. - * The final width represent a static width, it is the value as set in the definition (except column without width, where the calculated global width is set). - */ - resetColumnsWidth(): void { - resetColumnWidths(this._store.getStaticWidth(), this._store.visibleColumns, this._store.metaColumns); - } - - /** - * Update the size of all group columns in the grid based on the size of their visible children (not hidden). - * @param dynamicWidthLogic - Optional logic container, if not set a new one is created. - */ - syncColumnGroupsSize(dynamicWidthLogic?: DynamicColumnWidthLogic): void { - if (!dynamicWidthLogic) { - dynamicWidthLogic = this._extApi.dynamicColumnWidthFactory(); - } - - // From all meta columns (header/footer/headerGroup) we filter only `headerGroup` columns. - // For each we calculate it's width from all of the columns that the headerGroup "groups". - // We use the same strategy and the same RowWidthDynamicAggregator instance which will prevent duplicate calculations. - // Note that we might have multiple header groups, i.e. same columns on multiple groups with different row index. - for (const g of this._store.getAllHeaderGroup()) { - // We go over all columns because g.columns does not represent the current owned columns of the group - // it is static, representing the initial state. - // Only columns hold their group owners. - // TODO: find way to improve iteration - const colSizeInfos = this._store.visibleColumns.filter( c => !c.hidden && c.isInGroup(g)).map( c => c.sizeInfo ); - if (colSizeInfos.length > 0) { - const groupWidth = dynamicWidthLogic.addGroup(colSizeInfos); - g.minWidth = groupWidth; - g.updateWidth(`${groupWidth}px`); - } else { - g.minWidth = undefined; - g.updateWidth(`0px`); - } - } - } - - resizeColumns(columns?: PblColumn[]): void { - if (!columns) { - columns = this._store.visibleColumns; - } - - // protect from per-mature resize. - // Will happen on additional header/header-group rows AND ALSO when vScrollNone is set - // This will cause size not to populate because it takes time to render the rows, since it's not virtual and happens immediately. - // TODO: find a better protection. - if (!columns[0].sizeInfo) { - return; - } - - // stores and calculates width for columns added to it. Aggregate's the total width of all added columns. - const rowWidth = this._extApi.dynamicColumnWidthFactory(); - this.syncColumnGroupsSize(rowWidth); - - // if this is a grid without groups - if (rowWidth.minimumRowWidth === 0) { - rowWidth.addGroup(columns.map( c => c.sizeInfo )); - } - - // if the max lock state has changed we need to update re-calculate the static width's again. - if (rowWidth.maxWidthLockChanged) { - resetColumnWidths(this._store.getStaticWidth(), this._store.visibleColumns, this._store.metaColumns); - this.resizeColumns(columns); - return; - } - - this._cdkTable.minWidth = rowWidth.minimumRowWidth; - - this.ngZone.run( () => { - this.rowsApi.syncRows('header'); - this._plugin.emitEvent({ source: 'grid', kind: 'onResizeRow' }); - }); - } - /** * Create an embedded view before or after the user projected content. */ @@ -798,80 +690,6 @@ export class PblNgridComponent implements AfterContentInit, AfterViewIn } } - private listenToResize(): void { - let resizeObserver: ResizeObserver; - const ro$ = fromEventPattern<[ResizeObserverEntry[], ResizeObserver]>( - handler => { - if (!resizeObserver) { - resizeObserver = new ResizeObserver(handler); - resizeObserver.observe(this.elRef.nativeElement); - } - }, - handler => { - if (resizeObserver) { - resizeObserver.unobserve(this.elRef.nativeElement); - resizeObserver.disconnect(); - resizeObserver = undefined; - } - } - ); - - // Skip the first emission - // Debounce all resizes until the next complete animation frame without a resize - // finally maps to the entries collection - // SKIP: We should skip the first emission (`skip(1)`) before we debounce, since its called upon calling "observe" on the resizeObserver. - // The problem is that some grid might require this because they do change size. - // An example is a grid in a mat-tab that is hidden, the grid will hit the resize one when we focus the tab - // which will require a resize handling because it's initial size is 0 - // To workaround this, we only skip elements not yet added to the DOM, which means they will not trigger a resize event. - let skipValue = document.body.contains(this.elRef.nativeElement) ? 1 : 0; - - ro$ - .pipe( - skip(skipValue), - debounceTime(0, animationFrameScheduler), - unrx(this), - ) - .subscribe( (args: [ResizeObserverEntry[], ResizeObserver]) => { - if (skipValue === 0) { - skipValue = 1; - const columns = this._store.visibleColumns; - columns.forEach( c => c.sizeInfo.updateSize() ); - } - this.onResize(args[0]); - }); - } - - private onResize(entries: ResizeObserverEntry[]): void { - this.viewport?.checkViewportSize(); - // this.resetColumnsWidth(); - this.resizeColumns(); - } - - private setupNoData(force?: boolean): void { - if (this._noDateEmbeddedVRef) { - this.removeView(this._noDateEmbeddedVRef, 'beforeContent'); - this._noDateEmbeddedVRef = undefined; - } - if (force === false) { - return; - } - - const noData = this._dataSource && this._dataSource.renderLength === 0; - if (noData) { - this.addClass('pbl-ngrid-empty'); - } else { - this.removeClass('pbl-ngrid-empty'); - } - - if (noData || force === true) { - const noDataTemplate = this.registry.getSingle('noData'); - if (noDataTemplate) { - this._noDateEmbeddedVRef = this.createView('beforeContent', noDataTemplate.tRef, { $implicit: this }, 0); - } - } - } - private getInternalVcRef(location: 'beforeTable' | 'beforeContent' | 'afterContent'): ViewContainerRef { return location === 'beforeTable' ? this._vcRefBeforeTable @@ -879,32 +697,6 @@ export class PblNgridComponent implements AfterContentInit, AfterViewIn ; } - private setupPaginator(): void { - const paginationKillKey = 'pblPaginationKillKey'; - const usePagination = this.ds && this.usePagination; - - if (usePagination) { - this.ds.pagination = this._pagination; - if (this.ds.paginator) { - this.ds.paginator.noCacheMode = this._noCachePaginator; - } - } - - if (this.isInit) { - unrx.kill(this, paginationKillKey); - if (this._paginatorEmbeddedVRef) { - this.removeView(this._paginatorEmbeddedVRef, 'beforeContent'); - this._paginatorEmbeddedVRef = undefined; - } - if (usePagination) { - const paginatorTemplate = this.registry.getSingle('paginator'); - if (paginatorTemplate) { - this._paginatorEmbeddedVRef = this.createView('beforeContent', paginatorTemplate.tRef, { $implicit: this }); - } - } - } - } - private resetHeaderRowDefs(): void { if (this._headerRowDefs) { // The grid header (main, with column names) is always the last row def (index 0)