All files / src/app/pages/product/product-links-carousel product-links-carousel.component.ts

88.37% Statements 38/43
68% Branches 17/25
93.75% Functions 15/16
86.48% Lines 32/37

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 1411x 1x 1x 1x 1x   1x 1x 1x 1x 1x 1x   1x                                 1x         3x                   2x     3x         3x                     3x 3x             3x         3x                                             3x       4x 1x 3x     4x 1x 1x       3x         9x         15x   3x                          
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 { InjectSingle } from 'ish-core/utils/injection';
import { mapToProperty } from 'ish-core/utils/operators';
 
SwiperCore.use([Navigation, Pagination]);
 
/**
 * The Product Link Carousel Component
 *
 * Displays the products which are assigned to a specific product link type as an carousel.
 * It uses the {@link ProductItemComponent} for the rendering of products.
 *
 * @example
 * <ish-product-links-carousel [links]="links.crossselling" [productLinkTitle]="'product.product_links.crossselling.title' | translate"></ish-product-links-carousel>
 */
@Component({
  selector: 'ish-product-links-carousel',
  templateUrl: './product-links-carousel.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [RxState],
})
export class ProductLinksCarouselComponent {
  /**
   * list of products which are assigned to the specific product link type
   */
  @Input({ required: true }) set links(links: ProductLinks) {
    this.state.set('products', () => links.products);
  }
  /**
   * title that should displayed for the specific product link type
   */
  @Input({ required: true }) productLinkTitle: string;
  /**
   * display only available products if set to 'true'
   */
  @Input() set displayOnlyAvailableProducts(value: boolean) {
    this.state.set('displayOnlyAvailableProducts', () => value);
  }
 
  productSKUs$ = this.state.select('products$');
 
  /**
   * track already fetched SKUs
   */
  private fetchedSKUs = new Set<Observable<string>>();
 
  /**
   * configuration of swiper carousel
   * https://swiperjs.com/swiper-api
   */
  swiperConfig: SwiperOptions;
 
  constructor(
    @Inject(LARGE_BREAKPOINT_WIDTH) largeBreakpointWidth: InjectSingle<typeof LARGE_BREAKPOINT_WIDTH>,
    @Inject(MEDIUM_BREAKPOINT_WIDTH) mediumBreakpointWidth: InjectSingle<typeof MEDIUM_BREAKPOINT_WIDTH>,
    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: {
        clickable: true,
      },
      breakpoints: {
        0: {
          slidesPerView: 2,
          slidesPerGroup: 2,
        },
        [mediumBreakpointWidth]: {
          slidesPerView: 3,
          slidesPerGroup: 3,
        },
        [largeBreakpointWidth]: {
          slidesPerView: 4,
          slidesPerGroup: 4,
        },
      },
    };
 
    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$);
  }
 
  lazyFetch(fetch: boolean, sku$: Observable<string>): Observable<string> {
    Iif (fetch) {
      this.fetchedSKUs.add(sku$);
    }
    Iif (this.fetchedSKUs.has(sku$)) {
      return sku$;
    }
    return EMPTY;
  }
}