From 98e899c5bf55df750d9c0177e5f3bd2f5d9cd0d5 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Mon, 20 Jul 2026 16:59:57 -0300 Subject: [PATCH] feat(product-list): enhance layout handling with responsive design and computed properties --- .../product-list/product-list.component.html | 6 ++-- .../product-list.component.spec.ts | 24 +++++++++++++- .../product-list/product-list.component.ts | 31 ++++++++++++++++++- 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/app/shared/components/product-list/product-list.component.html b/src/app/shared/components/product-list/product-list.component.html index c3e9ff2..86568b7 100644 --- a/src/app/shared/components/product-list/product-list.component.html +++ b/src/app/shared/components/product-list/product-list.component.html @@ -1,11 +1,11 @@
@for (item of items().data; track item.id; let index = $index) {
- @switch (layout()) { + @switch (effectiveLayout()) { @case ('row') { { } }); - afterEach(() => TestBed.resetTestingModule()); + afterEach(() => { + vi.unstubAllGlobals(); + TestBed.resetTestingModule(); + }); async function render(layout: ProductListLayout) { await TestBed.configureTestingModule({ imports: [ProductListComponent] }).compileComponents(); @@ -76,6 +79,25 @@ describe('ProductListComponent', () => { expect(element.querySelector('app-product-column-with-image')).toBeNull(); }); + it('replaces the row layout with column with cart on mobile', async () => { + const matchMedia = vi.fn().mockReturnValue({ + matches: true, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + } as unknown as MediaQueryList); + vi.stubGlobal('matchMedia', matchMedia); + + const fixture = await render('row'); + await fixture.whenStable(); + fixture.detectChanges(); + const element = fixture.nativeElement as HTMLElement; + + expect(matchMedia).toHaveBeenCalledWith('(max-width: 767.98px)'); + expect(element.querySelector('.product-list--column')).not.toBeNull(); + expect(element.querySelectorAll('app-product-vertical-with-cart-card')).toHaveLength(2); + expect(element.querySelector('app-product-row-card')).toBeNull(); + }); + it('renders image products next to each other in the column grid', async () => { const fixture = await render('column_with_image'); const element = fixture.nativeElement as HTMLElement; diff --git a/src/app/shared/components/product-list/product-list.component.ts b/src/app/shared/components/product-list/product-list.component.ts index 4d10101..f231200 100644 --- a/src/app/shared/components/product-list/product-list.component.ts +++ b/src/app/shared/components/product-list/product-list.component.ts @@ -1,4 +1,14 @@ -import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; +import { + afterNextRender, + ChangeDetectionStrategy, + Component, + computed, + DestroyRef, + inject, + input, + output, + signal, +} from '@angular/core'; import { ApiPaginatedResponse } from '../../../core/services/api-paginated-response.interface'; import { @@ -37,6 +47,9 @@ export interface ProductListCartEvent { changeDetection: ChangeDetectionStrategy.OnPush, }) export class ProductListComponent { + private readonly destroyRef = inject(DestroyRef); + private readonly mobile = signal(false); + readonly layout = input.required(); readonly items = input.required>(); readonly loading = input(false); @@ -45,6 +58,22 @@ export class ProductListComponent { readonly addToCart = output(); readonly pageChange = output(); + protected readonly effectiveLayout = computed(() => + this.mobile() && this.layout() === 'row' ? 'column_with_cart' : this.layout(), + ); + + constructor() { + afterNextRender(() => { + const mobileQuery = window.matchMedia('(max-width: 767.98px)'); + const updateMobile = (event: MediaQueryList | MediaQueryListEvent): void => + this.mobile.set(event.matches); + + updateMobile(mobileQuery); + mobileQuery.addEventListener('change', updateMobile); + this.destroyRef.onDestroy(() => mobileQuery.removeEventListener('change', updateMobile)); + }); + } + protected price(item: ProductListItem): number { const price = Number(item.precio);