Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: lazy check for available product links #1162

Merged
merged 2 commits into from May 20, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -4,8 +4,12 @@ <h2>{{ productLinkTitle }}</h2>

<div class="product-list">
<swiper [config]="swiperConfig">
<ng-template *ngFor="let sku of productSKUs" swiperSlide>
<ish-product-item ishProductContext [sku]="sku"></ish-product-item>
<ng-template *ngFor="let sku$ of productSKUs" swiperSlide let-data>
<ish-product-item
*ngIf="lazyFetch(data.isVisible, sku$) | async as sku"
ishProductContext
[sku]="sku"
></ish-product-item>
</ng-template>
</swiper>
</div>
Expand Down
@@ -1,6 +1,6 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MockComponent } from 'ng-mocks';
import { of } from 'rxjs';
import { forkJoin, of, switchMap } from 'rxjs';
import { SwiperComponent } from 'swiper/angular';
import { anything, instance, mock, when } from 'ts-mockito';

Expand Down Expand Up @@ -37,16 +37,14 @@ describe('Product Links Carousel Component', () => {
it('should be created', () => {
expect(component).toBeTruthy();
expect(element).toBeTruthy();
expect(() => component.ngOnChanges()).not.toThrow();
expect(() => fixture.detectChanges()).not.toThrow();
expect(element.querySelector('swiper')).toBeTruthy();
});

it('should render all product slides if stocks filtering is off', done => {
component.displayOnlyAvailableProducts = false;
component.ngOnChanges();

component.productSKUs$.subscribe(products => {
component.productSKUs$.pipe(switchMap(products$ => forkJoin(products$))).subscribe(products => {
expect(products).toHaveLength(3);
done();
});
Expand All @@ -58,9 +56,8 @@ describe('Product Links Carousel Component', () => {
);

component.displayOnlyAvailableProducts = true;
component.ngOnChanges();

component.productSKUs$.subscribe(products => {
component.productSKUs$.pipe(switchMap(products$ => forkJoin(products$))).subscribe(products => {
expect(products).toHaveLength(2);
done();
});
Expand Down
@@ -1,12 +1,14 @@
import { ChangeDetectionStrategy, Component, Inject, Input, OnChanges } from '@angular/core';
import { Observable, combineLatest, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { ChangeDetectionStrategy, Component, Inject, Input } from '@angular/core';
import { RxState } from '@rx-angular/state';
import { EMPTY, Observable, combineLatest, of } from 'rxjs';
import { filter, map, tap } from 'rxjs/operators';
import SwiperCore, { Navigation, Pagination, SwiperOptions } from 'swiper';

import { LARGE_BREAKPOINT_WIDTH, MEDIUM_BREAKPOINT_WIDTH } from 'ish-core/configurations/injection-keys';
import { ShoppingFacade } from 'ish-core/facades/shopping.facade';
import { ProductLinks } from 'ish-core/models/product-links/product-links.model';
import { ProductCompletenessLevel } from 'ish-core/models/product/product.model';
import { mapToProperty } from 'ish-core/utils/operators';

SwiperCore.use([Navigation, Pagination]);

Expand All @@ -23,22 +25,32 @@ SwiperCore.use([Navigation, Pagination]);
selector: 'ish-product-links-carousel',
templateUrl: './product-links-carousel.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [RxState],
})
export class ProductLinksCarouselComponent implements OnChanges {
export class ProductLinksCarouselComponent {
/**
* list of products which are assigned to the specific product link type
*/
@Input() links: ProductLinks;
@Input() set links(links: ProductLinks) {
this.state.set('products', () => links.products);
}
/**
* title that should displayed for the specific product link type
*/
@Input() productLinkTitle: string;
/**
* display only available products if set to 'true'
*/
@Input() displayOnlyAvailableProducts = false;
@Input() set displayOnlyAvailableProducts(value: boolean) {
this.state.set('displayOnlyAvailableProducts', () => value);
}

productSKUs$: Observable<string[]>;
productSKUs$ = this.state.select('products$');

/**
* track already fetched SKUs
*/
private fetchedSKUs = new Set<Observable<string>>();

/**
* configuration of swiper carousel
Expand All @@ -49,9 +61,21 @@ export class ProductLinksCarouselComponent implements OnChanges {
constructor(
@Inject(LARGE_BREAKPOINT_WIDTH) largeBreakpointWidth: number,
@Inject(MEDIUM_BREAKPOINT_WIDTH) mediumBreakpointWidth: number,
private shoppingFacade: ShoppingFacade
private shoppingFacade: ShoppingFacade,
private state: RxState<{
products: string[];
displayOnlyAvailableProducts: boolean;
hiddenSlides: number[];
products$: Observable<string>[];
}>
) {
this.state.set(() => ({
hiddenSlides: [],
displayOnlyAvailableProducts: false,
}));

this.swiperConfig = {
watchSlidesProgress: true,
direction: 'horizontal',
navigation: true,
pagination: {
Expand All @@ -72,13 +96,44 @@ export class ProductLinksCarouselComponent implements OnChanges {
},
},
};

const filteredProducts$ = combineLatest([
combineLatest([this.state.select('products'), this.state.select('displayOnlyAvailableProducts')]).pipe(
map(([products, displayOnlyAvailableProducts]) => {
// prepare lazy observables for all products
if (displayOnlyAvailableProducts) {
return products.map((sku, index) =>
this.shoppingFacade.product$(sku, ProductCompletenessLevel.List).pipe(
tap(product => {
// add slide to the hidden list if product is not available
if (!product.available || product.failed) {
this.state.set('hiddenSlides', () =>
[...this.state.get('hiddenSlides'), index].filter((v, i, a) => a.indexOf(v) === i)
);
}
}),
filter(product => product.available && !product.failed),
mapToProperty('sku')
)
);
} else {
return products.map(sku => of(sku));
}
})
),
this.state.select('hiddenSlides'),
]).pipe(map(([products, hiddenSlides]) => products.filter((_, index) => !hiddenSlides.includes(index))));

this.state.connect('products$', filteredProducts$);
}

ngOnChanges() {
this.productSKUs$ = this.displayOnlyAvailableProducts
? combineLatest(
this.links.products.map(sku => this.shoppingFacade.product$(sku, ProductCompletenessLevel.List))
).pipe(map(products => products.filter(p => p.available).map(p => p.sku)))
: of(this.links.products);
lazyFetch(fetch: boolean, sku$: Observable<string>): Observable<string> {
if (fetch) {
this.fetchedSKUs.add(sku$);
}
if (this.fetchedSKUs.has(sku$)) {
return sku$;
}
return EMPTY;
}
}