Skip to content
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NgForOfContext } from '@angular/common';
import { ChangeDetectorRef, ComponentRef, Directive, EmbeddedViewRef, EventEmitter, Input, IterableChanges, IterableDiffer, IterableDiffers, NgZone, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, TemplateRef, TrackByFunction, ViewContainerRef, AfterViewInit, booleanAttribute, DOCUMENT, inject, afterNextRender, runInInjectionContext, EnvironmentInjector } 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, afterNextRender, runInInjectionContext, EnvironmentInjector } from '@angular/core';

import { DisplayContainerComponent } from './display.container';
import { HVirtualHelperComponent } from './horizontal.virtual.helper.component';
Expand Down Expand Up @@ -84,7 +84,7 @@ export abstract class IgxForOfToken<T, U extends T[] = T[]> {
],
standalone: true
})
export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U> implements OnInit, OnChanges, OnDestroy, AfterViewInit {
export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U> implements OnInit, OnChanges, OnDestroy {
private _viewContainer = inject(ViewContainerRef);
protected _template = inject<TemplateRef<NgForOfContext<T>>>(TemplateRef);
protected _differs = inject(IterableDiffers);
Expand All @@ -95,6 +95,7 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
protected platformUtil = inject(PlatformUtil);
protected document = inject(DOCUMENT);
private _igxForOf: U & T[] | null = null;
private _embeddedViewSizesCache = new Map<EmbeddedViewRef<any>, number>();

/**
* Sets the data to be rendered.
Expand Down Expand Up @@ -272,7 +273,7 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
protected _bScrollInternal = false;
// End properties related to virtual height handling
protected _embeddedViews: Array<EmbeddedViewRef<any>> = [];
protected contentResizeNotify = new Subject<void>();
protected contentResizeNotify = new Subject<ResizeObserverEntry[]>();
protected contentObserver: ResizeObserver;
/** Size that is being virtualized. */
protected _virtSize = 0;
Expand Down Expand Up @@ -470,9 +471,8 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
const destructor = takeUntil<any>(this.destroy$);
this.contentResizeNotify.pipe(
filter(() => this.igxForContainerSize && this.igxForOf && this.igxForOf.length > 0),
throttleTime(40, undefined, { leading: false, trailing: true }),
destructor
).subscribe(() => this._zone.runTask(() => this.updateSizes()));
).subscribe((entries: ResizeObserverEntry[]) => this._zone.runTask(() => this.updateSizes(entries)));
}

if (this.igxForScrollOrientation === 'horizontal') {
Expand All @@ -498,12 +498,13 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
this.resolveDataDiff();
}

