feat(variant-selector): implement manual selection when autoSelectFirst is disabled
This commit is contained in:
@@ -258,6 +258,15 @@ describe('ProductListComponent', () => {
|
||||
expect(element.querySelector('.product-list--single')).not.toBeNull();
|
||||
expect(element.querySelectorAll('app-product-ticket-selector')).toHaveLength(1);
|
||||
expect(element.querySelectorAll('.variant-selector__select')).toHaveLength(4);
|
||||
expect(
|
||||
(element.querySelector('.ticket-selector__actions .btn-primary') as HTMLButtonElement)
|
||||
.disabled,
|
||||
).toBe(true);
|
||||
|
||||
const ticketSelector = fixture.debugElement.query(By.directive(ProductTicketSelectorComponent))
|
||||
.componentInstance as ProductTicketSelectorComponent;
|
||||
ticketSelector['updateRow'](1, 401);
|
||||
fixture.detectChanges();
|
||||
|
||||
(element.querySelector('.ticket-selector__add-row-button') as HTMLButtonElement).click();
|
||||
fixture.detectChanges();
|
||||
@@ -265,6 +274,9 @@ describe('ProductListComponent', () => {
|
||||
expect(element.querySelectorAll('.ticket-selector__row')).toHaveLength(2);
|
||||
expect(element.querySelectorAll('.variant-selector__select')).toHaveLength(8);
|
||||
|
||||
ticketSelector['updateRow'](2, 402);
|
||||
fixture.detectChanges();
|
||||
|
||||
(element.querySelector('.ticket-selector__actions .btn-primary') as HTMLButtonElement).click();
|
||||
|
||||
expect(buySpy).toHaveBeenCalledWith({
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
class="ticket-selector__fields"
|
||||
[variants]="variantsForRow(row.id)"
|
||||
[disabled]="disabled()"
|
||||
[autoSelectFirst]="false"
|
||||
[selectedVariant]="row.variantId"
|
||||
(selectedVariantChange)="updateRow(row.id, $event)"
|
||||
/>
|
||||
@@ -55,11 +56,7 @@
|
||||
|
||||
<div class="ticket-selector__actions row g-0 justify-content-end">
|
||||
<div class="col-12 col-md-3">
|
||||
<app-button
|
||||
variant="primary"
|
||||
[disabled]="disabled() || !hasSelection()"
|
||||
(click)="onBuy()"
|
||||
>
|
||||
<app-button variant="primary" [disabled]="disabled() || !hasSelection()" (click)="onBuy()">
|
||||
Comprar
|
||||
</app-button>
|
||||
</div>
|
||||
|
||||
@@ -4,11 +4,14 @@
|
||||
<select
|
||||
class="form-select variant-selector__select"
|
||||
[attr.aria-label]="selector.label"
|
||||
[disabled]="disabled()"
|
||||
[disabled]="disabled() || (!autoSelectFirst() && selector.options.length === 0)"
|
||||
[compareWith]="compareValues"
|
||||
[ngModel]="selectedValues()[selector.key]"
|
||||
[ngModel]="selectedValues()[selector.key] ?? null"
|
||||
(ngModelChange)="onValueChange(selector.key, $event)"
|
||||
>
|
||||
@if (!autoSelectFirst()) {
|
||||
<option [ngValue]="null" disabled>Seleccioná {{ selector.label }}</option>
|
||||
}
|
||||
@for (option of selector.options; track option.key) {
|
||||
<option [ngValue]="option.value">{{ option.label }}</option>
|
||||
}
|
||||
|
||||
@@ -79,4 +79,39 @@ describe('VariantSelectorComponent', () => {
|
||||
|
||||
expect(fixture.componentInstance.selectedVariant()).toBe(1);
|
||||
});
|
||||
|
||||
it('requires manual selections when autoSelectFirst is disabled', async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [VariantSelectorComponent],
|
||||
}).compileComponents();
|
||||
const fixture = TestBed.createComponent(VariantSelectorComponent);
|
||||
fixture.componentRef.setInput('variants', [
|
||||
{ id: 1, values: { sector: 'A', asiento: '1' } },
|
||||
{ id: 2, values: { sector: 'A', asiento: '2' } },
|
||||
]);
|
||||
fixture.componentRef.setInput('autoSelectFirst', false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.selectedVariant()).toBeNull();
|
||||
expect((fixture.componentInstance as any).selectedValues()).toEqual({});
|
||||
|
||||
(fixture.componentInstance as any).onValueChange('sector', 'A');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.selectedVariant()).toBeNull();
|
||||
expect((fixture.componentInstance as any).selectedValues()).toEqual({ sector: 'A' });
|
||||
|
||||
fixture.componentRef.setInput('variants', [
|
||||
{ id: 1, values: { sector: 'A', asiento: '1' } },
|
||||
{ id: 2, values: { sector: 'A', asiento: '2' } },
|
||||
]);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect((fixture.componentInstance as any).selectedValues()).toEqual({ sector: 'A' });
|
||||
expect((fixture.componentInstance as any).selectors()[1].options).toHaveLength(2);
|
||||
|
||||
(fixture.componentInstance as any).onValueChange('asiento', '2');
|
||||
|
||||
expect(fixture.componentInstance.selectedVariant()).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,6 +48,7 @@ export class VariantSelectorComponent {
|
||||
readonly selectedVariant = model<unknown>(null);
|
||||
readonly disabled = input(false);
|
||||
readonly compact = input(false);
|
||||
readonly autoSelectFirst = input(true);
|
||||
|
||||
protected readonly selectedValues = signal<Record<string, VariantAttributeValue>>({});
|
||||
protected readonly compareValues = (
|
||||
@@ -82,6 +83,7 @@ export class VariantSelectorComponent {
|
||||
effect(() => {
|
||||
const variants = this.variants();
|
||||
const selectedVariant = this.selectedVariant();
|
||||
const autoSelectFirst = this.autoSelectFirst();
|
||||
|
||||
untracked(() => {
|
||||
if (variants.length === 0) {
|
||||
@@ -90,19 +92,31 @@ export class VariantSelectorComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
const selected =
|
||||
variants.find((variant) => Object.is(variant.id, selectedVariant)) ?? variants[0];
|
||||
this.selectedValues.set({ ...selected.values });
|
||||
if (!Object.is(selected.id, selectedVariant)) this.selectedVariant.set(selected.id);
|
||||
const selected = variants.find((variant) => Object.is(variant.id, selectedVariant));
|
||||
|
||||
if (!selected && !autoSelectFirst) {
|
||||
this.selectedValues.set(this.reconcileManualSelection(this.selectedValues(), variants));
|
||||
if (selectedVariant !== null) this.selectedVariant.set(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedSelection = selected ?? variants[0];
|
||||
this.selectedValues.set({ ...resolvedSelection.values });
|
||||
if (!Object.is(resolvedSelection.id, selectedVariant)) {
|
||||
this.selectedVariant.set(resolvedSelection.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected onValueChange(key: string, value: VariantAttributeValue): void {
|
||||
protected onValueChange(key: string, value: VariantAttributeValue | null): void {
|
||||
const variants = this.variants();
|
||||
const keys = this.attributeKeys();
|
||||
const changedIndex = keys.indexOf(key);
|
||||
const values = { ...this.selectedValues(), [key]: value };
|
||||
const values = { ...this.selectedValues() };
|
||||
|
||||
if (value === null) delete values[key];
|
||||
else values[key] = value;
|
||||
|
||||
for (let index = changedIndex + 1; index < keys.length; index++) {
|
||||
const currentKey = keys[index];
|
||||
@@ -116,7 +130,7 @@ export class VariantSelectorComponent {
|
||||
|
||||
if (!options.some((option) => this.sameValue(option.value, values[currentKey]))) {
|
||||
const firstOption = options[0];
|
||||
if (firstOption) values[currentKey] = firstOption.value;
|
||||
if (firstOption && this.autoSelectFirst()) values[currentKey] = firstOption.value;
|
||||
else delete values[currentKey];
|
||||
}
|
||||
}
|
||||
@@ -151,6 +165,32 @@ export class VariantSelectorComponent {
|
||||
return Array.from(options.values());
|
||||
}
|
||||
|
||||
private reconcileManualSelection(
|
||||
selectedValues: Record<string, VariantAttributeValue>,
|
||||
variants: VariantSelectorVariant[],
|
||||
): Record<string, VariantAttributeValue> {
|
||||
const reconciled: Record<string, VariantAttributeValue> = {};
|
||||
|
||||
for (const key of this.attributeKeys()) {
|
||||
const selectedValue = selectedValues[key];
|
||||
if (selectedValue === undefined) break;
|
||||
|
||||
const compatibleVariants = variants.filter((variant) =>
|
||||
Object.entries(reconciled).every(([previousKey, previousValue]) =>
|
||||
this.sameValue(variant.values[previousKey], previousValue),
|
||||
),
|
||||
);
|
||||
const selectionIsAvailable = this.optionsFor(compatibleVariants, key).some((option) =>
|
||||
this.sameValue(option.value, selectedValue),
|
||||
);
|
||||
|
||||
if (!selectionIsAvailable) break;
|
||||
reconciled[key] = selectedValue;
|
||||
}
|
||||
|
||||
return reconciled;
|
||||
}
|
||||
|
||||
private sameValue(
|
||||
left: VariantAttributeValue | undefined,
|
||||
right: VariantAttributeValue | undefined,
|
||||
|
||||
Reference in New Issue
Block a user