# @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: `

Shipping Address

` }) 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: `
Delivery to:
` }) 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: ` ` }) 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
` - **Description:** Address data to display - **Default:** `undefined` #### Template Structure ```html
@if (careOfLine()) {
{{ careOfLine() }}
} @if (streetLine()) {
{{ streetLine() }}
} @if (a.info) {
{{ a.info }}
} @if (cityLine()) {
{{ cityLine() }}
}
``` #### Computed Properties ##### `careOfLine()` - **Type:** `Signal` - **Returns:** `'c/o {careOf}'` if careOf exists, otherwise empty string ##### `streetLine()` - **Type:** `Signal` - **Returns:** Space-separated combination of street, streetNumber, and apartment - **Example:** `'Hauptstraße 123 Apt 4B'` ##### `cityLine()` - **Type:** `Signal` - **Returns:** Zip code and city, optionally with country name if non-German - **Example:** `'10115 Berlin'` or `'75001 Paris, France'` #### Example ```typescript ``` ### InlineAddressComponent Single-line address display component for compact layouts. **Selector:** `shared-inline-address` **Standalone:** `true` #### Inputs ##### `address` (optional) - **Type:** `InputSignal
` - **Description:** Address data to display - **Default:** `undefined` #### Template Structure ```html @if (addressLine()) { {{ addressLine() }} } ``` #### Computed Properties ##### `addressLine()` - **Type:** `Signal` - **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 ``` ### 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: `

Shipping Address

`, 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: `

Order #{{ order().orderId }}

Delivery to:
Total: {{ order().totalAmount | currency }}
` }) export class OrderSummaryComponent { order = input.required(); } ``` ### 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: `
@for (address of addresses; track address.id) {

{{ address.type }}

@if (isDesktop()) { } @else { }
}
` }) 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: `
@for (address of internationalAddresses; track $index) {

{{ address.country }}

}
` }) 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: `

Invoice Address

@if (showCareOfInput()) { }
` }) export class InvoiceAddressComponent { careOfName = ''; showCareOfInput = signal(false); invoiceAddress = signal
({ 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: `

Billing Address

Shipping Address

@if (!addressesMatch()) {
Billing and shipping addresses differ
}
` }) 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
...
...
...
...
``` ### 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; 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 // Good - Inline for lists and summaries // Avoid - Multi-line in compact layouts
``` ### 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
``` ### 4. Handle Missing Addresses Always handle undefined/null addresses: ```typescript // Good - Conditional rendering @if (customer().address) { } @else {

No address available

} // Avoid - Direct binding without check (component handles, but better to be explicit) ``` ### 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.