Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion projects/igniteui-angular/core/src/core/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isPlatformBrowser } from '@angular/common';
import { Injectable, InjectionToken, PLATFORM_ID, inject } from '@angular/core';
import { Injectable, InjectionToken, PLATFORM_ID, inject, afterNextRender, type AfterRenderRef, type Injector } from '@angular/core';
import { mergeWith } from 'lodash-es';
import { NEVER, Observable } from 'rxjs';
import { isDevMode } from '@angular/core';
Expand All @@ -8,6 +8,31 @@ import type { IgxTheme } from '../services/theme/theme.token';
/** @hidden @internal */
export const ELEMENTS_TOKEN = /*@__PURE__*/new InjectionToken<boolean>('elements environment');

/** @hidden @internal */
export type RenderPhase = 'earlyRead' | 'write' | 'mixedReadWrite' | 'read';

interface AfterNextRenderSpec {
earlyRead?: () => void;
write?: () => void;
mixedReadWrite?: () => void;
read?: () => void;
}

/**
* Schedules `callback` to run once after Angular finishes the next render pass.
*
* Central scheduling point for all work that previously waited on `NgZone.onStable`,
* which never emits in zoneless applications. Every deferred render callback in the
* library goes through here, so if the scheduling needs to change (different phase,
* timing or API), change it in this single place.
*
* @hidden @internal
*/
export function runAfterRenderOnce(injector: Injector, callback: () => void, phase: RenderPhase = 'mixedReadWrite'): AfterRenderRef {
const spec: AfterNextRenderSpec = {};
spec[phase as keyof AfterNextRenderSpec] = callback;
return afterNextRender(spec, { injector });
}

