feat: refactor variant selection to use app-variant-selector for improved maintainability

This commit is contained in:
2026-08-11 08:28:42 -03:00
parent 019c9df23b
commit f6f3a997f7
4 changed files with 29 additions and 96 deletions

View File

@@ -21,18 +21,10 @@
</div> </div>
<div class="product-vertical-with-cart-card__variant-selectors"> <div class="product-vertical-with-cart-card__variant-selectors">
@for (selector of variantSelectors(); track selector.key) { <app-variant-selector
<select [variants]="selectorVariants()"
class="form-select product-vertical-with-cart-card__variant-select" [(selectedVariant)]="selectedVariant"
[attr.aria-label]="selector.label" />
[ngModel]="selectedValues()[selector.key]"
(ngModelChange)="onVariantValueChange(selector.key, $event)"
>
@for (option of selector.options; track option) {
<option [ngValue]="option">{{ option }}</option>
}
</select>
}
</div> </div>
</div> </div>
} }

View File

@@ -66,24 +66,22 @@
} }
&__variant-selectors { &__variant-selectors {
display: flex;
align-items: center;
width: 100%; width: 100%;
gap: 8px;
} }
&__variant-select { &__variant-selectors app-variant-selector {
flex: 1 1 0; width: 100%;
min-width: 0; }
height: 40px;
border-color: #d8d8d8;
color: #6f6f6f;
font-size: 12px;
&:focus { &__variant-selectors ::ng-deep .variant-selector {
border-color: var(--tenant-primary, #009933); flex-wrap: nowrap;
box-shadow: 0 0 0 0.2rem color-mix(in srgb, transparent 75%, var(--tenant-primary, #009933)); width: 100%;
} }
&__variant-selectors ::ng-deep .variant-selector__select {
flex: 1 1 0;
width: 0;
min-width: 0;
} }
&__price { &__price {

View File

@@ -131,7 +131,7 @@ describe('ProductVerticalWithCartCardComponent', () => {
expect(summary?.querySelector('.product-vertical-with-cart-card__price')).not.toBeNull(); expect(summary?.querySelector('.product-vertical-with-cart-card__price')).not.toBeNull();
expect(summary?.querySelector('app-quantity-selector')).not.toBeNull(); expect(summary?.querySelector('app-quantity-selector')).not.toBeNull();
expect( expect(
selectors?.querySelectorAll('.product-vertical-with-cart-card__variant-select'), selectors?.querySelectorAll('.variant-selector__select'),
).toHaveLength(1); ).toHaveLength(1);
expect(selectors?.querySelector('app-quantity-selector')).toBeNull(); expect(selectors?.querySelector('app-quantity-selector')).toBeNull();
}); });
@@ -150,7 +150,7 @@ describe('ProductVerticalWithCartCardComponent', () => {
expect(layout?.querySelector('.product-vertical-with-cart-card__summary')).not.toBeNull(); expect(layout?.querySelector('.product-vertical-with-cart-card__summary')).not.toBeNull();
expect( expect(
layout?.querySelectorAll( layout?.querySelectorAll(
'.product-vertical-with-cart-card__variant-selectors .product-vertical-with-cart-card__variant-select', '.product-vertical-with-cart-card__variant-selectors .variant-selector__select',
), ),
).toHaveLength(2); ).toHaveLength(2);
}); });
@@ -166,7 +166,7 @@ describe('ProductVerticalWithCartCardComponent', () => {
fixture.componentInstance.buy.subscribe(buySpy); fixture.componentInstance.buy.subscribe(buySpy);
const select = fixture.nativeElement.querySelector( const select = fixture.nativeElement.querySelector(
'.product-vertical-with-cart-card__variant-select', '.variant-selector__select',
) as HTMLSelectElement; ) as HTMLSelectElement;
select.value = select.options[1].value; select.value = select.options[1].value;
select.dispatchEvent(new Event('change')); select.dispatchEvent(new Event('change'));
@@ -207,7 +207,7 @@ describe('ProductVerticalWithCartCardComponent', () => {
).toBe('$ 10.000'); ).toBe('$ 10.000');
const select = element.querySelector( const select = element.querySelector(
'.product-vertical-with-cart-card__variant-select', '.variant-selector__select',
) as HTMLSelectElement; ) as HTMLSelectElement;
select.value = select.options[1].value; select.value = select.options[1].value;
select.dispatchEvent(new Event('change')); select.dispatchEvent(new Event('change'));

View File

@@ -2,17 +2,17 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
computed, computed,
effect,
input, input,
model, model,
output, output,
signal,
untracked,
} from '@angular/core'; } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ButtonComponent } from '../button/button.component'; import { ButtonComponent } from '../button/button.component';
import { QuantitySelectorComponent } from '../quantity-selector/quantity-selector.component'; import { QuantitySelectorComponent } from '../quantity-selector/quantity-selector.component';
import {
VariantSelectorComponent,
VariantSelectorVariant,
} from '../variant-selector/variant-selector.component';
export interface VerticalCartVariant { export interface VerticalCartVariant {
id: number; id: number;
@@ -21,15 +21,9 @@ export interface VerticalCartVariant {
values: Record<string, string>; values: Record<string, string>;
} }
interface VariantSelector {
key: string;
label: string;
options: string[];
}
@Component({ @Component({
selector: 'app-product-vertical-with-cart-card', selector: 'app-product-vertical-with-cart-card',
imports: [ButtonComponent, FormsModule, QuantitySelectorComponent], imports: [ButtonComponent, QuantitySelectorComponent, VariantSelectorComponent],
templateUrl: './product-vertical-with-cart-card.component.html', templateUrl: './product-vertical-with-cart-card.component.html',
styleUrl: './product-vertical-with-cart-card.component.scss', styleUrl: './product-vertical-with-cart-card.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@@ -46,8 +40,6 @@ export class ProductVerticalWithCartCardComponent {
readonly buy = output<{ quantity: number; variant: number | null }>(); readonly buy = output<{ quantity: number; variant: number | null }>();
readonly addToCart = output<{ quantity: number; variant: number | null }>(); readonly addToCart = output<{ quantity: number; variant: number | null }>();
protected readonly selectedValues = signal<Record<string, string>>({});
protected readonly selectedVariantData = computed(() => protected readonly selectedVariantData = computed(() =>
this.variants().find((variant) => variant.id === this.selectedVariant()), this.variants().find((variant) => variant.id === this.selectedVariant()),
); );
@@ -60,54 +52,10 @@ export class ProductVerticalWithCartCardComponent {
return Number.isFinite(variantPrice) ? variantPrice : this.price(); return Number.isFinite(variantPrice) ? variantPrice : this.price();
}); });
protected readonly formattedPrice = computed(() => this.formatCurrency(this.effectivePrice())); protected readonly formattedPrice = computed(() => this.formatCurrency(this.effectivePrice()));
protected readonly variantSelectors = computed<VariantSelector[]>(() => { protected readonly selectorVariants = computed<VariantSelectorVariant[]>(() =>
const variants = this.variants(); this.variants().map((variant) => ({ value: variant.id, values: variant.values })),
const keys = Array.from(new Set(variants.flatMap((variant) => Object.keys(variant.values)))); );
protected readonly hasVariants = computed(() => this.selectorVariants().length > 0);
return keys.map((key) => ({
key,
label: this.formatVariantLabel(key),
options: Array.from(
new Set(
variants
.map((variant) => variant.values[key])
.filter((value): value is string => Boolean(value)),
),
),
}));
});
protected readonly hasVariants = computed(() => this.variantSelectors().length > 0);
constructor() {
effect(() => {
const variants = this.variants();
untracked(() => {
if (variants.length === 0) {
this.selectedValues.set({});
this.selectedVariant.set(null);
return;
}
const selected =
variants.find((variant) => variant.id === this.selectedVariant()) ?? variants[0];
this.selectedValues.set({ ...selected.values });
this.selectedVariant.set(selected.id);
});
});
}
protected onVariantValueChange(key: string, value: string): void {
const values = { ...this.selectedValues(), [key]: value };
const selectors = this.variantSelectors();
const matchingVariant = this.variants().find((variant) =>
selectors.every((selector) => variant.values[selector.key] === values[selector.key]),
);
this.selectedValues.set(values);
this.selectedVariant.set(matchingVariant?.id ?? null);
}
protected onAddToCart(): void { protected onAddToCart(): void {
this.addToCart.emit({ quantity: this.quantity(), variant: this.selectedVariant() }); this.addToCart.emit({ quantity: this.quantity(), variant: this.selectedVariant() });
@@ -117,11 +65,6 @@ export class ProductVerticalWithCartCardComponent {
this.buy.emit({ quantity: this.quantity(), variant: this.selectedVariant() }); this.buy.emit({ quantity: this.quantity(), variant: this.selectedVariant() });
} }
private formatVariantLabel(key: string): string {
const label = key.replace(/[_-]+/g, ' ');
return label.charAt(0).toUpperCase() + label.slice(1);
}
private formatCurrency(value: number): string { private formatCurrency(value: number): string {
const rounded = Math.round(value); const rounded = Math.round(value);
const parts = rounded.toString().split('.'); const parts = rounded.toString().split('.');