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 class="product-vertical-with-cart-card__variant-selectors">
@for (selector of variantSelectors(); track selector.key) {
<select
class="form-select product-vertical-with-cart-card__variant-select"
[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>
}
<app-variant-selector
[variants]="selectorVariants()"
[(selectedVariant)]="selectedVariant"
/>
</div>
</div>
}

View File

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

View File

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

View File

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