Skip to content

Commit a42bb10

Browse files
authored
FIX: content duplication on async carousels (#458)
With the previous version when the dataSource input was async and had an element count less than the displayed grid, there was a duplication of elements (you can test it with your example app, setting take(3) to tile.component.ts in a viewport that supports 5 tiles).
1 parent 33862ed commit a42bb10

File tree

1 file changed

+35
-59
lines changed

1 file changed

+35
-59
lines changed

libs/ngu/carousel/src/lib/ngu-carousel/ngu-carousel.component.ts

Lines changed: 35 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
IterableChanges,
1616
IterableDiffer,
1717
IterableDiffers,
18+
NgIterable,
1819
NgZone,
1920
OnDestroy,
2021
OnInit,
@@ -46,7 +47,7 @@ import { NguCarouselHammerManager } from './ngu-carousel-hammer-manager';
4647

4748
type DirectionSymbol = '' | '-';
4849

49-
type NguCarouselDataSource = Observable<any[]> | any[] | null | undefined;
50+
type NguCarouselDataSource<T, U> = (U & NgIterable<T>) | null | undefined;
5051

5152
// This will be provided through Terser global definitions by Angular CLI.
5253
// This is how Angular does tree-shaking internally.
@@ -62,9 +63,9 @@ const NG_DEV_MODE = typeof ngDevMode === 'undefined' || ngDevMode;
6263
providers: [NguCarouselHammerManager]
6364
})
6465
// eslint-disable-next-line @angular-eslint/component-class-suffix
65-
export class NguCarousel<T>
66+
export class NguCarousel<T, U extends NgIterable<T> = NgIterable<T>>
6667
extends NguCarouselStore
67-
implements OnInit, AfterContentInit, AfterViewInit, OnDestroy, DoCheck
68+
implements AfterContentInit, AfterViewInit, OnDestroy, DoCheck
6869
{
6970
/** Public property that may be accessed outside of the component. */
7071
activePoint = 0;
@@ -77,28 +78,18 @@ export class NguCarousel<T>
7778
// eslint-disable-next-line @angular-eslint/no-output-on-prefix
7879
@Output() onMove = new EventEmitter<NguCarousel<T>>();
7980

80-
private _arrayChanges: IterableChanges<{}> | null = null;
81+
private _arrayChanges: IterableChanges<T> | null = null;
8182

8283
@Input()
83-
get dataSource(): NguCarouselDataSource {
84+
get dataSource(): NguCarouselDataSource<T, U> {
8485
return this._dataSource;
8586
}
86-
set dataSource(data: NguCarouselDataSource) {
87+
set dataSource(data: NguCarouselDataSource<T, U>) {
8788
if (data) {
8889
this._switchDataSource(data);
8990
}
9091
}
91-
private _dataSource: NguCarouselDataSource = null;
92-
93-
/**
94-
* `_dataSource` allows multiple values to be set considering nullable and
95-
* observable values. We shouldn't try to get `_dataSource.length` since it
96-
* might be `null|undefined` which will throw an error that property doesn't
97-
* exist on `undefined`. It will also always equal `undefined` on observable.
98-
* We should wait until the observable is unwrapped and then check the length
99-
* of the actual unwrapped data.
100-
*/
101-
private _unwrappedData: any[] = [];
92+
private _dataSource: NguCarouselDataSource<T, U> = null;
10293

10394
private _defaultNodeDef: NguCarouselDefDirective<any> | null;
10495

@@ -145,14 +136,16 @@ export class NguCarousel<T>
145136

146137
private _carouselCssNode: HTMLStyleElement;
147138

148-
private _dataDiffer: IterableDiffer<{}>;
139+
private _dataDiffer: IterableDiffer<T>;
149140

150141
private _styleid: string;
151142

152143
private _pointIndex: number;
153144

154145
private _destroy$ = new Subject<void>();
155146

147+
private ngu_dirty: boolean = true;
148+
156149
/**
157150
* Tracking function that will be used to check the differences in data changes. Used similarly
158151
* to `ngFor` `trackBy` function. Optimize Items operations by identifying a Items based on its data
@@ -189,47 +182,32 @@ export class NguCarousel<T>
189182
this._setupButtonListeners();
190183
}
191184

192-
ngOnInit() {
193-
this._dataDiffer = this._differs
194-
.find([])
195-
.create((index: number, item: any) => (this.trackBy ? this.trackBy(index, item) : item));
196-
}
197-
198185
ngDoCheck() {
199-
this._arrayChanges = this._dataDiffer.diff(this._unwrappedData)!;
200-
if (this._arrayChanges && this._defDirectives) {
201-
this._observeRenderChanges();
186+
if (this.ngu_dirty) {
187+
this.ngu_dirty = false;
188+
const dataStream = this._dataSource;
189+
if (!this._arrayChanges && !!dataStream) {
190+
this._dataDiffer = this._differs
191+
.find(dataStream)
192+
.create((index: number, item: any) => (this.trackBy ? this.trackBy(index, item) : item))!;
193+
}
194+
}
195+
if (this._dataDiffer && this._defDirectives) {
196+
this._arrayChanges = this._dataDiffer.diff(this._dataSource)!;
197+
if (this._arrayChanges) {
198+
this.renderNodeChanges(Array.from(this._dataSource!));
199+
}
202200
}
203201
}
204202

205203
private _switchDataSource(dataSource: any): any {
206204
this._dataSource = dataSource;
207-
if (this._defDirectives) {
208-
this._observeRenderChanges();
209-
}
210-
}
211-
212-
private _observeRenderChanges() {
213-
let dataStream: Observable<any[]> | undefined;
214-
215-
if (this._dataSource instanceof Observable) {
216-
dataStream = this._dataSource;
217-
} else if (Array.isArray(this._dataSource)) {
218-
dataStream = of(this._dataSource);
219-
}
220-
221-
dataStream
222-
?.pipe(takeUntil(merge(this._intervalController$, this._destroy$)))
223-
.subscribe(data => {
224-
this._unwrappedData = data;
225-
this.renderNodeChanges(data);
226-
this.isLast = this._pointIndex === this.currentSlide;
227-
});
205+
this.ngu_dirty = true;
228206
}
229207

230208
private renderNodeChanges(data: any[]) {
231209
if (!this._arrayChanges) return;
232-
210+
this.isLast = this._pointIndex === this.currentSlide;
233211
const viewContainer = this._nodeOutlet.viewContainer;
234212

235213
this._arrayChanges.forEachOperation(
@@ -304,8 +282,6 @@ export class NguCarousel<T>
304282
}
305283

306284
ngAfterContentInit() {
307-
this._observeRenderChanges();
308-
309285
this._cdr.markForCheck();
310286
}
311287

@@ -507,7 +483,7 @@ export class NguCarousel<T>
507483

508484
/** Init carousel point */
509485
private _carouselPoint(): void {
510-
const Nos = this._unwrappedData.length - (this.items - this.slideItems);
486+
const Nos = Array.from(this._dataSource!).length - (this.items - this.slideItems);
511487
this._pointIndex = Math.ceil(Nos / this.slideItems);
512488
const pointers: number[] = [];
513489

@@ -552,7 +528,7 @@ export class NguCarousel<T>
552528
break;
553529
case this._pointIndex - 1:
554530
this._btnBoolean(0, 1);
555-
slideremains = this._unwrappedData.length - this.items;
531+
slideremains = Array.from(this._dataSource!).length - this.items;
556532
break;
557533
default:
558534
this._btnBoolean(0, 0);
@@ -665,7 +641,7 @@ export class NguCarousel<T>
665641
const MoveSlide = currentSlideD + this.slideItems;
666642
this._btnBoolean(0, 1);
667643
if (this.currentSlide === 0) {
668-
currentSlide = this._unwrappedData.length - this.items;
644+
currentSlide = Array.from(this._dataSource!).length - this.items;
669645
itemSpeed = 400;
670646
this._btnBoolean(0, 1);
671647
} else if (this.slideItems >= MoveSlide) {
@@ -683,10 +659,10 @@ export class NguCarousel<T>
683659
this._carouselScrollTwo(Btn, currentSlide, itemSpeed);
684660
} else if (Btn === 1 && ((!this.loop && !this.isLast) || this.loop)) {
685661
if (
686-
this._unwrappedData.length <= this.currentSlide + this.items + this.slideItems &&
662+
Array.from(this._dataSource!).length <= this.currentSlide + this.items + this.slideItems &&
687663
!this.isLast
688664
) {
689-
currentSlide = this._unwrappedData.length - this.items;
665+
currentSlide = Array.from(this._dataSource!).length - this.items;
690666
this._btnBoolean(0, 1);
691667
} else if (this.isLast) {
692668
currentSlide = 0;
@@ -732,7 +708,7 @@ export class NguCarousel<T>
732708
this._setStyle(this._nguItemsContainer.nativeElement, 'transition', ``);
733709
}
734710

735-
this.itemLength = this._unwrappedData.length;
711+
this.itemLength = Array.from(this._dataSource!).length;
736712
this._transformStyle(currentSlide);
737713
this.currentSlide = currentSlide;
738714
this.onMove.emit(this);
@@ -785,7 +761,7 @@ export class NguCarousel<T>
785761
/** this will trigger the carousel to load the items */
786762
private _carouselLoadTrigger(): void {
787763
if (typeof this.inputs.load === 'number') {
788-
this._unwrappedData.length - this.load <= this.currentSlide + this.items &&
764+
Array.from(this._dataSource!).length - this.load <= this.currentSlide + this.items &&
789765
this.carouselLoad.emit(this.currentSlide);
790766
}
791767
}
@@ -957,5 +933,5 @@ export class NguCarousel<T>
957933
);
958934
}
959935

960-
static ngAcceptInputType_dataSource: NguCarouselDataSource;
936+
static ngAcceptInputType_dataSource: NguCarouselDataSource<any, any>;
961937
}

0 commit comments

Comments
 (0)