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.querySelector('.product-list--single')).not.toBeNull();
|
||||||
expect(element.querySelectorAll('app-product-ticket-selector')).toHaveLength(1);
|
expect(element.querySelectorAll('app-product-ticket-selector')).toHaveLength(1);
|
||||||
expect(element.querySelectorAll('.variant-selector__select')).toHaveLength(4);
|
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();
|
(element.querySelector('.ticket-selector__add-row-button') as HTMLButtonElement).click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -265,6 +274,9 @@ describe('ProductListComponent', () => {
|
|||||||
expect(element.querySelectorAll('.ticket-selector__row')).toHaveLength(2);
|
expect(element.querySelectorAll('.ticket-selector__row')).toHaveLength(2);
|
||||||
expect(element.querySelectorAll('.variant-selector__select')).toHaveLength(8);
|
expect(element.querySelectorAll('.variant-selector__select')).toHaveLength(8);
|
||||||
|
|
||||||
|
ticketSelector['updateRow'](2, 402);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
(element.querySelector('.ticket-selector__actions .btn-primary') as HTMLButtonElement).click();
|
(element.querySelector('.ticket-selector__actions .btn-primary') as HTMLButtonElement).click();
|
||||||
|
|
||||||
expect(buySpy).toHaveBeenCalledWith({
|
expect(buySpy).toHaveBeenCalledWith({
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
class="ticket-selector__fields"
|
class="ticket-selector__fields"
|
||||||
[variants]="variantsForRow(row.id)"
|
[variants]="variantsForRow(row.id)"
|
||||||
[disabled]="disabled()"
|
[disabled]="disabled()"
|
||||||
|
[autoSelectFirst]="false"
|
||||||
[selectedVariant]="row.variantId"
|
[selectedVariant]="row.variantId"
|
||||||
(selectedVariantChange)="updateRow(row.id, $event)"
|
(selectedVariantChange)="updateRow(row.id, $event)"
|
||||||
/>
|
/>
|
||||||
@@ -55,11 +56,7 @@
|
|||||||
|
|
||||||
<div class="ticket-selector__actions row g-0 justify-content-end">
|
<div class="ticket-selector__actions row g-0 justify-content-end">
|
||||||
<div class="col-12 col-md-3">
|
<div class="col-12 col-md-3">
|
||||||
<app-button
|
<app-button variant="primary" [disabled]="disabled() || !hasSelection()" (click)="onBuy()">
|
||||||
variant="primary"
|
|
||||||
[disabled]="disabled() || !hasSelection()"
|
|
||||||
(click)="onBuy()"
|
|
||||||
>
|
|
||||||
Comprar
|
Comprar
|
||||||
</app-button>
|
</app-button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,11 +4,14 @@
|
|||||||
<select
|
<select
|
||||||
class="form-select variant-selector__select"
|
class="form-select variant-selector__select"
|
||||||
[attr.aria-label]="selector.label"
|
[attr.aria-label]="selector.label"
|
||||||
[disabled]="disabled()"
|
[disabled]="disabled() || (!autoSelectFirst() && selector.options.length === 0)"
|
||||||
[compareWith]="compareValues"
|
[compareWith]="compareValues"
|
||||||
[ngModel]="selectedValues()[selector.key]"
|
[ngModel]="selectedValues()[selector.key] ?? null"
|
||||||
(ngModelChange)="onValueChange(selector.key, $event)"
|
(ngModelChange)="onValueChange(selector.key, $event)"
|
||||||
>
|
>
|
||||||
|
@if (!autoSelectFirst()) {
|
||||||
|
<option [ngValue]="null" disabled>Seleccioná {{ selector.label }}</option>
|
||||||
|
}
|
||||||
@for (option of selector.options; track option.key) {
|
@for (option of selector.options; track option.key) {
|
||||||
<option [ngValue]="option.value">{{ option.label }}</option>
|
<option [ngValue]="option.value">{{ option.label }}</option>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,4 +79,39 @@ describe('VariantSelectorComponent', () => {
|
|||||||
|
|
||||||
expect(fixture.componentInstance.selectedVariant()).toBe(1);
|
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 selectedVariant = model<unknown>(null);
|
||||||
readonly disabled = input(false);
|
readonly disabled = input(false);
|
||||||
readonly compact = input(false);
|
readonly compact = input(false);
|
||||||
|
readonly autoSelectFirst = input(true);
|
||||||
|
|
||||||
protected readonly selectedValues = signal<Record<string, VariantAttributeValue>>({});
|
protected readonly selectedValues = signal<Record<string, VariantAttributeValue>>({});
|
||||||
protected readonly compareValues = (
|
protected readonly compareValues = (
|
||||||
@@ -82,6 +83,7 @@ export class VariantSelectorComponent {
|
|||||||
effect(() => {
|
effect(() => {
|
||||||
const variants = this.variants();
|
const variants = this.variants();
|
||||||
const selectedVariant = this.selectedVariant();
|
const selectedVariant = this.selectedVariant();
|
||||||
|
const autoSelectFirst = this.autoSelectFirst();
|
||||||
|
|
||||||
untracked(() => {
|
untracked(() => {
|
||||||
if (variants.length === 0) {
|
if (variants.length === 0) {
|
||||||
@@ -90,19 +92,31 @@ export class VariantSelectorComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selected =
|
const selected = variants.find((variant) => Object.is(variant.id, selectedVariant));
|
||||||
variants.find((variant) => Object.is(variant.id, selectedVariant)) ?? variants[0];
|
|
||||||
this.selectedValues.set({ ...selected.values });
|
if (!selected && !autoSelectFirst) {
|
||||||
if (!Object.is(selected.id, selectedVariant)) this.selectedVariant.set(selected.id);
|
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 variants = this.variants();
|
||||||
const keys = this.attributeKeys();
|
const keys = this.attributeKeys();
|
||||||
const changedIndex = keys.indexOf(key);
|
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++) {
|
for (let index = changedIndex + 1; index < keys.length; index++) {
|
||||||
const currentKey = keys[index];
|
const currentKey = keys[index];
|
||||||
@@ -116,7 +130,7 @@ export class VariantSelectorComponent {
|
|||||||
|
|
||||||
if (!options.some((option) => this.sameValue(option.value, values[currentKey]))) {
|
if (!options.some((option) => this.sameValue(option.value, values[currentKey]))) {
|
||||||
const firstOption = options[0];
|
const firstOption = options[0];
|
||||||
if (firstOption) values[currentKey] = firstOption.value;
|
if (firstOption && this.autoSelectFirst()) values[currentKey] = firstOption.value;
|
||||||
else delete values[currentKey];
|
else delete values[currentKey];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,6 +165,32 @@ export class VariantSelectorComponent {
|
|||||||
return Array.from(options.values());
|
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(
|
private sameValue(
|
||||||
left: VariantAttributeValue | undefined,
|
left: VariantAttributeValue | undefined,
|
||||||
right: VariantAttributeValue | undefined,
|
right: VariantAttributeValue | undefined,
|
||||||
|
|||||||
Reference in New Issue
Block a user