feat(product-list): enhance layout handling with responsive design and computed properties
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
<div
|
<div
|
||||||
class="product-list"
|
class="product-list"
|
||||||
[class.product-list--row]="layout() === 'row'"
|
[class.product-list--row]="effectiveLayout() === 'row'"
|
||||||
[class.product-list--column]="layout() !== 'row'"
|
[class.product-list--column]="effectiveLayout() !== 'row'"
|
||||||
>
|
>
|
||||||
@for (item of items().data; track item.id; let index = $index) {
|
@for (item of items().data; track item.id; let index = $index) {
|
||||||
<div class="product-list__item">
|
<div class="product-list__item">
|
||||||
@switch (layout()) {
|
@switch (effectiveLayout()) {
|
||||||
@case ('row') {
|
@case ('row') {
|
||||||
<app-product-row-card
|
<app-product-row-card
|
||||||
[title]="item.nombre"
|
[title]="item.nombre"
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ describe('ProductListComponent', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => TestBed.resetTestingModule());
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
});
|
||||||
|
|
||||||
async function render(layout: ProductListLayout) {
|
async function render(layout: ProductListLayout) {
|
||||||
await TestBed.configureTestingModule({ imports: [ProductListComponent] }).compileComponents();
|
await TestBed.configureTestingModule({ imports: [ProductListComponent] }).compileComponents();
|
||||||
@@ -76,6 +79,25 @@ describe('ProductListComponent', () => {
|
|||||||
expect(element.querySelector('app-product-column-with-image')).toBeNull();
|
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 () => {
|
it('renders image products next to each other in the column grid', async () => {
|
||||||
const fixture = await render('column_with_image');
|
const fixture = await render('column_with_image');
|
||||||
const element = fixture.nativeElement as HTMLElement;
|
const element = fixture.nativeElement as HTMLElement;
|
||||||
|
|||||||
@@ -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 { ApiPaginatedResponse } from '../../../core/services/api-paginated-response.interface';
|
||||||
import {
|
import {
|
||||||
@@ -37,6 +47,9 @@ export interface ProductListCartEvent {
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class ProductListComponent {
|
export class ProductListComponent {
|
||||||
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
|
private readonly mobile = signal(false);
|
||||||
|
|
||||||
readonly layout = input.required<ProductListLayout>();
|
readonly layout = input.required<ProductListLayout>();
|
||||||
readonly items = input.required<ApiPaginatedResponse<ProductListItem[]>>();
|
readonly items = input.required<ApiPaginatedResponse<ProductListItem[]>>();
|
||||||
readonly loading = input(false);
|
readonly loading = input(false);
|
||||||
@@ -45,6 +58,22 @@ export class ProductListComponent {
|
|||||||
readonly addToCart = output<ProductListCartEvent>();
|
readonly addToCart = output<ProductListCartEvent>();
|
||||||
readonly pageChange = output<number>();
|
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 {
|
protected price(item: ProductListItem): number {
|
||||||
const price = Number(item.precio);
|
const price = Number(item.precio);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user