public ngAfterViewInit(): void {
protected subscribeToObserver(target: Element) {
if (this.igxForScrollOrientation === 'vertical') {
this._zone.runOutsideAngular(() => {
// console.log(target);
if (this.platformUtil.isBrowser) {
this.contentObserver = new (getResizeObserver())(() => this.contentResizeNotify.next());
this.contentObserver.observe(this.dc.instance._viewContainer.element.nativeElement);
this.contentObserver = new (getResizeObserver())((entries: ResizeObserverEntry[]) => this.contentResizeNotify.next(entries));
this.contentObserver.observe(target);
}
});
}
Expand Down Expand Up @@ -819,36 +820,39 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
|| containerSize && endTopOffset - containerSize > 5;
}


/**
* @hidden
* Function that recalculates and updates cache sizes.
*/
public recalcUpdateSizes() {
const dimension = this.igxForScrollOrientation === 'horizontal' ?
this.igxForSizePropName : 'height';
public recalcUpdateSizes(prevState?: IForOfState) {
if (prevState && prevState.startIndex === this.state.startIndex && prevState.chunkSize === this.state.chunkSize) {
// nothing changed
return;
}
const diffs = [];
let totalDiff = 0;
const l = this._embeddedViews.length;
const rNodes = this.embeddedViewNodes;
for (let i = 0; i < l; i++) {
const rNode = rNodes[i];
if (rNode) {
const height = window.getComputedStyle(rNode).getPropertyValue('height');
const h = parseFloat(height) || parseInt(this.igxForItemSize, 10);
const index = this.state.startIndex + i;
if (!this.isRemote && !this.igxForOf[index]) {
continue;
}
const margin = this.getMargin(rNode, dimension);
const oldVal = this.individualSizeCache[index];
const newVal = (dimension === 'height' ? h : rNode.clientWidth) + margin;
this.individualSizeCache[index] = newVal;
const currDiff = newVal - oldVal;
diffs.push(currDiff);
totalDiff += currDiff;
this.sizesCache[index + 1] = (this.sizesCache[index] || 0) + newVal;

for (let index = 0; index < this._embeddedViews.length; index++) {
const targetIndex = this.state.startIndex + index;
const view = this._embeddedViews[index];
const cachedNodeSize = this._embeddedViewSizesCache.get(view);
const oldVal = this.individualSizeCache[targetIndex];
const newVal = cachedNodeSize;
if (!newVal) {
continue;
}
const currDiff = newVal - oldVal;
// if (currDiff > 0) {
// console.log('RECALC Size increased for index ', targetIndex, ' from ', oldVal, ' to ', newVal);
// }
diffs.push(currDiff);
totalDiff += currDiff;
this.individualSizeCache[targetIndex] = cachedNodeSize;
this.sizesCache[targetIndex + 1] = (this.sizesCache[targetIndex] || 0) + newVal;
}

// console.log(this.individualSizeCache);
// update cache
if (Math.abs(totalDiff) > 0) {
for (let j = this.state.startIndex + this.state.chunkSize + 1; j < this.sizesCache.length; j++) {
Expand Down Expand Up @@ -959,17 +963,16 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
}
}

protected updateSizes() {
if (!this.scrollComponent.nativeElement.isConnected) return;
const scrollable = this.isScrollable();
protected updateSizes(entries:ResizeObserverEntry[] ) {
entries.forEach((entry) => {
const index = parseInt(entry.target.getAttribute('data-index'), 0);
const height = entry.contentRect.height;
const embView = this._embeddedViews[index - this.state.startIndex];
this._embeddedViewSizesCache.set(embView, height);
// console.log('Observed size change for index ', index, ' new size ', height);
// console.log(this._embeddedViewSizesCache);
});
this.recalcUpdateSizes();
this._applyChanges();
this._updateScrollOffset();
if (scrollable !== this.isScrollable()) {
this.scrollbarVisibilityChanged.emit();
} else {
this.contentSizeChange.emit();
}
}

/**
Expand Down Expand Up @@ -1415,9 +1418,11 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
protected removeLastElem() {
const oldElem = this._embeddedViews.pop();
this.beforeViewDestroyed.emit(oldElem);
this.contentObserver.unobserve(oldElem.rootNodes.find(node => node.nodeType === Node.ELEMENT_NODE) || oldElem.rootNodes[0].nextElementSibling);
// also detach from ViewContainerRef to make absolutely sure this is removed from the view container.
this.dc.instance._vcr.detach(this.dc.instance._vcr.length - 1);
oldElem.destroy();
this._embeddedViewSizesCache.delete(oldElem);

this.state.chunkSize--;
}
Expand Down Expand Up @@ -1598,9 +1603,9 @@ export class IgxGridForOfDirective<T, U extends T[] = T[]> extends IgxForOfDirec
return this.igxForSizePropName || 'height';
}

public override recalcUpdateSizes() {
public override recalcUpdateSizes(prevState?: IForOfState) {
if (this.igxGridForOfVariableSizes && this.igxForScrollOrientation === 'vertical') {
super.recalcUpdateSizes();
super.recalcUpdateSizes(prevState);
}
}

Expand Down Expand Up @@ -1714,12 +1719,13 @@ export class IgxGridForOfDirective<T, U extends T[] = T[]> extends IgxForOfDirec
} else {
this._bScrollInternal = false;
}
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));
this._zone.onStable.pipe(first()).subscribe(this.recalcUpdateSizes.bind(this, prevState));
}
});
});
Expand Down Expand Up @@ -1851,6 +1857,7 @@ export class IgxGridForOfDirective<T, U extends T[] = T[]> extends IgxForOfDirec
);

this._embeddedViews.push(embeddedView);
this.subscribeToObserver(embeddedView.rootNodes.find(node => node.nodeType === Node.ELEMENT_NODE) || embeddedView.rootNodes[0].nextElementSibling);
this.state.chunkSize++;
}

Expand Down
2 changes: 2 additions & 0 deletions projects/igniteui-angular/grids/grid/src/grid.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
[igxForItemSize]="hasColumnLayouts ? rowHeight * multiRowLayoutRowSize + 1 : renderedRowHeight"
[igxForTrackBy]="trackChanges"
#verticalScrollContainer (chunkPreload)="dataLoading($event)" (dataChanging)="dataRebinding($event)" (dataChanged)="dataRebound($event)">
<div [attr.data-index]="rowIndex">
<ng-template
[igxTemplateOutlet]="getRowTemplate(rowData)"
[igxTemplateOutletContext]="getContext(rowData, rowIndex)"
Expand All @@ -112,6 +113,7 @@
(beforeViewDetach)="viewDetachHandler($event)"
(viewMoved)="viewMovedHandler($event)">
</ng-template>
</div>
</ng-template>
<ng-container *ngTemplateOutlet="hasPinnedRecords && !isRowPinningToTop ? pinnedRecordsTemplate : null">
</ng-container>
Expand Down
Loading