feat(checkout): add branch selection to reward catalog - Add new select-branch-dropdown library with BranchDropdownComponent and SelectedBranchDropdownComponent for branch selection - Extend DropdownButtonComponent with filter and option subcomponents - Integrate branch selection into reward catalog page - Add BranchesResource for fetching available branches - Update CheckoutMetadataService with branch selection persistence - Add comprehensive tests for dropdown components Related work items: #5464
@isa/checkout/feature/select-branch-dropdown
Branch selection dropdown components for the checkout domain, enabling users to select a branch for their checkout session.
Overview
The Select Branch Dropdown feature library provides two UI components for branch selection in the checkout flow:
- SelectedBranchDropdownComponent - High-level component that integrates with
CheckoutMetadataServicefor automatic persistence - BranchDropdownComponent - Low-level component with
ControlValueAccessorsupport for custom form integration
Table of Contents
- Features
- Quick Start
- Component API Reference
- Usage Examples
- State Management
- Dependencies
- Testing
- Best Practices
Features
- Branch Selection - Dropdown interface for selecting a branch
- Metadata Integration - Persists selected branch in checkout metadata via
CheckoutMetadataService.setSelectedBranch() - Standalone Component - Modern Angular standalone architecture
- Responsive Design - Works across tablet and desktop layouts
- E2E Testing Support - Includes data-what and data-which attributes
Quick Start
1. Import the Component
import { SelectedBranchDropdownComponent } from '@isa/checkout/feature/select-branch-dropdown';
@Component({
selector: 'app-checkout',
imports: [SelectedBranchDropdownComponent],
template: `
<checkout-selected-branch-dropdown [tabId]="tabId()" />
`
})
export class CheckoutComponent {
tabId = injectTabId();
}
2. Using the Low-Level Component with Forms
import { BranchDropdownComponent } from '@isa/checkout/feature/select-branch-dropdown';
@Component({
selector: 'app-branch-form',
imports: [BranchDropdownComponent, ReactiveFormsModule],
template: `
<checkout-branch-dropdown formControlName="branch" />
`
})
export class BranchFormComponent {}
Component API Reference
SelectedBranchDropdownComponent
High-level branch selection component with automatic CheckoutMetadataService integration.
Selector: checkout-selected-branch-dropdown
Inputs:
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
tabId |
number |
Yes | - | The tab ID for metadata storage |
appearance |
DropdownAppearance |
No | AccentOutline |
Visual style of the dropdown |
Example:
<checkout-selected-branch-dropdown
[tabId]="tabId()"
[appearance]="DropdownAppearance.AccentOutline"
/>
BranchDropdownComponent
Low-level branch dropdown with ControlValueAccessor support for reactive forms.
Selector: checkout-branch-dropdown
Inputs:
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
appearance |
DropdownAppearance |
No | AccentOutline |
Visual style of the dropdown |
Outputs:
| Output | Type | Description |
|---|---|---|
selected |
Branch | null |
Two-way bindable selected branch (model) |
Example:
<!-- With ngModel -->
<checkout-branch-dropdown [(selected)]="selectedBranch" />
<!-- With reactive forms -->
<checkout-branch-dropdown formControlName="branch" />
Usage Examples
Basic Usage in Checkout Flow
import { Component } from '@angular/core';
import { SelectedBranchDropdownComponent } from '@isa/checkout/feature/select-branch-dropdown';
import { injectTabId } from '@isa/core/tabs';
@Component({
selector: 'app-checkout-header',
imports: [SelectedBranchDropdownComponent],
template: `
<div class="checkout-header">
<h1>Checkout</h1>
<checkout-selected-branch-dropdown [tabId]="tabId()" />
</div>
`
})
export class CheckoutHeaderComponent {
tabId = injectTabId();
}
Using BranchDropdownComponent with Reactive Forms
import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { BranchDropdownComponent } from '@isa/checkout/feature/select-branch-dropdown';
@Component({
selector: 'app-branch-form-example',
imports: [BranchDropdownComponent, ReactiveFormsModule],
template: `
<form [formGroup]="form">
<checkout-branch-dropdown formControlName="branch" />
</form>
`
})
export class BranchFormExampleComponent {
#fb = inject(FormBuilder);
form = this.#fb.group({
branch: [null]
});
}
Reading Selected Branch from Metadata
import { Component, inject, computed } from '@angular/core';
import { CheckoutMetadataService } from '@isa/checkout/data-access';
import { injectTabId } from '@isa/core/tabs';
@Component({
selector: 'app-branch-reader-example',
template: `
@if (selectedBranch(); as branch) {
<p>Selected Branch: {{ branch.name }}</p>
} @else {
<p>No branch selected</p>
}
`
})
export class BranchReaderExampleComponent {
#checkoutMetadata = inject(CheckoutMetadataService);
tabId = injectTabId();
selectedBranch = computed(() => {
return this.#checkoutMetadata.getSelectedBranch(this.tabId()!);
});
}
State Management
CheckoutMetadataService Integration
The component integrates with CheckoutMetadataService from @isa/checkout/data-access:
// Set selected branch (stores the full Branch object)
this.#checkoutMetadata.setSelectedBranch(tabId, branch);
// Get selected branch (returns Branch | undefined)
const branch = this.#checkoutMetadata.getSelectedBranch(tabId);
// Clear selected branch
this.#checkoutMetadata.setSelectedBranch(tabId, undefined);
State Persistence:
- Branch selection persists in tab metadata
- Available across all checkout components in the same tab
- Cleared when tab is closed or session ends
Dependencies
Required Libraries
Angular Core
@angular/core- Angular framework@angular/forms- Form controls and ControlValueAccessor
ISA Feature Libraries
@isa/checkout/data-access- CheckoutMetadataService, BranchesResource, Branch type@isa/core/logging- Logger utility
ISA UI Libraries
@isa/ui/input-controls- DropdownButtonComponent, DropdownFilterComponent, DropdownOptionComponent
Path Alias
Import from: @isa/checkout/feature/select-branch-dropdown
// Import components
import {
SelectedBranchDropdownComponent,
BranchDropdownComponent
} from '@isa/checkout/feature/select-branch-dropdown';
Testing
The library uses Vitest with Angular Testing Utilities for testing.
Running Tests
# Run tests for this library
npx nx test checkout-feature-select-branch-dropdown --skip-nx-cache
# Run tests with coverage
npx nx test checkout-feature-select-branch-dropdown --coverage.enabled=true --skip-nx-cache
# Run tests in watch mode
npx nx test checkout-feature-select-branch-dropdown --watch
Test Configuration
- Framework: Vitest
- Test Runner: @nx/vite:test
- Coverage: v8 provider with Cobertura reports
- JUnit XML:
testresults/junit-checkout-feature-select-branch-dropdown.xml - Coverage XML:
coverage/libs/checkout/feature/select-branch-dropdown/cobertura-coverage.xml
Testing Recommendations
Component Testing
import { TestBed } from '@angular/core/testing';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { SelectedBranchDropdownComponent } from './selected-branch-dropdown.component';
import { CheckoutMetadataService, BranchesResource } from '@isa/checkout/data-access';
import { signal } from '@angular/core';
describe('SelectedBranchDropdownComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [SelectedBranchDropdownComponent],
providers: [
{
provide: CheckoutMetadataService,
useValue: {
setSelectedBranch: vi.fn(),
getSelectedBranch: vi.fn(() => null)
}
},
{
provide: BranchesResource,
useValue: {
resource: {
hasValue: () => true,
value: () => []
}
}
}
]
});
});
it('should create', () => {
const fixture = TestBed.createComponent(SelectedBranchDropdownComponent);
fixture.componentRef.setInput('tabId', 1);
fixture.detectChanges();
expect(fixture.componentInstance).toBeTruthy();
});
});
E2E Attribute Requirements
All interactive elements should include data-what and data-which attributes:
<!-- Dropdown -->
<select
data-what="branch-dropdown"
data-which="branch-selection">
</select>
<!-- Option -->
<option
data-what="branch-option"
[attr.data-which]="branch.id">
</option>
Best Practices
1. Always Use Tab Context
Ensure tab context is available when using the component:
// Good
const tabId = this.#tabService.activatedTabId();
if (tabId) {
this.#checkoutMetadata.setSelectedBranch(tabId, branch);
}
// Bad (missing tab context validation)
this.#checkoutMetadata.setSelectedBranch(null, branch);
2. Clear Branch on Checkout Completion
Always clear branch selection when checkout is complete:
// Good
completeCheckout() {
// ... checkout logic
this.#checkoutMetadata.setSelectedBranch(tabId, undefined);
}
// Bad (leaves stale branch)
completeCheckout() {
// ... checkout logic
// Forgot to clear branch!
}
3. Use Computed Signals for Derived State
Leverage Angular signals for reactive values:
// Good
selectedBranch = computed(() => {
return this.#checkoutMetadata.getSelectedBranch(this.tabId()!);
});
// Bad (manual tracking)
selectedBranch: Branch | undefined;
ngOnInit() {
effect(() => {
this.selectedBranch = this.#checkoutMetadata.getSelectedBranch(this.tabId()!);
});
}
4. Follow OnPush Change Detection
Always use OnPush change detection for performance:
// Good
@Component({
selector: 'checkout-select-branch-dropdown',
changeDetection: ChangeDetectionStrategy.OnPush,
// ...
})
// Bad (default change detection)
@Component({
selector: 'checkout-select-branch-dropdown',
// Missing changeDetection
})
5. Add E2E Attributes
Always include data-what and data-which attributes:
// Good
<select
data-what="branch-dropdown"
data-which="branch-selection"
[(ngModel)]="selectedBranch">
</select>
// Bad (no E2E attributes)
<select [(ngModel)]="selectedBranch">
</select>
Architecture Notes
Component Structure
SelectedBranchDropdownComponent (high-level, with metadata integration)
├── Uses BranchDropdownComponent internally
├── CheckoutMetadataService integration
├── Tab context via tabId input
└── Automatic branch persistence
BranchDropdownComponent (low-level, form-compatible)
├── ControlValueAccessor implementation
├── BranchesResource for loading options
├── DropdownButtonComponent from @isa/ui/input-controls
└── Filter support via DropdownFilterComponent
Data Flow
SelectedBranchDropdownComponent:
1. Component receives tabId input
↓
2. Reads current branch from CheckoutMetadataService.getSelectedBranch(tabId)
↓
3. User selects branch from dropdown
↓
4. setSelectedBranch(tabId, branch) called
↓
5. Other checkout components react to metadata change
BranchDropdownComponent:
1. BranchesResource loads available branches
↓
2. User selects branch from dropdown
↓
3. selected model emits new value
↓
4. ControlValueAccessor notifies form (if used with forms)
License
Internal ISA Frontend library - not for external distribution.