# @isa/shared/product-router-link

An Angular library providing a customizable directive for creating product navigation links based on EAN codes with flexible URL generation strategies.

## Overview

The Product Router Link library extends Angular's `RouterLink` to provide a declarative way of navigating to product detail pages using EAN codes. It features a pluggable URL builder system that allows applications to customize how product URLs are generated, making it adaptable to different routing strategies and application requirements.

## Table of Contents

- [Features](#features)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [API Reference](#api-reference)
- [Usage Examples](#usage-examples)
- [Configuration](#configuration)
- [Testing](#testing)
- [Architecture Notes](#architecture-notes)

## Features

- **Declarative navigation** - Simple directive extending Angular's RouterLink
- **EAN-based routing** - Automatic URL generation from product EAN codes
- **Pluggable URL builder** - Customizable URL generation strategy via DI
- **Async URL support** - Builder can return Promise for dynamic URL resolution
- **Effect-based updates** - Reactive URL updates when EAN changes
- **Type-safe configuration** - TypeScript interfaces for custom builders
- **Cursor styling** - Automatic pointer cursor for better UX
- **Standalone architecture** - Modern Angular standalone directive

## Quick Start

### 1. Basic Usage with Default Builder

The library includes a default URL builder that generates product detail URLs:

```typescript
import { Component } from '@angular/core';
import { ProductRouterLinkDirective } from '@isa/shared/product-router-link';

@Component({
  selector: 'app-product-list',
  standalone: true,
  imports: [ProductRouterLinkDirective],
  template: `
    <div class="product-grid">
      @for (product of products; track product.ean) {
        <div
          sharedProductRouterLink
          [ean]="product.ean"
          class="product-card"
        >
          <h3>{{ product.name }}</h3>
          <p>Click to view details</p>
        </div>
      }
    </div>
  `
})
export class ProductListComponent {
  products = [
    { ean: '1234567890123', name: 'Product 1' },
    { ean: '9876543210987', name: 'Product 2' }
  ];
}
```

### 2. Custom URL Builder

Override the default URL builder to match your routing structure:

```typescript
import { ApplicationConfig } from '@angular/core';
import { provideProductRouterLinkBuilder } from '@isa/shared/product-router-link';

export const appConfig: ApplicationConfig = {
  providers: [
    provideProductRouterLinkBuilder((ean: string) => {
      return `/products/${ean}`;
    })
  ]
};
```

### 3. Async URL Builder

Use async builders for dynamic URL resolution:

```typescript
import { inject } from '@angular/core';
import { provideProductRouterLinkBuilder } from '@isa/shared/product-router-link';
import { ProductService } from './product.service';

export const appConfig: ApplicationConfig = {
  providers: [
    provideProductRouterLinkBuilder(async (ean: string) => {
      const productService = inject(ProductService);
      const product = await productService.getProductByEan(ean);
      return `/category/${product.categoryId}/product/${ean}`;
    })
  ]
};
```

## Core Concepts

### EAN-Based Navigation

The directive uses EAN (European Article Number) as the primary identifier for product navigation. This ensures:

- **Consistent routing**: Same EAN always links to same product
- **SEO-friendly**: Product URLs can include EAN for search optimization
- **API integration**: EAN directly maps to backend product identifiers

### URL Builder Pattern

The library uses the Strategy pattern for URL generation:

```typescript
type ProductRouterLinkBuilder = (ean: string) => PromiseLike<string> | string;
```

This allows:
- **Customization**: Each application defines its own routing structure
- **Flexibility**: Synchronous or asynchronous URL generation
- **Testability**: Easy to mock and test different URL strategies

### Default URL Format

Without custom configuration, the default builder generates URLs in this format:

```
/kunde/{timestamp}/product/details/{ean}/ean
```

**Example:**
```
/kunde/1698765432000/product/details/1234567890123/ean
```

The timestamp ensures each navigation creates a new route entry, useful for certain navigation stack requirements.

### Effect-Based Reactivity

The directive uses Angular effects to automatically update the router link when the EAN input changes:

```typescript
urlEffect = effect(async () => {
  const ean = this.ean();
  if (!ean) return;
  const url = await this.#builder(ean);
  this.routerLink = url;
});
```

This ensures the link is always synchronized with the current EAN value.

## API Reference

### ProductRouterLinkDirective

A standalone directive extending Angular's `RouterLink` for EAN-based product navigation.

**Selector:** `[sharedProductRouterLink]`

**Extends:** `RouterLink`

#### Inputs

##### `ean` (required)
- **Type:** `InputSignal<string>`
- **Description:** The product's EAN code to navigate to

#### Host Bindings

- **`class`**: Automatically adds `'cursor-pointer'` for better UX

#### Inherited Properties

Inherits all properties from Angular's `RouterLink`:
- `routerLink` - The dynamically generated product URL
- `queryParams` - Optional query parameters
- `fragment` - Optional URL fragment
- `preserveFragment` - Whether to preserve URL fragment
- `skipLocationChange` - Whether to skip location change
- `replaceUrl` - Whether to replace current history entry
- `state` - Navigation state

#### Example

```typescript
<div
  sharedProductRouterLink
  [ean]="productEan"
  [queryParams]="{ source: 'search' }"
>
  View Product Details
</div>
```

### ProductRouterLinkBuilder

Type definition for custom URL builder functions.

**Type:**
```typescript
type ProductRouterLinkBuilder = (ean: string) => PromiseLike<string> | string;
```

**Parameters:**
- `ean: string` - The product EAN to build a URL for

**Returns:** `PromiseLike<string> | string` - The generated product URL (can be async)

**Example Implementations:**

```typescript
// Synchronous builder
const syncBuilder: ProductRouterLinkBuilder = (ean) => {
  return `/products/${ean}`;
};

// Asynchronous builder
const asyncBuilder: ProductRouterLinkBuilder = async (ean) => {
  const category = await fetchCategoryForEan(ean);
  return `/categories/${category}/products/${ean}`;
};
```

### PRODUCT_ROUTER_LINK_BUILDER

Injection token for the product router link builder function.

**Type:** `InjectionToken<ProductRouterLinkBuilder>`

**Default Factory:**
```typescript
(ean: string) => `/kunde/${Date.now()}/product/details/${ean}/ean`
```

The default implementation creates URLs with timestamps for unique navigation entries.

### provideProductRouterLinkBuilder(builder)

Provider function for configuring a custom product URL builder.

**Parameters:**
- `builder: ProductRouterLinkBuilder` - Custom URL builder function

**Returns:** `Provider[]` - Array of providers to include in application config

**Example:**
```typescript
import { provideProductRouterLinkBuilder } from '@isa/shared/product-router-link';

export const appConfig: ApplicationConfig = {
  providers: [
    provideProductRouterLinkBuilder((ean) => `/shop/product/${ean}`)
  ]
};
```

## Usage Examples

### Product Card Navigation

```typescript
import { Component } from '@angular/core';
import { ProductRouterLinkDirective } from '@isa/shared/product-router-link';
import { ProductImageDirective } from '@isa/shared/product-image';

@Component({
  selector: 'app-product-card',
  standalone: true,
  imports: [ProductRouterLinkDirective, ProductImageDirective],
  template: `
    <div
      sharedProductRouterLink
      [ean]="product.ean"
      class="product-card hover:shadow-lg transition-shadow"
    >
      <img
        sharedProductImage
        [ean]="product.ean"
        [imageWidth]="300"
        [imageHeight]="300"
        alt="{{ product.name }}"
      />
      <h3 class="font-bold">{{ product.name }}</h3>
      <p class="text-gray-600">{{ product.price | currency }}</p>
    </div>
  `,
  styles: [`
    .product-card {
      padding: 1rem;
      border: 1px solid #e5e7eb;
      border-radius: 0.5rem;
      transition: box-shadow 0.2s;
    }
  `]
})
export class ProductCardComponent {
  @Input() product!: { ean: string; name: string; price: number };
}
```

### Search Results with Query Parameters

```typescript
import { Component, inject } from '@angular/core';
import { ProductRouterLinkDirective } from '@isa/shared/product-router-link';
import { Router } from '@angular/router';

@Component({
  selector: 'app-search-results',
  standalone: true,
  imports: [ProductRouterLinkDirective],
  template: `
    <div class="search-results">
      @for (result of searchResults; track result.ean) {
        <div
          sharedProductRouterLink
          [ean]="result.ean"
          [queryParams]="{ source: 'search', q: searchQuery }"
          class="result-item"
        >
          <h4>{{ result.name }}</h4>
          <p>{{ result.description }}</p>
        </div>
      }
    </div>
  `
})
export class SearchResultsComponent {
  searchResults = [
    { ean: '1234567890123', name: 'Product 1', description: 'Description 1' },
    { ean: '2345678901234', name: 'Product 2', description: 'Description 2' }
  ];

  searchQuery = 'laptop';
}
```

### Table Row Navigation

```typescript
import { Component } from '@angular/core';
import { ProductRouterLinkDirective } from '@isa/shared/product-router-link';

@Component({
  selector: 'app-product-table',
  standalone: true,
  imports: [ProductRouterLinkDirective],
  template: `
    <table class="w-full">
      <thead>
        <tr>
          <th>EAN</th>
          <th>Name</th>
          <th>Stock</th>
          <th>Price</th>
        </tr>
      </thead>
      <tbody>
        @for (product of products; track product.ean) {
          <tr
            sharedProductRouterLink
            [ean]="product.ean"
            class="hover:bg-gray-100"
          >
            <td>{{ product.ean }}</td>
            <td>{{ product.name }}</td>
            <td>{{ product.stock }}</td>
            <td>{{ product.price | currency }}</td>
          </tr>
        }
      </tbody>
    </table>
  `
})
export class ProductTableComponent {
  products = [
    { ean: '1234567890123', name: 'Laptop', stock: 5, price: 999.99 },
    { ean: '2345678901234', name: 'Mouse', stock: 50, price: 29.99 },
    { ean: '3456789012345', name: 'Keyboard', stock: 30, price: 79.99 }
  ];
}
```

### Navigation with State

```typescript
import { Component } from '@angular/core';
import { ProductRouterLinkDirective } from '@isa/shared/product-router-link';

@Component({
  selector: 'app-cart-items',
  standalone: true,
  imports: [ProductRouterLinkDirective],
  template: `
    <div class="cart-items">
      @for (item of cartItems; track item.ean) {
        <div
          sharedProductRouterLink
          [ean]="item.ean"
          [state]="{ returnUrl: '/cart', quantity: item.quantity }"
          class="cart-item"
        >
          <h4>{{ item.name }}</h4>
          <p>Quantity: {{ item.quantity }}</p>
          <p>Click to view details</p>
        </div>
      }
    </div>
  `
})
export class CartItemsComponent {
  cartItems = [
    { ean: '1234567890123', name: 'Product 1', quantity: 2 },
    { ean: '2345678901234', name: 'Product 2', quantity: 1 }
  ];
}
```

### Dynamic EAN with Signal

```typescript
import { Component, signal } from '@angular/core';
import { ProductRouterLinkDirective } from '@isa/shared/product-router-link';

@Component({
  selector: 'app-related-products',
  standalone: true,
  imports: [ProductRouterLinkDirective],
  template: `
    <div class="related-products">
      <h3>Related Products</h3>
      @for (relatedEan of relatedEans; track relatedEan) {
        <div
          sharedProductRouterLink
          [ean]="relatedEan"
          class="related-item"
        >
          View Product {{ relatedEan }}
        </div>
      }
    </div>

    <button (click)="loadMoreRelated()">
      Load More
    </button>
  `
})
export class RelatedProductsComponent {
  relatedEans = ['1234567890123', '2345678901234'];

  loadMoreRelated(): void {
    this.relatedEans = [
      ...this.relatedEans,
      '3456789012345',
      '4567890123456'
    ];
  }
}
```

## Configuration

### Application-Level Configuration

Configure a custom URL builder at the application level:

```typescript
// In app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideProductRouterLinkBuilder } from '@isa/shared/product-router-link';

export const appConfig: ApplicationConfig = {
  providers: [
    provideProductRouterLinkBuilder((ean: string) => {
      // Simple product detail route
      return `/products/${ean}`;
    })
  ]
};
```

### Feature-Specific Configuration

Provide different builders for specific features or modules:

```typescript
import { Component } from '@angular/core';
import { provideProductRouterLinkBuilder } from '@isa/shared/product-router-link';

@Component({
  selector: 'app-admin-products',
  standalone: true,
  providers: [
    // Admin-specific URL builder
    provideProductRouterLinkBuilder((ean) => {
      return `/admin/catalog/products/${ean}/edit`;
    })
  ],
  template: `...`
})
export class AdminProductsComponent {}
```

### Category-Based URL Builder

Build URLs that include category information:

```typescript
import { inject } from '@angular/core';
import { provideProductRouterLinkBuilder } from '@isa/shared/product-router-link';
import { CategoryService } from './category.service';

export const appConfig: ApplicationConfig = {
  providers: [
    provideProductRouterLinkBuilder(async (ean: string) => {
      const categoryService = inject(CategoryService);
      const category = await categoryService.getCategoryByEan(ean);
      return `/shop/${category.slug}/products/${ean}`;
    })
  ]
};
```

### Multi-Tenant URL Builder

Generate tenant-specific product URLs:

```typescript
import { inject } from '@angular/core';
import { provideProductRouterLinkBuilder } from '@isa/shared/product-router-link';
import { TenantService } from './tenant.service';

export const appConfig: ApplicationConfig = {
  providers: [
    provideProductRouterLinkBuilder((ean: string) => {
      const tenantService = inject(TenantService);
      const tenantId = tenantService.getCurrentTenantId();
      return `/tenant/${tenantId}/products/${ean}`;
    })
  ]
};
```

### Testing Configuration

Override builder for testing:

```typescript
import { TestBed } from '@angular/core/testing';
import { provideProductRouterLinkBuilder } from '@isa/shared/product-router-link';

describe('ProductListComponent', () => {
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [ProductListComponent],
      providers: [
        // Use simple URLs for testing
        provideProductRouterLinkBuilder((ean) => `/test-products/${ean}`)
      ]
    }).compileComponents();
  });

  it('should navigate to correct URL', () => {
    // Test navigation with predictable URLs
  });
});
```

## Testing

The library uses **Jest** for testing.

### Running Tests

```bash
# Run tests for this library
npx nx test shared-product-router-link --skip-nx-cache

# Run tests with coverage
npx nx test shared-product-router-link --code-coverage --skip-nx-cache

# Run tests in watch mode
npx nx test shared-product-router-link --watch
```

### Test Examples

#### Testing the Directive

```typescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { ProductRouterLinkDirective, provideProductRouterLinkBuilder } from '@isa/shared/product-router-link';

@Component({
  standalone: true,
  imports: [ProductRouterLinkDirective],
  template: `
    <div sharedProductRouterLink [ean]="ean">Navigate</div>
  `
})
class TestComponent {
  ean = '1234567890123';
}

describe('ProductRouterLinkDirective', () => {
  let fixture: ComponentFixture<TestComponent>;
  let router: Router;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [TestComponent],
      providers: [
        provideProductRouterLinkBuilder((ean) => `/products/${ean}`)
      ]
    }).compileComponents();

    fixture = TestBed.createComponent(TestComponent);
    router = TestBed.inject(Router);
    fixture.detectChanges();
  });

  it('should have cursor-pointer class', () => {
    const element = fixture.nativeElement.querySelector('[sharedProductRouterLink]');
    expect(element.classList.contains('cursor-pointer')).toBe(true);
  });

  it('should generate correct router link', async () => {
    const directive = fixture.debugElement.query(
      By.directive(ProductRouterLinkDirective)
    ).componentInstance;

    // Wait for effect to process
    await fixture.whenStable();

    expect(directive.routerLink).toBe('/products/1234567890123');
  });
});
```

#### Testing URL Builder

```typescript
import { ProductRouterLinkBuilder } from '@isa/shared/product-router-link';

describe('Custom URL Builder', () => {
  it('should generate category-based URLs', async () => {
    const builder: ProductRouterLinkBuilder = (ean) => {
      // Mock category lookup
      const categoryMap: Record<string, string> = {
        '1234567890123': 'electronics',
        '2345678901234': 'books'
      };
      const category = categoryMap[ean] || 'general';
      return `/category/${category}/product/${ean}`;
    };

    const url1 = await builder('1234567890123');
    const url2 = await builder('2345678901234');

    expect(url1).toBe('/category/electronics/product/1234567890123');
    expect(url2).toBe('/category/books/product/2345678901234');
  });

  it('should handle async builders', async () => {
    const builder: ProductRouterLinkBuilder = async (ean) => {
      await new Promise(resolve => setTimeout(resolve, 10));
      return `/products/${ean}`;
    };

    const url = await builder('1234567890123');
    expect(url).toBe('/products/1234567890123');
  });
});
```

#### Integration Testing

```typescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { Location } from '@angular/common';
import { ProductListComponent } from './product-list.component';
import { provideProductRouterLinkBuilder } from '@isa/shared/product-router-link';

describe('ProductListComponent Navigation', () => {
  let fixture: ComponentFixture<ProductListComponent>;
  let router: Router;
  let location: Location;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [ProductListComponent],
      providers: [
        provideRouter([
          { path: 'products/:ean', component: ProductDetailComponent }
        ]),
        provideProductRouterLinkBuilder((ean) => `/products/${ean}`)
      ]
    }).compileComponents();

    fixture = TestBed.createComponent(ProductListComponent);
    router = TestBed.inject(Router);
    location = TestBed.inject(Location);
    fixture.detectChanges();
  });

  it('should navigate to product detail when clicked', async () => {
    const productLink = fixture.nativeElement.querySelector('[sharedProductRouterLink]');

    productLink.click();
    await fixture.whenStable();

    expect(location.path()).toBe('/products/1234567890123');
  });
});
```

## Architecture Notes

### Design Patterns

#### Strategy Pattern
The library implements the Strategy pattern for URL generation:

```typescript
// Strategy interface
type ProductRouterLinkBuilder = (ean: string) => PromiseLike<string> | string;

// Concrete strategies
const defaultStrategy: ProductRouterLinkBuilder = (ean) =>
  `/kunde/${Date.now()}/product/details/${ean}/ean`;

const simpleStrategy: ProductRouterLinkBuilder = (ean) =>
  `/products/${ean}`;

const categoryStrategy: ProductRouterLinkBuilder = async (ean) => {
  const category = await fetchCategory(ean);
  return `/category/${category}/product/${ean}`;
};
```

This enables:
- **Flexibility**: Easy to change URL generation logic
- **Testability**: Each strategy can be tested in isolation
- **Reusability**: Same directive works with different strategies

#### Directive Extension Pattern
Extends Angular's `RouterLink` instead of reimplementing:

```typescript
export class ProductRouterLinkDirective extends RouterLink {
  // Inherits all RouterLink functionality
  // Only adds EAN-specific behavior
}
```

**Benefits:**
- Reuses battle-tested Angular router logic
- Inherits all RouterLink features (query params, state, etc.)
- Minimal code surface for bugs

### Effect-Based URL Resolution

Uses Angular effects for reactive URL updates:

```typescript
urlEffect = effect(async () => {
  const ean = this.ean();
  if (!ean) return;
  const url = await this.#builder(ean);
  this.routerLink = url;
});
```

**Why Effects?**
- Automatically runs when EAN signal changes
- Handles both sync and async builders
- Cleans up automatically with directive lifecycle

### Dependency Injection Strategy

Uses injection token with default factory:

```typescript
export const PRODUCT_ROUTER_LINK_BUILDER =
  new InjectionToken<ProductRouterLinkBuilder>('PRODUCT_ROUTER_LINK_BUILDER', {
    factory: () => (ean: string) =>
      `/kunde/${Date.now()}/product/details/${ean}/ean`,
  });
```

This provides:
- **Default behavior**: Works out of the box
- **Override capability**: Easy to customize per application
- **Tree-shakeable**: Unused code can be removed

### Host Class Binding

Automatically adds cursor styling:

```typescript
@Directive({
  selector: '[sharedProductRouterLink]',
  host: {
    class: 'cursor-pointer',
  },
})
```

This improves UX without requiring consumers to remember CSS classes.

### Type Safety

TypeScript interface ensures builder correctness:

```typescript
export type ProductRouterLinkBuilder = (
  ean: string,
) => PromiseLike<string> | string;
```

Prevents runtime errors from invalid builder implementations.

## Dependencies

### Required Libraries

- `@angular/core` - Angular framework
- `@angular/router` - Angular router for RouterLink extension

### Path Alias

Import from: `@isa/shared/product-router-link`

### Peer Dependencies

- Angular Router must be configured in the application
- Router providers must be included in application config

## Best Practices

### 1. Consistent URL Structure

Maintain consistent URL patterns across your application:

```typescript
// Good: Consistent structure
provideProductRouterLinkBuilder((ean) => `/products/${ean}`)

// Avoid: Inconsistent patterns
// Some links go to /product/:ean, others to /item/:ean
```

### 2. SEO-Friendly URLs

Include product slugs for better SEO:

```typescript
provideProductRouterLinkBuilder(async (ean) => {
  const product = await fetchProduct(ean);
  const slug = product.name.toLowerCase().replace(/\s+/g, '-');
  return `/products/${slug}/${ean}`;
});
// Result: /products/wireless-mouse/1234567890123
```

### 3. Error Handling

Handle missing products gracefully:

```typescript
provideProductRouterLinkBuilder(async (ean) => {
  try {
    const product = await fetchProduct(ean);
    return `/products/${product.id}`;
  } catch (error) {
    console.error(`Product not found: ${ean}`);
    return `/products/not-found?ean=${ean}`;
  }
});
```

### 4. Caching

Cache URL generation for frequently accessed products:

```typescript
const urlCache = new Map<string, string>();

provideProductRouterLinkBuilder(async (ean) => {
  if (urlCache.has(ean)) {
    return urlCache.get(ean)!;
  }

  const url = await generateUrl(ean);
  urlCache.set(ean, url);
  return url;
});
```

### 5. Analytics Tracking

Include tracking parameters in URLs:

```typescript
provideProductRouterLinkBuilder((ean) => {
  const source = getTrackingSource(); // e.g., 'search', 'recommendation'
  return `/products/${ean}?utm_source=${source}`;
});
```

## Performance Considerations

### Async Builder Performance

- Async builders run on every EAN change
- Consider caching for frequently viewed products
- Avoid expensive operations in builder functions

### Effect Cleanup

- Effects automatically clean up when directive is destroyed
- No manual subscription management needed
- Guards against setting `routerLink` after destruction

### Bundle Size

- Library is small (~2KB minified)
- Extends RouterLink, no duplicate router logic
- Tree-shakeable with proper build configuration

## Common Use Cases

### E-commerce Product Navigation
```typescript
provideProductRouterLinkBuilder((ean) => `/shop/products/${ean}`)
```

### Admin Panel
```typescript
provideProductRouterLinkBuilder((ean) => `/admin/inventory/${ean}`)
```

### Multi-Language Sites
```typescript
provideProductRouterLinkBuilder((ean) => {
  const lang = getCurrentLanguage();
  return `/${lang}/products/${ean}`;
})
```

### Marketplace with Vendors
```typescript
provideProductRouterLinkBuilder(async (ean) => {
  const product = await fetchProduct(ean);
  return `/vendors/${product.vendorId}/products/${ean}`;
})
```

## License

Internal ISA Frontend library - not for external distribution.
