- Add new reward-order-confirmation feature library with components and store - Implement checkout completion orchestrator service for order finalization - Migrate checkout/oms/crm models to Zod schemas for better type safety - Add order creation facade and display order schemas - Update shopping cart facade with order completion flow - Add comprehensive tests for shopping cart facade - Update routing to include order confirmation page
26 KiB
@isa/shared/address
Comprehensive Angular components for displaying addresses in both multi-line and inline formats with automatic country name resolution and intelligent formatting.
Overview
The Address library provides two standalone components for rendering addresses in different layouts. It automatically formats address data, handles country name lookups, and applies special rules for German addresses. The library integrates with the CRM data access layer to resolve ISO country codes to localized country names.
Table of Contents
- Features
- Quick Start
- Core Concepts
- API Reference
- Usage Examples
- Address Formatting Rules
- Architecture Notes
- Testing
- Dependencies
Features
- Two display modes - Multi-line (
shared-address) and single-line (shared-inline-address) formats - Country name resolution - Automatic ISO 3166 Alpha-3 country code to name conversion
- German address handling - Special rule to hide country for German addresses
- Care of (c/o) support - Proper formatting of care-of address lines
- Signal-based reactivity - Efficient computed properties for address line formatting
- Optional fields - Graceful handling of missing address components
- E2E testing attributes - Built-in
data-whatanddata-whichattributes - Standalone components - Modern Angular architecture with explicit imports
- Flexible input - Accepts partial addresses with optional fields
- Type-safe interface - TypeScript interface for address data structure
Quick Start
1. Multi-Line Address Display
import { Component } from '@angular/core';
import { AddressComponent, Address } from '@isa/shared/address';
@Component({
selector: 'app-customer-details',
standalone: true,
imports: [AddressComponent],
template: `
<div class="address-section">
<h3>Shipping Address</h3>
<shared-address [address]="shippingAddress" />
</div>
`
})
export class CustomerDetailsComponent {
shippingAddress: Address = {
careOf: 'John Doe',
street: 'Hauptstraße',
streetNumber: '123',
apartment: 'Apt 4B',
zipCode: '10115',
city: 'Berlin',
country: 'DEU'
};
}
2. Inline Address Display
import { Component } from '@angular/core';
import { InlineAddressComponent, Address } from '@isa/shared/address';
@Component({
selector: 'app-order-summary',
standalone: true,
imports: [InlineAddressComponent],
template: `
<div class="order-info">
<span class="label">Delivery to:</span>
<shared-inline-address [address]="deliveryAddress" />
</div>
`
})
export class OrderSummaryComponent {
deliveryAddress: Address = {
street: 'Bakerstreet',
streetNumber: '221B',
zipCode: 'NW1 6XE',
city: 'London',
country: 'GBR'
};
}
3. Partial Address Handling
import { Component } from '@angular/core';
import { AddressComponent, Address } from '@isa/shared/address';
@Component({
selector: 'app-address-card',
standalone: true,
imports: [AddressComponent],
template: `
<shared-address [address]="partialAddress" />
`
})
export class AddressCardComponent {
// All fields are optional - component handles missing data gracefully
partialAddress: Address = {
street: 'Main Street',
zipCode: '12345',
city: 'Springfield'
// Missing: streetNumber, apartment, country, etc.
};
}
Core Concepts
Address Data Structure
The Address interface defines the structure for address data:
export interface Address {
apartment?: string; // Apartment/suite number
careOf?: string; // Care of (c/o) recipient
city?: string; // City name
country?: string; // ISO 3166 Alpha-3 country code (e.g., 'DEU', 'GBR', 'USA')
district?: string; // District (currently not displayed)
info?: string; // Additional address information
po?: string; // P.O. Box (currently not displayed)
region?: string; // Region (currently not displayed)
state?: string; // State (currently not displayed)
street?: string; // Street name
streetNumber?: string; // Street number
zipCode?: string; // ZIP/Postal code
}
All fields are optional to support partial addresses.
Multi-Line Format
The AddressComponent displays addresses in a structured multi-line format:
c/o John Doe
Hauptstraße 123 Apt 4B
Additional info
10115 Berlin
Lines displayed:
- Care of line:
c/o {careOf}(if careOf exists) - Street line:
{street} {streetNumber} {apartment}(if any exist) - Info line:
{info}(if exists) - City line:
{zipCode} {city}(if any exist), optionally, {country}(if non-German)
Inline Format
The InlineAddressComponent displays addresses in a single line:
Hauptstraße 123, 10115 Berlin
Format: {street} {streetNumber}, {zipCode} {city} optionally , {country} (if non-German)
Country Name Resolution
Both components automatically resolve ISO 3166 Alpha-3 country codes to localized country names:
// Input
country: 'GBR'
// Displayed
"United Kingdom"
The resolution uses the CountryResource from @isa/crm/data-access.
German Address Special Rule
Addresses with country code 'DEU' (Germany) do not display the country name:
// German address
{
street: 'Bahnhofstraße',
streetNumber: '1',
zipCode: '80335',
city: 'München',
country: 'DEU'
}
// Displays: "80335 München" (no "Germany")
// Non-German address
{
street: 'Rue de Rivoli',
streetNumber: '99',
zipCode: '75001',
city: 'Paris',
country: 'FRA'
}
// Displays: "75001 Paris, France"
This improves readability for the primary market (Germany) while maintaining clarity for international addresses.
API Reference
AddressComponent
Multi-line address display component with structured formatting.
Selector: shared-address
Standalone: true
Inputs
address (optional)
- Type:
InputSignal<Address | undefined> - Description: Address data to display
- Default:
undefined
Template Structure
<div class="flex flex-col" data-what="address-display">
@if (careOfLine()) {
<div data-which="care-of-line">{{ careOfLine() }}</div>
}
@if (streetLine()) {
<div data-which="street-line">{{ streetLine() }}</div>
}
@if (a.info) {
<div data-which="info">{{ a.info }}</div>
}
@if (cityLine()) {
<div data-which="city-line">{{ cityLine() }}</div>
}
</div>
Computed Properties
careOfLine()
- Type:
Signal<string> - Returns:
'c/o {careOf}'if careOf exists, otherwise empty string
streetLine()
- Type:
Signal<string> - Returns: Space-separated combination of street, streetNumber, and apartment
- Example:
'Hauptstraße 123 Apt 4B'
cityLine()
- Type:
Signal<string> - Returns: Zip code and city, optionally with country name if non-German
- Example:
'10115 Berlin'or'75001 Paris, France'
Example
<shared-address
[address]="{
careOf: 'Jane Smith',
street: 'Main Street',
streetNumber: '456',
zipCode: '10001',
city: 'New York',
country: 'USA'
}"
/>
<!-- Renders as:
c/o Jane Smith
Main Street 456
10001 New York, United States
-->
InlineAddressComponent
Single-line address display component for compact layouts.
Selector: shared-inline-address
Standalone: true
Inputs
address (optional)
- Type:
InputSignal<Address | undefined> - Description: Address data to display
- Default:
undefined
Template Structure
@if (addressLine()) {
{{ addressLine() }}
}
Computed Properties
addressLine()
- Type:
Signal<string> - Returns: Comma-separated address components with country if non-German
- Format:
{street} {streetNumber}, {zipCode} {city}optionally, {country} - Example:
'Baker Street 221B, NW1 6XE London, United Kingdom'
Example
<shared-inline-address
[address]="{
street: 'Oxford Street',
streetNumber: '100',
zipCode: 'W1D 1LL',
city: 'London',
country: 'GBR'
}"
/>
<!-- Renders as:
Oxford Street 100, W1D 1LL London, United Kingdom
-->
Address Interface
Type definition for address data structure.
export interface Address {
apartment?: string; // Apartment/suite number
careOf?: string; // Care of (c/o) recipient
city?: string; // City name
country?: string; // ISO 3166 Alpha-3 country code
district?: string; // District (not currently displayed)
info?: string; // Additional information
po?: string; // P.O. Box (not currently displayed)
region?: string; // Region (not currently displayed)
state?: string; // State (not currently displayed)
street?: string; // Street name
streetNumber?: string; // Street number
zipCode?: string; // ZIP/Postal code
}
All fields are optional to support various address formats and partial data.
Usage Examples
Customer Shipping Address
import { Component } from '@angular/core';
import { AddressComponent, Address } from '@isa/shared/address';
@Component({
selector: 'app-customer-shipping',
standalone: true,
imports: [AddressComponent],
template: `
<div class="shipping-section">
<h2 class="isa-text-heading-3-bold">Shipping Address</h2>
<shared-address [address]="shippingAddress" />
<button (click)="editAddress()">Edit Address</button>
</div>
`,
styles: [`
.shipping-section {
padding: 1rem;
border: 1px solid var(--isa-neutral-300);
border-radius: 0.5rem;
}
`]
})
export class CustomerShippingComponent {
shippingAddress: Address = {
careOf: 'Maria Schmidt',
street: 'Kurfürstendamm',
streetNumber: '100',
apartment: 'Wohnung 5',
info: 'Hinterhaus, 2. Stock',
zipCode: '10709',
city: 'Berlin',
country: 'DEU'
};
editAddress(): void {
// Navigate to address edit form
}
}
Order Summary with Inline Address
import { Component, input } from '@angular/core';
import { InlineAddressComponent, Address } from '@isa/shared/address';
interface Order {
orderId: string;
deliveryAddress: Address;
totalAmount: number;
}
@Component({
selector: 'app-order-summary',
standalone: true,
imports: [InlineAddressComponent],
template: `
<div class="order-summary">
<h3>Order #{{ order().orderId }}</h3>
<div class="order-details">
<div class="detail-row">
<span class="label">Delivery to:</span>
<shared-inline-address [address]="order().deliveryAddress" />
</div>
<div class="detail-row">
<span class="label">Total:</span>
<span>{{ order().totalAmount | currency }}</span>
</div>
</div>
</div>
`
})
export class OrderSummaryComponent {
order = input.required<Order>();
}
Address List with Both Formats
import { Component } from '@angular/core';
import { AddressComponent, InlineAddressComponent, Address } from '@isa/shared/address';
import { breakpoint, Breakpoint } from '@isa/ui/layout';
@Component({
selector: 'app-address-list',
standalone: true,
imports: [AddressComponent, InlineAddressComponent],
template: `
<div class="address-list">
@for (address of addresses; track address.id) {
<div class="address-card">
<h4>{{ address.type }}</h4>
<!-- Desktop: Multi-line format -->
@if (isDesktop()) {
<shared-address [address]="address.data" />
}
<!-- Mobile: Inline format -->
@else {
<shared-inline-address [address]="address.data" />
}
<button (click)="selectAddress(address.id)">
Select
</button>
</div>
}
</div>
`
})
export class AddressListComponent {
isDesktop = breakpoint([Breakpoint.Desktop, Breakpoint.DekstopL, Breakpoint.DekstopXL]);
addresses = [
{
id: 1,
type: 'Home',
data: {
street: 'Lindenstraße',
streetNumber: '45',
zipCode: '50674',
city: 'Köln',
country: 'DEU'
}
},
{
id: 2,
type: 'Office',
data: {
street: 'Goethestraße',
streetNumber: '12',
zipCode: '60313',
city: 'Frankfurt',
country: 'DEU'
}
}
];
selectAddress(id: number): void {
console.log('Selected address:', id);
}
}
International Addresses
import { Component } from '@angular/core';
import { AddressComponent, Address } from '@isa/shared/address';
@Component({
selector: 'app-international-addresses',
standalone: true,
imports: [AddressComponent],
template: `
<div class="addresses-grid">
@for (address of internationalAddresses; track $index) {
<div class="address-card">
<h4>{{ address.country }}</h4>
<shared-address [address]="address" />
</div>
}
</div>
`
})
export class InternationalAddressesComponent {
internationalAddresses: Address[] = [
// Germany (country name hidden)
{
street: 'Alexanderplatz',
streetNumber: '1',
zipCode: '10178',
city: 'Berlin',
country: 'DEU'
},
// United Kingdom (country name shown)
{
street: 'Downing Street',
streetNumber: '10',
zipCode: 'SW1A 2AA',
city: 'London',
country: 'GBR'
},
// France (country name shown)
{
street: 'Champs-Élysées',
streetNumber: '1',
zipCode: '75008',
city: 'Paris',
country: 'FRA'
},
// USA (country name shown)
{
street: 'Pennsylvania Avenue NW',
streetNumber: '1600',
zipCode: '20500',
city: 'Washington',
country: 'USA'
}
];
}
Invoice Address with Care Of
import { Component, signal } from '@angular/core';
import { AddressComponent, Address } from '@isa/shared/address';
@Component({
selector: 'app-invoice-address',
standalone: true,
imports: [AddressComponent],
template: `
<div class="invoice-section">
<h3 class="isa-text-heading-3-bold">Invoice Address</h3>
<div class="address-container">
<shared-address [address]="invoiceAddress()" />
</div>
@if (showCareOfInput()) {
<input
type="text"
placeholder="Care of recipient"
[(ngModel)]="careOfName"
(blur)="updateCareOf()"
/>
}
</div>
`
})
export class InvoiceAddressComponent {
careOfName = '';
showCareOfInput = signal(false);
invoiceAddress = signal<Address>({
street: 'Friedrichstraße',
streetNumber: '200',
zipCode: '10117',
city: 'Berlin',
country: 'DEU'
});
updateCareOf(): void {
this.invoiceAddress.update(addr => ({
...addr,
careOf: this.careOfName || undefined
}));
}
}
Address Comparison
import { Component } from '@angular/core';
import { AddressComponent, InlineAddressComponent, Address } from '@isa/shared/address';
@Component({
selector: 'app-address-comparison',
standalone: true,
imports: [AddressComponent, InlineAddressComponent],
template: `
<div class="comparison-container">
<div class="address-column">
<h3>Billing Address</h3>
<shared-address [address]="billingAddress" />
</div>
<div class="address-column">
<h3>Shipping Address</h3>
<shared-address [address]="shippingAddress" />
</div>
@if (!addressesMatch()) {
<div class="warning">
Billing and shipping addresses differ
</div>
}
</div>
`
})
export class AddressComparisonComponent {
billingAddress: Address = {
street: 'Hauptstraße',
streetNumber: '100',
zipCode: '10115',
city: 'Berlin',
country: 'DEU'
};
shippingAddress: Address = {
careOf: 'Office Reception',
street: 'Nebenstraße',
streetNumber: '50',
zipCode: '20095',
city: 'Hamburg',
country: 'DEU'
};
addressesMatch(): boolean {
return JSON.stringify(this.billingAddress) === JSON.stringify(this.shippingAddress);
}
}
Address Formatting Rules
Street Line Formatting
The street line combines multiple components with spaces:
// All components present
street: 'Hauptstraße'
streetNumber: '123'
apartment: 'Apt 4B'
// Result: "Hauptstraße 123 Apt 4B"
// Only street and number
street: 'Main Street'
streetNumber: '456'
// Result: "Main Street 456"
// Only street
street: 'Broadway'
// Result: "Broadway"
// No components
// Result: "" (line not displayed)
City Line Formatting
The city line combines zip code, city, and optionally country:
// German address (country hidden)
zipCode: '10115'
city: 'Berlin'
country: 'DEU'
// Result: "10115 Berlin"
// Non-German address (country shown)
zipCode: 'SW1A 1AA'
city: 'London'
country: 'GBR'
// Result: "SW1A 1AA London, United Kingdom"
// Only city
city: 'Paris'
// Result: "Paris"
// Only zip
zipCode: '75001'
// Result: "75001"
// No components
// Result: "" (line not displayed)
Inline Formatting
Inline format uses commas to separate components:
// Full address
street: 'Oxford Street'
streetNumber: '100'
zipCode: 'W1D 1LL'
city: 'London'
country: 'GBR'
// Result: "Oxford Street 100, W1D 1LL London, United Kingdom"
// Partial address
street: 'Main Street'
city: 'Springfield'
// Result: "Main Street, Springfield"
// Only street
street: 'Broadway'
// Result: "Broadway"
Country Name Resolution
The components resolve ISO 3166 Alpha-3 codes to country names:
'DEU' → 'Deutschland' (or localized equivalent)
'GBR' → 'United Kingdom'
'USA' → 'United States'
'FRA' → 'France'
'AUT' → 'Austria'
'CHE' → 'Switzerland'
Fallback: If country code not found in resource, displays the code itself.
Architecture Notes
Design Patterns
Computed Signal Pattern
Both components use computed signals for efficient reactive formatting:
streetLine = computed(() => {
const a = this.address();
if (!a) return '';
const parts: string[] = [];
if (a.street) parts.push(a.street);
if (a.streetNumber) parts.push(a.streetNumber);
if (a.apartment) parts.push(a.apartment);
return parts.join(' ');
});
Benefits:
- Automatic re-computation when address changes
- Efficient - only recalculates affected computed values
- OnPush compatible
- No manual subscription management
Resource Integration Pattern
Uses CountryResource from CRM data access for country lookups:
#countryResource = inject(CountryResource);
#getCountryName(countryCode: string): string {
const countries = this.#countryResource.resource.value();
if (!countries || !Array.isArray(countries)) return countryCode;
const country = countries.find((c) => c.isO3166_A_3 === countryCode);
return country?.name || countryCode;
}
Benefits:
- Centralized country data management
- Automatic localization support
- Fallback to code if resource unavailable
Optional Field Handling
All address fields are optional, with graceful degradation:
// Component handles undefined/null addresses
@if (address(); as a) {
// Display logic
}
// Computed properties return empty strings for missing data
if (!a || !a.careOf) return '';
Component Separation
Two components serve different use cases:
AddressComponent: Structured multi-line display for detailed viewsInlineAddressComponent: Compact single-line for lists and summaries
This separation of concerns improves:
- Code maintainability
- Component reusability
- Performance (import only what you need)
E2E Testing Support
Both components include data-what and data-which attributes for automated testing:
<div class="flex flex-col" data-what="address-display">
<div data-which="care-of-line">...</div>
<div data-which="street-line">...</div>
<div data-which="info">...</div>
<div data-which="city-line">...</div>
</div>
Performance Considerations
- Computed Signals - Efficient change detection with minimal recalculations
- OnPush Compatible - Works with OnPush change detection strategy
- Lazy Resource Loading - Country resource loaded on demand
- Minimal DOM - Only renders lines with actual content
Future Enhancements
Potential improvements identified:
- State/Region Display - Add support for displaying state/region field
- P.O. Box Support - Integrate po field into formatting logic
- Custom Formatting - Allow custom format templates via input
- Address Validation - Integrate with address validation service
- Localization - Locale-specific address formatting rules
- Copy to Clipboard - Built-in copy functionality
- Map Integration - Optional map link for addresses
Testing
The library uses Vitest with Angular Testing Utilities for testing.
Running Tests
# Run tests for this library
npx nx test shared-address --skip-nx-cache
# Run tests with coverage
npx nx test shared-address --code-coverage --skip-nx-cache
# Run tests in watch mode
npx nx test shared-address --watch
Test Structure
The library includes unit tests covering:
- Address rendering - Tests multi-line and inline formatting
- Country resolution - Tests country name lookup
- German address handling - Tests country name hiding for DEU
- Missing fields - Tests graceful handling of partial addresses
- E2E attributes - Tests presence of data-what and data-which attributes
Example Test
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AddressComponent, Address } from '@isa/shared/address';
import { CountryResource } from '@isa/crm/data-access';
describe('AddressComponent', () => {
let component: AddressComponent;
let fixture: ComponentFixture<AddressComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AddressComponent],
providers: [
// Mock CountryResource
{
provide: CountryResource,
useValue: {
resource: {
value: () => [
{ isO3166_A_3: 'DEU', name: 'Germany' },
{ isO3166_A_3: 'GBR', name: 'United Kingdom' }
]
}
}
}
]
}).compileComponents();
fixture = TestBed.createComponent(AddressComponent);
component = fixture.componentInstance;
});
it('should display multi-line address', () => {
const testAddress: Address = {
street: 'Main Street',
streetNumber: '123',
zipCode: '12345',
city: 'Berlin',
country: 'DEU'
};
fixture.componentRef.setInput('address', testAddress);
fixture.detectChanges();
const element = fixture.nativeElement;
expect(element.textContent).toContain('Main Street 123');
expect(element.textContent).toContain('12345 Berlin');
expect(element.textContent).not.toContain('Germany'); // Hidden for DEU
});
it('should display country for non-German addresses', () => {
const testAddress: Address = {
street: 'Oxford Street',
streetNumber: '100',
zipCode: 'W1D 1LL',
city: 'London',
country: 'GBR'
};
fixture.componentRef.setInput('address', testAddress);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('United Kingdom');
});
});
Dependencies
Required Libraries
@angular/core- Angular framework for components and signals@isa/crm/data-access- CRM data access layer for country resource
Path Alias
Import from: @isa/shared/address
Exports
AddressComponent- Multi-line address display componentInlineAddressComponent- Single-line address display componentAddress- TypeScript interface for address data
Best Practices
1. Use Appropriate Component
Choose the right component for your use case:
// Good - Multi-line for detail pages
<shared-address [address]="customerAddress" />
// Good - Inline for lists and summaries
<shared-inline-address [address]="deliveryAddress" />
// Avoid - Multi-line in compact layouts
<div class="compact-list">
<shared-address [address]="addr" /> <!-- Too verbose -->
</div>
2. Provide Complete Address Data
Include all relevant address fields:
// Good - Complete address
const address: Address = {
street: 'Hauptstraße',
streetNumber: '100',
zipCode: '10115',
city: 'Berlin',
country: 'DEU'
};
// Acceptable - Partial address (component handles gracefully)
const partialAddress: Address = {
city: 'Berlin'
};
3. Include E2E Attributes in Parent
Add test attributes to parent containers:
<div class="address-section" data-what="customer-address-section">
<shared-address [address]="address" />
</div>
4. Handle Missing Addresses
Always handle undefined/null addresses:
// Good - Conditional rendering
@if (customer().address) {
<shared-address [address]="customer().address" />
} @else {
<p>No address available</p>
}
// Avoid - Direct binding without check (component handles, but better to be explicit)
<shared-address [address]="possiblyUndefinedAddress" />
5. Use Type-Safe Address Objects
Leverage TypeScript for type safety:
import { Address } from '@isa/shared/address';
// Good - Type-safe
const address: Address = {
street: 'Main Street',
zipCode: '12345'
};
// Avoid - Untyped objects
const address = {
street: 'Main Street',
zip: '12345' // Wrong property name!
};
License
Internal ISA Frontend library - not for external distribution.