diff --git a/src/app/shared/components/product/product-item/product-item.component.ts b/src/app/shared/components/product/product-item/product-item.component.ts index 7cf8df8461..4e967c7f75 100644 --- a/src/app/shared/components/product/product-item/product-item.component.ts +++ b/src/app/shared/components/product/product-item/product-item.component.ts @@ -14,7 +14,7 @@ import { ProductView } from 'ish-core/models/product-view/product-view.model'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ProductItemComponent implements OnInit { - @Input() displayType: 'tile' | 'row' = 'tile'; + @Input() displayType: 'tile' | 'row' | string = 'tile'; product$: Observable; loading$: Observable; diff --git a/src/app/shared/components/product/products-list/products-list.component.html b/src/app/shared/components/product/products-list/products-list.component.html new file mode 100644 index 0000000000..03c729b73b --- /dev/null +++ b/src/app/shared/components/product/products-list/products-list.component.html @@ -0,0 +1,19 @@ + +
+ + +
+ +
+
+
+
+
+ + +
+
+ +
+
+
diff --git a/src/app/shared/components/product/products-list/products-list.component.spec.ts b/src/app/shared/components/product/products-list/products-list.component.spec.ts new file mode 100644 index 0000000000..ac61ad6717 --- /dev/null +++ b/src/app/shared/components/product/products-list/products-list.component.spec.ts @@ -0,0 +1,92 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { MockComponent, MockDirective } from 'ng-mocks'; +import { SwiperComponent } from 'swiper/angular'; + +import { ProductContextDirective } from 'ish-core/directives/product-context.directive'; +import { ProductItemComponent } from 'ish-shared/components/product/product-item/product-item.component'; + +import { ProductsListComponent } from './products-list.component'; + +describe('Products List Component', () => { + let component: ProductsListComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ + MockComponent(ProductItemComponent), + MockComponent(SwiperComponent), + MockDirective(ProductContextDirective), + ProductsListComponent, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ProductsListComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); + + describe('carousel', () => { + beforeEach(() => { + component.productSKUs = ['1', '2']; + }); + + it('should display a carousel when listStyle is set to carousel', () => { + component.listStyle = 'carousel'; + + fixture.detectChanges(); + + expect(element).toMatchInlineSnapshot(`
`); + }); + }); + + it('should set displayType of product item to listItemStyle value', () => { + component.productSKUs = ['1', '2']; + component.listItemStyle = 'tile'; + + fixture.detectChanges(); + + const productItem = fixture.debugElement.query(By.css('ish-product-item')) + .componentInstance as ProductItemComponent; + + expect(productItem.displayType).toEqual('tile'); + }); + + it('should display product items for all product skus', () => { + component.productSKUs = ['1', '2', '3']; + component.listItemStyle = 'row'; + + fixture.detectChanges(); + + expect(element.querySelectorAll('ish-product-item')).toHaveLength(3); + expect(element.querySelectorAll('ish-product-item')).toMatchInlineSnapshot(` + NodeList [ + , + , + , + ] + `); + }); +}); diff --git a/src/app/shared/components/product/products-list/products-list.component.ts b/src/app/shared/components/product/products-list/products-list.component.ts new file mode 100644 index 0000000000..b85aef7e92 --- /dev/null +++ b/src/app/shared/components/product/products-list/products-list.component.ts @@ -0,0 +1,119 @@ +import { ChangeDetectionStrategy, Component, Inject, Input, OnChanges } from '@angular/core'; +import { SwiperOptions } from 'swiper'; +import SwiperCore, { Navigation, Pagination } from 'swiper/core'; + +import { + LARGE_BREAKPOINT_WIDTH, + MEDIUM_BREAKPOINT_WIDTH, + SMALL_BREAKPOINT_WIDTH, +} from 'ish-core/configurations/injection-keys'; + +SwiperCore.use([Pagination, Navigation]); + +@Component({ + selector: 'ish-products-list', + templateUrl: './products-list.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ProductsListComponent implements OnChanges { + @Input() productSKUs: string[]; + @Input() listStyle: string; + @Input() slideItems: number; + @Input() listItemStyle: string; + @Input() listItemCSSClass: string; + + /** + * configuration of swiper carousel + * https://swiperjs.com/swiper-api + */ + swiperConfig: SwiperOptions; + + constructor( + @Inject(SMALL_BREAKPOINT_WIDTH) private smallBreakpointWidth: number, + @Inject(MEDIUM_BREAKPOINT_WIDTH) private mediumBreakpointWidth: number, + @Inject(LARGE_BREAKPOINT_WIDTH) private largeBreakpointWidth: number + ) { + this.swiperConfig = { + direction: 'horizontal', + navigation: true, + pagination: { + clickable: true, + }, + observer: true, + observeParents: true, + }; + } + + ngOnChanges(): void { + this.configureSlides(this.slideItems); + } + + /** + * Configure Swipers slidesPerView/slidesPerGroup settings + * with breakpoint responsive design considerations based on the given slide items. + * @param slideItems The amount of slide items that should be rendered if enough screen space is available. + */ + configureSlides(slideItems: number) { + switch (slideItems) { + case 1: { + this.swiperConfig.breakpoints = { + 0: { + slidesPerView: 1, + slidesPerGroup: 1, + }, + }; + break; + } + case 2: { + this.swiperConfig.breakpoints = { + 0: { + slidesPerView: 1, + slidesPerGroup: 1, + }, + [this.smallBreakpointWidth]: { + slidesPerView: 2, + slidesPerGroup: 2, + }, + }; + break; + } + case 3: { + this.swiperConfig.breakpoints = { + 0: { + slidesPerView: 1, + slidesPerGroup: 1, + }, + [this.smallBreakpointWidth]: { + slidesPerView: 2, + slidesPerGroup: 2, + }, + [this.mediumBreakpointWidth]: { + slidesPerView: 3, + slidesPerGroup: 3, + }, + }; + break; + } + default: { + this.swiperConfig.breakpoints = { + 0: { + slidesPerView: 1, + slidesPerGroup: 1, + }, + [this.smallBreakpointWidth]: { + slidesPerView: 2, + slidesPerGroup: 2, + }, + [this.mediumBreakpointWidth]: { + slidesPerView: 3, + slidesPerGroup: 3, + }, + [this.largeBreakpointWidth]: { + slidesPerView: 4, + slidesPerGroup: 4, + }, + }; + } + } + } +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index e53219cf10..2983cd076f 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -118,6 +118,7 @@ import { ProductShipmentComponent } from './components/product/product-shipment/ import { ProductTileComponent } from './components/product/product-tile/product-tile.component'; import { ProductVariationDisplayComponent } from './components/product/product-variation-display/product-variation-display.component'; import { ProductVariationSelectComponent } from './components/product/product-variation-select/product-variation-select.component'; +import { ProductsListComponent } from './components/product/products-list/products-list.component'; import { PromotionDetailsComponent } from './components/promotion/promotion-details/promotion-details.component'; import { PromotionRemoveComponent } from './components/promotion/promotion-remove/promotion-remove.component'; import { RecentlyViewedComponent } from './components/recently/recently-viewed/recently-viewed.component'; @@ -253,6 +254,7 @@ const exportedComponents = [ ProductShipmentComponent, ProductVariationDisplayComponent, ProductVariationSelectComponent, + ProductsListComponent, PromotionDetailsComponent, PromotionRemoveComponent, RecentlyViewedComponent,