/**
* Returns true if the element's direction is left-to-right
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { NgForOfContext } from '@angular/common';
import { ChangeDetectorRef, ComponentRef, Directive, EmbeddedViewRef, EventEmitter, Input, IterableChanges, IterableDiffer, IterableDiffers, NgZone, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, TemplateRef, TrackByFunction, ViewContainerRef, booleanAttribute, DOCUMENT, inject, afterNextRender, runInInjectionContext, EnvironmentInjector, AfterViewInit } from '@angular/core';
import { ChangeDetectorRef, ComponentRef, Directive, EmbeddedViewRef, EventEmitter, Input, IterableChanges, IterableDiffer, IterableDiffers, NgZone, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, TemplateRef, TrackByFunction, ViewContainerRef, booleanAttribute, DOCUMENT, inject, EnvironmentInjector, AfterViewInit } from '@angular/core';

import { DisplayContainerComponent } from './display.container';
import { HVirtualHelperComponent } from './horizontal.virtual.helper.component';
import { VirtualHelperComponent } from './virtual.helper.component';

import { IgxForOfSyncService, IgxForOfScrollSyncService } from './for_of.sync.service';
import { Subject } from 'rxjs';
import { takeUntil, filter, throttleTime, first } from 'rxjs/operators';
import { getResizeObserver } from 'igniteui-angular/core';
import { takeUntil, filter, throttleTime } from 'rxjs/operators';
import { getResizeObserver, runAfterRenderOnce } from 'igniteui-angular/core';
import { IBaseEventArgs, PlatformUtil } from 'igniteui-angular/core';
import { VirtualHelperBaseDirective } from './base.helper.component';

Expand Down Expand Up @@ -658,13 +658,9 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
// Actual scroll delta that was added is smaller than 1 and onScroll handler doesn't trigger when scrolling < 1px
const scrollOffset = this.fixedUpdateAllElements(this._virtScrollPosition);
// scrollOffset = scrollOffset !== parseInt(this.igxForItemSize, 10) ? scrollOffset : 0;
runInInjectionContext(this._injector, () => {
afterNextRender({
write: () => {
this.dc.instance._viewContainer.element.nativeElement.style.transform = `translateY(${-scrollOffset}px)`;
}
});
});
runAfterRenderOnce(this._injector, () => {
this.dc.instance._viewContainer.element.nativeElement.style.transform = `translateY(${-scrollOffset}px)`;
}, 'write');
}

const maxRealScrollTop = this.scrollComponent.nativeElement.scrollHeight - containerSize;
Expand Down Expand Up @@ -911,7 +907,7 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
// in case scrolled to specific index where after scroll heights are changed
// need to adjust the offsets so that item is last in view.
const updatesToIndex = this._adjustToIndex - this.state.startIndex + 1;
const sumDiffs = diffs.slice(0, updatesToIndex).reduce(reducer);
const sumDiffs = diffs.slice(0, updatesToIndex).reduce(reducer, 0);
if (sumDiffs !== 0) {
this.addScroll(sumDiffs);
}
Expand Down Expand Up @@ -961,15 +957,10 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
const prevStartIndex = this.state.startIndex;
const scrollOffset = this.fixedUpdateAllElements(this._virtScrollPosition);

runInInjectionContext(this._injector, () => {
afterNextRender({
write: () => {
this.dc.instance._viewContainer.element.nativeElement.style.transform = `translateY(${-scrollOffset}px)`;
}
});
});

this._zone.onStable.pipe(first()).subscribe(this.recalcUpdateSizes.bind(this));
runAfterRenderOnce(this._injector, () => {
this.dc.instance._viewContainer.element.nativeElement.style.transform = `translateY(${-scrollOffset}px)`;
}, 'write');
runAfterRenderOnce(this._injector, () => this.recalcUpdateSizes());

this.dc.changeDetectorRef.detectChanges();
if (prevStartIndex !== this.state.startIndex) {
Expand Down Expand Up @@ -1181,7 +1172,7 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
} else {
this.dc.instance._viewContainer.element.nativeElement.style.left = -scrollOffset + 'px';
}
this._zone.onStable.pipe(first()).subscribe(this.recalcUpdateSizes.bind(this));
runAfterRenderOnce(this._injector, () => this.recalcUpdateSizes());

this.dc.changeDetectorRef.detectChanges();
if (prevStartIndex !== this.state.startIndex) {
Expand Down Expand Up @@ -1783,14 +1774,10 @@ export class IgxGridForOfDirective<T, U extends T[] = T[]> extends IgxForOfDirec
}
const prevState = Object.assign({}, this.state);
const scrollOffset = this.fixedUpdateAllElements(this._virtScrollPosition);
runInInjectionContext(this._injector, () => {
afterNextRender({
write: () => {
this.dc.instance._viewContainer.element.nativeElement.style.transform = `translateY(${-scrollOffset}px)`;
this._zone.onStable.pipe(first()).subscribe(this.recalcUpdateSizes.bind(this, prevState));
}
});
});
runAfterRenderOnce(this._injector, () => {
this.dc.instance._viewContainer.element.nativeElement.style.transform = `translateY(${-scrollOffset}px)`;
}, 'write');
runAfterRenderOnce(this._injector, () => this.recalcUpdateSizes(prevState));

this.cdr.markForCheck();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export interface IActiveNode {
layout?: IMultiRowLayoutNode;
}

const VERTICAL_VIRTUALIZATION_NAV_KEYS = new Set(['arrowup', 'up', 'arrowdown', 'down', 'home', 'end']);

/** @hidden */
@Injectable()
export class IgxGridNavigationService {
Expand Down Expand Up @@ -106,8 +108,7 @@ export class IgxGridNavigationService {
}
const position = this.getNextPosition(this.activeNode.row, this.activeNode.column, key, shift, ctrl, event);
const shouldNotifyVirtualizedKeyboardSelection =
ctrl && (key === 'arrowup' || key === 'up' || key === 'arrowdown' || key === 'down') &&
this.shouldPerformVerticalScroll(position.rowIndex, position.colIndex);
this.shouldNotifyVirtualizedKeyboardSelection(key, position.rowIndex, position.colIndex);
if (NAVIGATION_KEYS.has(key)) {
event.preventDefault();
this.navigateInBody(position.rowIndex, position.colIndex, (obj) => {
Expand Down Expand Up @@ -231,6 +232,16 @@ export class IgxGridNavigationService {
|| containerHeight && endTopOffset - containerHeight > 5;
}

protected shouldNotifyVirtualizedKeyboardSelection(key: string, rowIndex: number, visibleColIndex: number): boolean {
// Any navigation key that ends up scrolling activates the target cell from the
// virtualization scroll callback, which runs outside Angular's knowledge, so the
// grid must be notified explicitly regardless of the ctrl modifier.
const shouldCheckVerticalScroll = VERTICAL_VIRTUALIZATION_NAV_KEYS.has(key);
const shouldCheckHorizontalScroll = HORIZONTAL_NAV_KEYS.has(key);

return (shouldCheckVerticalScroll && this.shouldPerformVerticalScroll(rowIndex, visibleColIndex)) ||
(shouldCheckHorizontalScroll && this.shouldPerformHorizontalScroll(visibleColIndex, rowIndex));
}
public performVerticalScrollToCell(rowIndex: number, visibleColIndex = -1, cb?: () => void) {
if (!this.shouldPerformVerticalScroll(rowIndex, visibleColIndex)) {
if (cb) {
Expand Down
19 changes: 11 additions & 8 deletions projects/igniteui-angular/grids/grid/src/column-group.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,19 +608,22 @@ describe('IgxGrid - multi-column headers #grid', () => {
grid = fixture.componentInstance.grid;
}));

it('Width should be correct. Column group with three columns. No width.', () => {
it('Width should be correct. Column group with three columns. No width.', async () => {
await wait(16);
fixture.detectChanges();
const scrWitdh = grid.nativeElement.querySelector('.igx-grid__tbody-scrollbar').getBoundingClientRect().width;
const availableWidth = (parseInt(componentInstance.gridWrapperWidthPx, 10) - scrWitdh).toString();
const availableWidth = parseInt(componentInstance.gridWrapperWidthPx, 10) - scrWitdh;
const locationColGroup = getColGroup(grid, 'Location');
const colWidth = Math.floor(parseInt(availableWidth, 10) / 3);
const colWidthPx = colWidth + 'px';
expect(locationColGroup.width).toBe((Math.round(colWidth) * 3) + 'px');
const colWidth = availableWidth / 3;
const expectWidthWithinPixel = (actualWidth: string, expectedWidth: number) =>
expect(Math.abs(parseFloat(actualWidth) - expectedWidth)).toBeLessThanOrEqual(1);
expectWidthWithinPixel(locationColGroup.width, availableWidth);
const countryColumn = grid.getColumnByName('Country');
expect(countryColumn.width).toBe(colWidthPx);
expectWidthWithinPixel(countryColumn.width, colWidth);
const regionColumn = grid.getColumnByName('Region');
expect(regionColumn.width).toBe(colWidthPx);
expectWidthWithinPixel(regionColumn.width, colWidth);
const cityColumn = grid.getColumnByName('City');
expect(cityColumn.width).toBe(colWidthPx);
expectWidthWithinPixel(cityColumn.width, colWidth);
});

it('Width should be correct. Column group with three columns. Width in px.', () => {
Expand Down
29 changes: 28 additions & 1 deletion projects/igniteui-angular/grids/grid/src/column.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, DebugElement, TemplateRef, ViewChild } from '@angular/core';
import { Component, DebugElement, TemplateRef, ViewChild, provideZonelessChangeDetection } from '@angular/core';
import { TestBed, fakeAsync, tick, waitForAsync, ComponentFixture } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { getLocaleCurrencySymbol, registerLocaleData } from '@angular/common';
Expand Down Expand Up @@ -1935,3 +1935,30 @@ export class DOMAttributesAsSettersComponent {

public data = [{ id: 1, value: 1 }];
}

describe('IgxGrid column autosizing in zoneless change detection #grid', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ResizableColumnsComponent, NoopAnimationsModule],
providers: [provideZonelessChangeDetection()]
});
});

it('should recalculate fit-content widths after data changes', async () => {
const fix = TestBed.createComponent(ResizableColumnsComponent);
fix.detectChanges();
await fix.whenStable();
const grid = fix.componentInstance.instance;

grid.data = [{
ID: 'VeryVeryVeryLongID',
Address: 'Avda. de la Constituci\u00f3n 2222 Obere Str. 57'
}];
await fix.whenStable();
grid.recalculateAutoSizes();
await fix.whenStable();

expect(grid.columns[0].width).toBe('164px');
expect(grid.columns[1].width).toBe('279px');
});
});
39 changes: 10 additions & 29 deletions projects/igniteui-angular/grids/grid/src/grid-base.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ import {
IGridResourceStrings,
IgxOverlayOutletDirective,
DEFAULT_LOCALE,
onResourceChangeHandle
onResourceChangeHandle,
runAfterRenderOnce
} from 'igniteui-angular/core';
import { IgcTrialWatermark } from 'igniteui-trial-watermark';
import { Subject, pipe, fromEvent, animationFrameScheduler, merge, BehaviorSubject, timer } from 'rxjs';
Expand Down Expand Up @@ -4188,9 +4189,7 @@ export abstract class IgxGridBaseDirective implements GridType,
if (this.hasColumnsToAutosize) {
this.headerContainer?.dataChanged.pipe(takeUntil(this.destroy$)).subscribe(() => {
this.cdr.detectChanges();
this.zone.onStable.pipe(first()).subscribe(() => {
this.autoSizeColumnsInView();
});
runAfterRenderOnce(this.injector, () => this.autoSizeColumnsInView());
});
}
// Window resize observer not needed because when you resize the window element the tbody container always resize so
Expand Down Expand Up @@ -4703,7 +4702,7 @@ export abstract class IgxGridBaseDirective implements GridType,
// reset auto-size and calculate it again.
this._columns.forEach(x => x.autoSize = undefined);
this.resetCaches();
this.zone.onStable.pipe(first()).subscribe(() => {
runAfterRenderOnce(this.injector, () => {
this.cdr.detectChanges();
this.autoSizeColumnsInView();
});
Expand Down Expand Up @@ -6399,7 +6398,7 @@ export abstract class IgxGridBaseDirective implements GridType,
const tmplId = args.context.templateID.type;
const index = args.context.index;
args.view.detectChanges();
this.zone.onStable.pipe(first()).subscribe(() => {
runAfterRenderOnce(this.injector, () => {
const row = tmplId === 'dataRow' ? this.gridAPI.get_row_by_index(index) : null;
const summaryRow = tmplId === 'summaryRow' ? this.summariesRowList.find((sr) => sr.dataRowIndex === index) : null;
if (row && row instanceof IgxRowDirective) {
Expand Down Expand Up @@ -7098,24 +7097,16 @@ export abstract class IgxGridBaseDirective implements GridType,
this.cdr.detectChanges();
}

if (this.zone.isStable) {
runAfterRenderOnce(this.injector, () => {
this.zone.run(() => {
this._applyWidthHostBinding();
this.cdr.detectChanges();
});
} else {
this.zone.onStable.pipe(first()).subscribe(() => {
this.zone.run(() => {
this._applyWidthHostBinding();
});
});
}
});
this.resetCaches(recalcFeatureWidth);
if (this.hasColumnsToAutosize) {
this.cdr.detectChanges();
this.zone.onStable.pipe(first()).subscribe(() => {
this._autoSizeColumnsNotify.next();
});
runAfterRenderOnce(this.injector, () => this._autoSizeColumnsNotify.next());
}

// in case horizontal scrollbar has appeared recalc to size correctly.
Expand Down Expand Up @@ -7766,19 +7757,13 @@ export abstract class IgxGridBaseDirective implements GridType,
protected verticalScrollHandler(event) {
this.verticalScrollContainer.onScroll(event);
this.disableTransitions = true;

const callback = () => {
this.verticalScrollContainer.chunkLoad.emit(this.verticalScrollContainer.state);
if (this.rowEditable) {
this.changeRowEditingOverlayStateOnScroll(this.crudService.rowInEditMode);
}
};
if (this.isZonelessChangeDetection()) {
this.cdr.detectChanges();
callback();
} else {
this.zone.onStable.pipe(first()).subscribe(callback);
}
runAfterRenderOnce(this.injector, callback);
this.disableTransitions = false;

this.hideOverlays();
Expand All @@ -7803,10 +7788,6 @@ export abstract class IgxGridBaseDirective implements GridType,
this.gridScroll.emit(args);
}

protected isZonelessChangeDetection(): boolean {
return this.zone.constructor.name === 'NoopNgZone';
}

protected hasMenuPinningActions(): boolean {
const strip = this.actionStrip;
const actionButtons = strip?.actionButtons;
Expand All @@ -7831,7 +7812,7 @@ export abstract class IgxGridBaseDirective implements GridType,
this.cdr.markForCheck();

this.zone.run(() => {
this.zone.onStable.pipe(first()).subscribe(() => {
runAfterRenderOnce(this.injector, () => {
this.parentVirtDir.chunkLoad.emit(this.headerContainer.state);
requestAnimationFrame(() => {
this.autoSizeColumnsInView();
Expand Down
Loading
Loading