feat(product-list): enhance layout handling with responsive design and computed properties

This commit is contained in:
2026-07-20 16:59:57 -03:00
parent 0c0d2b559d
commit 98e899c5bf
3 changed files with 56 additions and 5 deletions

View File

@@ -1,11 +1,11 @@
<div
class="product-list"
[class.product-list--row]="layout() === 'row'"
[class.product-list--column]="layout() !== 'row'"
[class.product-list--row]="effectiveLayout() === 'row'"
[class.product-list--column]="effectiveLayout() !== 'row'"
>
@for (item of items().data; track item.id; let index = $index) {
<div class="product-list__item">
@switch (layout()) {
@switch (effectiveLayout()) {
@case ('row') {
<app-product-row-card
[title]="item.nombre"

View File

@@ -33,7 +33,10 @@ describe('ProductListComponent', () => {
}
});
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;

View File

@@ -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<ProductListLayout>();
readonly items = input.required<ApiPaginatedResponse<ProductListItem[]>>();
readonly loading = input(false);
@@ -45,6 +58,22 @@ export class ProductListComponent {
readonly addToCart = output<ProductListCartEvent>();
readonly pageChange = output<number>();
protected readonly effectiveLayout = computed<ProductListLayout>(() =>
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);