mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
- 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
1076 lines
26 KiB
Markdown
1076 lines
26 KiB
Markdown
# @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](#features)
|
|
- [Quick Start](#quick-start)
|
|
- [Core Concepts](#core-concepts)
|
|
- [API Reference](#api-reference)
|
|
- [Usage Examples](#usage-examples)
|
|
- [Address Formatting Rules](#address-formatting-rules)
|
|
- [Architecture Notes](#architecture-notes)
|
|
- [Testing](#testing)
|
|
- [Dependencies](#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-what` and `data-which` attributes
|
|
- **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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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:**
|
|
1. **Care of line**: `c/o {careOf}` (if careOf exists)
|
|
2. **Street line**: `{street} {streetNumber} {apartment}` (if any exist)
|
|
3. **Info line**: `{info}` (if exists)
|
|
4. **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:
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```html
|
|
<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
|
|
|
|
```typescript
|
|
<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
|
|
|
|
```html
|
|
@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
|
|
|
|
```typescript
|
|
<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.
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
'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:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
#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:
|
|
|
|
```typescript
|
|
// 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 views
|
|
- **`InlineAddressComponent`**: 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:
|
|
|
|
```html
|
|
<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
|
|
|
|
1. **Computed Signals** - Efficient change detection with minimal recalculations
|
|
2. **OnPush Compatible** - Works with OnPush change detection strategy
|
|
3. **Lazy Resource Loading** - Country resource loaded on demand
|
|
4. **Minimal DOM** - Only renders lines with actual content
|
|
|
|
### Future Enhancements
|
|
|
|
Potential improvements identified:
|
|
|
|
1. **State/Region Display** - Add support for displaying state/region field
|
|
2. **P.O. Box Support** - Integrate po field into formatting logic
|
|
3. **Custom Formatting** - Allow custom format templates via input
|
|
4. **Address Validation** - Integrate with address validation service
|
|
5. **Localization** - Locale-specific address formatting rules
|
|
6. **Copy to Clipboard** - Built-in copy functionality
|
|
7. **Map Integration** - Optional map link for addresses
|
|
|
|
## Testing
|
|
|
|
The library uses **Vitest** with **Angular Testing Utilities** for testing.
|
|
|
|
### Running Tests
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```typescript
|
|
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 component
|
|
- `InlineAddressComponent` - Single-line address display component
|
|
- `Address` - TypeScript interface for address data
|
|
|
|
## Best Practices
|
|
|
|
### 1. Use Appropriate Component
|
|
|
|
Choose the right component for your use case:
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
<div class="address-section" data-what="customer-address-section">
|
|
<shared-address [address]="address" />
|
|
</div>
|
|
```
|
|
|
|
### 4. Handle Missing Addresses
|
|
|
|
Always handle undefined/null addresses:
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
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.
|