- 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
17 KiB
@isa/shared/product-image
A lightweight Angular library providing a directive and service for displaying product images from a CDN with dynamic sizing and fallback support.
Overview
The Product Image library simplifies the integration of product images throughout the ISA application by providing a declarative directive and a service for generating image URLs. It integrates with the application's configuration system to automatically fetch the CDN base URL and supports dynamic image dimensions, fallback images, and optional dummy image display.
Table of Contents
- Features
- Quick Start
- Core Concepts
- API Reference
- Usage Examples
- Configuration
- Testing
- Architecture Notes
Features
- Declarative directive - Simple
sharedProductImagedirective for img elements - CDN integration - Automatic CDN URL configuration from app config
- Dynamic sizing - Configurable width and height with intelligent defaults
- Dummy fallback - Optional dummy image display when product image unavailable
- Signal-based reactivity - Uses Angular signals for efficient change detection
- Type-safe configuration - Zod validation for CDN URL configuration
- Custom provider - Override CDN URL for testing or alternative environments
- Standalone architecture - Modern Angular standalone directive
Quick Start
1. Import the Directive
import { Component } from '@angular/core';
import { ProductImageDirective } from '@isa/shared/product-image';
@Component({
selector: 'app-product-list',
standalone: true,
imports: [ProductImageDirective],
template: `
<div class="product-grid">
@for (product of products; track product.ean) {
<div class="product-card">
<img
sharedProductImage
[ean]="product.ean"
[imageWidth]="300"
[imageHeight]="300"
alt="Product image"
/>
</div>
}
</div>
`
})
export class ProductListComponent {
products = [
{ ean: '1234567890123', name: 'Product 1' },
{ ean: '9876543210987', name: 'Product 2' }
];
}
2. Use with Default Dimensions
The directive uses sensible defaults (150x150) if dimensions are not specified:
@Component({
template: `
<!-- Uses default 150x150 dimensions -->
<img sharedProductImage [ean]="productEan" alt="Product" />
`
})
export class ProductDetailComponent {
productEan = '1234567890123';
}
3. Use the Service Directly
For programmatic URL generation:
import { Component, inject } from '@angular/core';
import { ProductImageService } from '@isa/shared/product-image';
@Component({
selector: 'app-custom-image',
template: `
<div [style.background-image]="'url(' + imageUrl + ')'"></div>
`
})
export class CustomImageComponent {
#productImageService = inject(ProductImageService);
imageUrl = this.#productImageService.getImageUrl({
imageId: '1234567890123',
width: 400,
height: 300,
showDummy: true
});
}
Core Concepts
Image URL Structure
The library generates CDN image URLs following this format:
{CDN_BASE_URL}/{imageId}_{width}x{height}.jpg?showDummy={showDummy}
Example:
https://cdn.example.com/1234567890123_300x300.jpg?showDummy=true
Image ID (EAN)
Product images are identified by their EAN (European Article Number), which serves as the imageId in the CDN URL structure. This ensures a consistent 1:1 mapping between products and their images.
Dynamic Sizing
The library supports dynamic image dimensions to optimize for different use cases:
- Thumbnail: 150x150 (default)
- Product Card: 300x300
- Detail View: 600x600
- Custom: Any width/height combination
The CDN automatically scales and serves the requested dimensions, improving page load performance.
Dummy Image Fallback
The showDummy parameter controls fallback behavior:
true(default): Display a generic placeholder when the actual product image is unavailablefalse: Return no image (useful for progressive loading scenarios)
API Reference
ProductImageDirective
A standalone directive that sets the src attribute of img elements to product image URLs.
Selector: img[sharedProductImage]
Inputs
ean (required)
- Type:
InputSignal<string> - Description: The product's EAN number, used as the image identifier
imageWidth (optional)
- Type:
InputSignal<number> - Default:
150 - Description: Desired image width in pixels
imageHeight (optional)
- Type:
InputSignal<number> - Default:
150 - Description: Desired image height in pixels
Host Bindings
[src]: Automatically binds to the computed image URL
Example
<img
sharedProductImage
[ean]="productEan"
[imageWidth]="300"
[imageHeight]="300"
alt="Product image"
class="rounded-lg shadow-md"
/>
ProductImageService
Injectable service for generating product image URLs.
Provided in: 'root'
Methods
getImageUrl(options): string
Generates a CDN image URL for the specified product.
Parameters:
options.imageId: string- Product EAN (image identifier)options.width?: number- Image width in pixels (default: 150)options.height?: number- Image height in pixels (default: 150)options.showDummy?: boolean- Show dummy/placeholder image if unavailable (default: true)
Returns: string - Complete CDN image URL
Example:
const url = productImageService.getImageUrl({
imageId: '1234567890123',
width: 400,
height: 400,
showDummy: false
});
// Result: "https://cdn.example.com/1234567890123_400x400.jpg?showDummy=false"
Properties
imageUrl: string
- Type:
string - Description: The base CDN URL injected from configuration
- Access:
readonly
PRODUCT_IMAGE_URL
Injection token providing the CDN base URL for product images.
Type: InjectionToken<string>
Default Factory:
inject(Config).get('@cdn/product-image.url', z.string().url())
The default implementation reads from the application's configuration service and validates the URL using Zod.
provideProductImageUrl(url)
Provider function for overriding the product image CDN URL.
Parameters:
url: string- Custom CDN base URL
Returns: Provider[] - Array of providers to include in application/component providers
Use Cases:
- Testing with mock image server
- Environment-specific CDN URLs
- Development vs. production configurations
Example:
// In app.config.ts or testing setup
import { provideProductImageUrl } from '@isa/shared/product-image';
export const appConfig: ApplicationConfig = {
providers: [
provideProductImageUrl('https://test-cdn.example.com'),
// other providers...
]
};
Usage Examples
Product Grid with Multiple Sizes
import { Component } from '@angular/core';
import { ProductImageDirective } from '@isa/shared/product-image';
@Component({
selector: 'app-product-grid',
standalone: true,
imports: [ProductImageDirective],
template: `
<div class="grid grid-cols-4 gap-4">
@for (product of products; track product.ean) {
<div class="product-card">
<img
sharedProductImage
[ean]="product.ean"
[imageWidth]="250"
[imageHeight]="250"
alt="{{ product.name }}"
class="w-full h-auto rounded"
/>
<h3>{{ product.name }}</h3>
</div>
}
</div>
`
})
export class ProductGridComponent {
products = [
{ ean: '1234567890123', name: 'Laptop Computer' },
{ ean: '2345678901234', name: 'Wireless Mouse' },
{ ean: '3456789012345', name: 'USB Cable' },
{ ean: '4567890123456', name: 'Monitor Stand' }
];
}
Thumbnail vs. Detail View
import { Component, signal } from '@angular/core';
import { ProductImageDirective } from '@isa/shared/product-image';
@Component({
selector: 'app-product-detail',
standalone: true,
imports: [ProductImageDirective],
template: `
<div class="flex gap-4">
<!-- Thumbnail gallery -->
<div class="flex flex-col gap-2">
@for (ean of productImages; track ean) {
<img
sharedProductImage
[ean]="ean"
[imageWidth]="100"
[imageHeight]="100"
(click)="selectedEan.set(ean)"
class="cursor-pointer border-2"
[class.border-isa-accent-500]="selectedEan() === ean"
/>
}
</div>
<!-- Large preview -->
<div class="flex-1">
<img
sharedProductImage
[ean]="selectedEan()"
[imageWidth]="600"
[imageHeight]="600"
class="w-full h-auto"
/>
</div>
</div>
`
})
export class ProductDetailComponent {
productImages = [
'1234567890123',
'1234567890124',
'1234567890125'
];
selectedEan = signal(this.productImages[0]);
}
Programmatic URL Generation
import { Component, inject, computed, signal } from '@angular/core';
import { ProductImageService } from '@isa/shared/product-image';
@Component({
selector: 'app-background-image',
template: `
<div
class="hero-section"
[style.background-image]="backgroundUrl()"
>
<h1>Featured Product</h1>
</div>
`,
styles: [`
.hero-section {
width: 100%;
height: 400px;
background-size: cover;
background-position: center;
}
`]
})
export class BackgroundImageComponent {
#productImageService = inject(ProductImageService);
featuredEan = signal('1234567890123');
backgroundUrl = computed(() => {
const url = this.#productImageService.getImageUrl({
imageId: this.featuredEan(),
width: 1920,
height: 600,
showDummy: false
});
return `url(${url})`;
});
}
Responsive Images with Different Sizes
import { Component } from '@angular/core';
import { ProductImageDirective } from '@isa/shared/product-image';
import { breakpoint, Breakpoint } from '@isa/ui/layout';
@Component({
selector: 'app-responsive-image',
standalone: true,
imports: [ProductImageDirective],
template: `
@if (isDesktop()) {
<img
sharedProductImage
[ean]="productEan"
[imageWidth]="600"
[imageHeight]="600"
alt="Product"
/>
} @else {
<img
sharedProductImage
[ean]="productEan"
[imageWidth]="300"
[imageHeight]="300"
alt="Product"
/>
}
`
})
export class ResponsiveImageComponent {
productEan = '1234567890123';
isDesktop = breakpoint([Breakpoint.Desktop, Breakpoint.DekstopL, Breakpoint.DekstopXL]);
}
Configuration
Application-Level Configuration
The library integrates with @isa/core/config to read the CDN URL from the application configuration:
// In your configuration file (e.g., environment.ts)
export const environment = {
'@cdn/product-image.url': 'https://cdn.example.com/products'
};
// The service automatically reads this configuration
// No additional setup required
Override for Testing
Use the provideProductImageUrl function to override the CDN URL for testing:
// In test setup or app.config.ts
import { provideProductImageUrl } from '@isa/shared/product-image';
export const appConfig: ApplicationConfig = {
providers: [
provideProductImageUrl('http://localhost:3000/mock-images'),
// other providers...
]
};
Testing
The library uses Jest for testing.
Running Tests
# Run tests for this library
npx nx test shared-product-image --skip-nx-cache
# Run tests with coverage
npx nx test shared-product-image --code-coverage --skip-nx-cache
# Run tests in watch mode
npx nx test shared-product-image --watch
Test Examples
Testing the Directive
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Component } from '@angular/core';
import { ProductImageDirective, provideProductImageUrl } from '@isa/shared/product-image';
@Component({
standalone: true,
imports: [ProductImageDirective],
template: `<img sharedProductImage [ean]="ean" [imageWidth]="300" [imageHeight]="300" />`
})
class TestComponent {
ean = '1234567890123';
}
describe('ProductImageDirective', () => {
let fixture: ComponentFixture<TestComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TestComponent],
providers: [
provideProductImageUrl('https://test-cdn.example.com')
]
}).compileComponents();
fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
});
it('should set src attribute with correct URL', () => {
const img = fixture.nativeElement.querySelector('img');
expect(img.src).toBe('https://test-cdn.example.com/1234567890123_300x300.jpg?showDummy=true');
});
});
Testing the Service
import { TestBed } from '@angular/core/testing';
import { ProductImageService, provideProductImageUrl } from '@isa/shared/product-image';
describe('ProductImageService', () => {
let service: ProductImageService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideProductImageUrl('https://cdn.test.com')
]
});
service = TestBed.inject(ProductImageService);
});
it('should generate URL with default dimensions', () => {
const url = service.getImageUrl({ imageId: '1234567890123' });
expect(url).toBe('https://cdn.test.com/1234567890123_150x150.jpg?showDummy=true');
});
it('should generate URL with custom dimensions', () => {
const url = service.getImageUrl({
imageId: '1234567890123',
width: 500,
height: 400,
showDummy: false
});
expect(url).toBe('https://cdn.test.com/1234567890123_500x400.jpg?showDummy=false');
});
});
Architecture Notes
Design Patterns
Service + Directive Pattern
The library uses a separation of concerns pattern:
- Service: Pure URL generation logic, easily testable
- Directive: Declarative UI integration, leverages Angular's host binding
This separation allows the service to be used independently for programmatic scenarios while the directive provides a simple template-based API.
Dependency Injection Strategy
Uses Angular's injection token pattern with a factory default:
export const PRODUCT_IMAGE_URL = new InjectionToken<string>('PRODUCT_IMAGE_URL', {
factory: () => inject(Config).get('@cdn/product-image.url', z.string().url()),
});
This approach provides:
- Default behavior: Automatic config integration
- Override capability: Custom providers for testing
- Type safety: Zod validation ensures valid URLs
Signal-Based Reactivity
The directive uses Angular signals for optimal change detection:
src = computed(() => {
const ean = this.ean();
const width = this.imageWidth();
const height = this.imageHeight();
return this.productImageService.getImageUrl({ imageId: ean, height, width });
});
Benefits:
- Automatic reactivity when inputs change
- Efficient change detection (OnPush-compatible)
- No manual subscription management needed
Host Binding Pattern
The directive uses host property binding to set the src attribute:
@Directive({
selector: 'img[sharedProductImage]',
host: { '[src]': 'src()' }
})
This ensures the directive can only be applied to img elements and automatically manages the src attribute lifecycle.
Dependencies
Required Libraries
@angular/core- Angular framework@isa/core/config- Application configuration servicezod- Schema validation for configuration
Path Alias
Import from: @isa/shared/product-image
Performance Considerations
CDN Optimization
- Images are served from CDN with automatic caching
- Dynamic sizing reduces bandwidth for thumbnails and previews
- Query parameter allows CDN to cache different image variants
Change Detection
- Uses OnPush-compatible signal-based reactivity
- Computed values only recalculate when inputs change
- No unnecessary re-renders
Best Practices
- Use appropriate dimensions: Don't request 600x600 images for 100px thumbnails
- Lazy loading: Combine with native
loading="lazy"or custom intersection observer - Alt text: Always provide meaningful alt text for accessibility
- Fallback handling: Set
showDummy="false"when implementing custom error handling
License
Internal ISA Frontend library - not for external distribution.