mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
1. Reward Shopping Cart Implementation - New shopping cart with quantity control and availability checking - Responsive shopping cart item component with improved CSS styling - Shipping address integration in cart - Customer reward card and billing/shipping address components 2. Navigation State Management Library (@isa/core/navigation) - New library with type-safe navigation context service (373 lines) - Navigation state service (287 lines) for temporary state between routes - Comprehensive test coverage (668 + 227 lines of tests) - Documentation (792 lines in README.md) - Replaces query parameters for passing temporary navigation context 3. CRM Shipping Address Services - New ShippingAddressService with fetching and validation - CustomerShippingAddressResource and CustomerShippingAddressesResource - Zod schemas for data validation 4. Additional Improvements - Enhanced searchbox accessibility with ARIA support - Availability data access rework for better fetching/mapping - Storybook tooltip variant support - Vitest JUnit and Cobertura reporting configuration Related work items: #5382, #5383, #5384
357 lines
12 KiB
Markdown
357 lines
12 KiB
Markdown
# State Management
|
|
|
|
## Local State
|
|
|
|
- Use Signals for component-level state
|
|
- Keep state close to where it's used
|
|
- Document state management decisions
|
|
|
|
## Global State (NgRx)
|
|
|
|
- Use for complex application state
|
|
- Follow feature-based store organization
|
|
- Implement proper error handling
|
|
|
|
## Navigation State
|
|
|
|
Navigation state refers to temporary data preserved between routes during navigation flows. Unlike global state or local component state, navigation state is transient and tied to a specific navigation flow within a tab.
|
|
|
|
### Storage Architecture
|
|
|
|
Navigation contexts are stored in **tab metadata** using `@isa/core/navigation`:
|
|
|
|
```typescript
|
|
// Context stored in tab metadata:
|
|
tab.metadata['navigation-contexts'] = {
|
|
'default': {
|
|
data: { returnUrl: '/cart', customerId: 123 },
|
|
createdAt: 1234567890
|
|
},
|
|
'customer-flow': {
|
|
data: { step: 2, selectedOptions: ['A', 'B'] },
|
|
createdAt: 1234567895
|
|
}
|
|
}
|
|
```
|
|
|
|
**Benefits:**
|
|
- ✅ **Automatic cleanup** when tabs close (no manual cleanup needed)
|
|
- ✅ **Tab isolation** (contexts don't leak between tabs)
|
|
- ✅ **Persistence** across page refresh (via TabService UserStorage)
|
|
- ✅ **Integration** with tab lifecycle management
|
|
|
|
### When to Use Navigation State
|
|
|
|
Use `@isa/core/navigation` for:
|
|
|
|
- **Return URLs**: Storing the previous route to navigate back to
|
|
- **Wizard/Multi-step Forms**: Passing context between wizard steps
|
|
- **Context Preservation**: Maintaining search queries, filters, or selections when drilling into details
|
|
- **Temporary Data**: Any data needed only for the current navigation flow within a tab
|
|
|
|
### When NOT to Use Navigation State
|
|
|
|
Avoid using navigation state for:
|
|
|
|
- **Persistent Data**: Use NgRx stores or services instead
|
|
- **Shareable URLs**: Use route parameters or query parameters if the URL should be bookmarkable
|
|
- **Long-lived State**: Use session storage or NgRx with persistence
|
|
- **Cross-tab Communication**: Use services with proper state management
|
|
|
|
### Best Practices
|
|
|
|
#### ✅ Do Use Navigation Context (Tab Metadata)
|
|
|
|
```typescript
|
|
// Good: Clean URLs, automatic cleanup, tab-scoped
|
|
navState.preserveContext({
|
|
returnUrl: '/customer-list',
|
|
searchQuery: 'John Doe',
|
|
context: 'reward-selection'
|
|
});
|
|
|
|
await router.navigate(['/customer', customerId]);
|
|
|
|
// Later (after intermediate navigations):
|
|
const context = navState.restoreAndClearContext<{ returnUrl: string }>();
|
|
await router.navigateByUrl(context.returnUrl);
|
|
```
|
|
|
|
#### ❌ Don't Use Query Parameters for Temporary State
|
|
|
|
```typescript
|
|
// Bad: URL pollution, can be overwritten, visible in browser bar
|
|
await router.navigate(['/customer', customerId], {
|
|
queryParams: {
|
|
returnUrl: '/customer-list',
|
|
searchQuery: 'John Doe'
|
|
}
|
|
});
|
|
// URL becomes: /customer/123?returnUrl=%2Fcustomer-list&searchQuery=John%20Doe
|
|
```
|
|
|
|
#### ❌ Don't Use Router State for Multi-Step Flows
|
|
|
|
```typescript
|
|
// Bad: Lost after intermediate navigations
|
|
await router.navigate(['/customer/search'], {
|
|
state: { returnUrl: '/reward/cart' } // Lost after next navigation!
|
|
});
|
|
```
|
|
|
|
### Integration with TabService
|
|
|
|
Navigation context relies on `TabService` for automatic tab scoping:
|
|
|
|
```typescript
|
|
// Context automatically scoped to active tab
|
|
const tabId = tabService.activatedTabId(); // e.g., 123
|
|
|
|
// When you preserve context:
|
|
navState.preserveContext({ returnUrl: '/cart' });
|
|
// Stored in: tab[123].metadata['navigation-contexts']['default']
|
|
|
|
// When you preserve with custom scope:
|
|
navState.preserveContext({ step: 2 }, 'wizard-flow');
|
|
// Stored in: tab[123].metadata['navigation-contexts']['wizard-flow']
|
|
```
|
|
|
|
**Automatic Cleanup:**
|
|
When the tab closes, all contexts stored in that tab's metadata are automatically removed. No manual cleanup required!
|
|
|
|
### Usage Example: Multi-Step Flow
|
|
|
|
**Start of Flow (preserving context):**
|
|
```typescript
|
|
import { inject } from '@angular/core';
|
|
import { Router } from '@angular/router';
|
|
import { NavigationStateService } from '@isa/core/navigation';
|
|
|
|
export class CustomerListComponent {
|
|
private router = inject(Router);
|
|
private navState = inject(NavigationStateService);
|
|
|
|
async viewCustomerDetails(customerId: number) {
|
|
// Preserve context before navigating
|
|
this.navState.preserveContext({
|
|
returnUrl: this.router.url,
|
|
searchQuery: this.searchForm.value.query
|
|
});
|
|
|
|
await this.router.navigate(['/customer', customerId]);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Intermediate Navigation (context persists):**
|
|
```typescript
|
|
export class CustomerDetailsComponent {
|
|
private router = inject(Router);
|
|
|
|
async editAddress(addressId: number) {
|
|
// Context still preserved through intermediate navigations
|
|
await this.router.navigate(['/address/edit', addressId]);
|
|
}
|
|
}
|
|
```
|
|
|
|
**End of Flow (restoring context):**
|
|
```typescript
|
|
import { inject } from '@angular/core';
|
|
import { Router } from '@angular/router';
|
|
import { NavigationStateService } from '@isa/core/navigation';
|
|
|
|
interface CustomerNavigationContext {
|
|
returnUrl: string;
|
|
searchQuery?: string;
|
|
}
|
|
|
|
export class AddressEditComponent {
|
|
private router = inject(Router);
|
|
private navState = inject(NavigationStateService);
|
|
|
|
async complete() {
|
|
// Restore and clear context
|
|
const context = this.navState.restoreAndClearContext<CustomerNavigationContext>();
|
|
|
|
if (context?.returnUrl) {
|
|
await this.router.navigateByUrl(context.returnUrl);
|
|
} else {
|
|
// Fallback navigation
|
|
await this.router.navigate(['/customers']);
|
|
}
|
|
}
|
|
|
|
// For template usage
|
|
hasReturnUrl(): boolean {
|
|
return this.navState.hasPreservedContext();
|
|
}
|
|
}
|
|
```
|
|
|
|
**Template:**
|
|
```html
|
|
@if (hasReturnUrl()) {
|
|
<button (click)="complete()">Zurück</button>
|
|
}
|
|
```
|
|
|
|
### Advanced: Multiple Concurrent Flows
|
|
|
|
Use custom scopes to manage multiple flows in the same tab:
|
|
|
|
```typescript
|
|
export class DashboardComponent {
|
|
private navState = inject(NavigationStateService);
|
|
private router = inject(Router);
|
|
|
|
async startCustomerFlow() {
|
|
// Save context for customer flow
|
|
this.navState.preserveContext(
|
|
{ returnUrl: '/dashboard', flowType: 'customer' },
|
|
'customer-flow'
|
|
);
|
|
|
|
await this.router.navigate(['/customer/search']);
|
|
}
|
|
|
|
async startProductFlow() {
|
|
// Save context for product flow (different scope)
|
|
this.navState.preserveContext(
|
|
{ returnUrl: '/dashboard', flowType: 'product' },
|
|
'product-flow'
|
|
);
|
|
|
|
await this.router.navigate(['/product/search']);
|
|
}
|
|
|
|
async completeCustomerFlow() {
|
|
// Restore customer flow context
|
|
const context = this.navState.restoreAndClearContext('customer-flow');
|
|
if (context?.returnUrl) {
|
|
await this.router.navigateByUrl(context.returnUrl);
|
|
}
|
|
}
|
|
|
|
async completeProductFlow() {
|
|
// Restore product flow context
|
|
const context = this.navState.restoreAndClearContext('product-flow');
|
|
if (context?.returnUrl) {
|
|
await this.router.navigateByUrl(context.returnUrl);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Architecture Decision
|
|
|
|
**Why Tab Metadata instead of SessionStorage?**
|
|
|
|
Tab metadata provides several advantages over SessionStorage:
|
|
|
|
- **Automatic Cleanup**: Contexts are automatically removed when tabs close (no manual cleanup or TTL management needed)
|
|
- **Better Integration**: Seamlessly integrated with tab lifecycle and TabService
|
|
- **Tab Isolation**: Impossible to leak contexts between tabs (scoped by tab ID)
|
|
- **Simpler Mental Model**: Contexts are "owned" by tabs, not stored globally
|
|
- **Persistence**: Tab metadata persists across page refresh via UserStorage
|
|
|
|
**Why not Query Parameters?**
|
|
|
|
Query parameters were traditionally used for passing state, but they have significant drawbacks:
|
|
|
|
- **URL Pollution**: Makes URLs long, ugly, and non-bookmarkable
|
|
- **Overwritable**: Intermediate navigations can overwrite query parameters
|
|
- **Security**: Sensitive data visible in browser URL bar
|
|
- **User Experience**: Affects URL sharing and bookmarking
|
|
|
|
**Why not Router State?**
|
|
|
|
Angular's Router state mechanism has limitations:
|
|
|
|
- **Lost After Navigation**: State is lost after the immediate navigation (doesn't survive intermediate navigations)
|
|
- **Not Persistent**: Doesn't survive page refresh
|
|
- **No Tab Scoping**: Can't isolate state by tab
|
|
|
|
**Why Tab Metadata is Better:**
|
|
|
|
Navigation context using tab metadata provides:
|
|
|
|
- **Survives Intermediate Navigations**: State persists across multiple navigation steps
|
|
- **Clean URLs**: No visible state in the URL bar
|
|
- **Reliable**: State survives page refresh (via TabService UserStorage)
|
|
- **Type-safe**: Full TypeScript support with generics
|
|
- **Platform-agnostic**: Works with SSR/Angular Universal
|
|
- **Automatic Cleanup**: No manual cleanup needed when tabs close
|
|
- **Tab Isolation**: Contexts automatically scoped to tabs
|
|
|
|
### Comparison with Other State Solutions
|
|
|
|
| Feature | Navigation Context | NgRx Store | Service State | Query Params | Router State |
|
|
|---------|-------------------|------------|---------------|--------------|--------------|
|
|
| **Scope** | Tab-scoped flow | Application-wide | Feature-specific | URL-based | Single navigation |
|
|
| **Persistence** | Until tab closes | Configurable | Component lifetime | URL lifetime | Lost after nav |
|
|
| **Survives Refresh** | ✅ Yes | ⚠️ Optional | ❌ No | ✅ Yes | ❌ No |
|
|
| **Survives Intermediate Navs** | ✅ Yes | ✅ Yes | ✅ Yes | ⚠️ Sometimes | ❌ No |
|
|
| **Automatic Cleanup** | ✅ Yes (tab close) | ❌ Manual | ❌ Manual | N/A | ✅ Yes |
|
|
| **Tab Isolation** | ✅ Yes | ❌ No | ❌ No | ❌ No | ❌ No |
|
|
| **Clean URLs** | ✅ Yes | N/A | N/A | ❌ No | ✅ Yes |
|
|
| **Shareability** | ❌ No | ❌ No | ❌ No | ✅ Yes | ❌ No |
|
|
| **Type Safety** | ✅ Yes | ✅ Yes | ✅ Yes | ⚠️ Limited | ✅ Yes |
|
|
| **Use Case** | Multi-step flow | Global state | Feature state | Bookmarkable state | Simple navigation |
|
|
|
|
### Best Practices for Navigation Context
|
|
|
|
#### ✅ Do
|
|
|
|
- **Use for temporary flow context** (return URLs, wizard state, search filters)
|
|
- **Use custom scopes** for multiple concurrent flows in the same tab
|
|
- **Always use type safety** with TypeScript generics
|
|
- **Trust automatic cleanup** - no need to manually clear contexts in `ngOnDestroy`
|
|
- **Check for null** when restoring contexts (they may not exist)
|
|
|
|
#### ❌ Don't
|
|
|
|
- **Don't store large objects** - keep contexts lean (URLs, IDs, simple flags)
|
|
- **Don't use for persistent data** - use NgRx or services for long-lived state
|
|
- **Don't store sensitive data** - contexts may be visible in browser dev tools
|
|
- **Don't manually clear in ngOnDestroy** - tab lifecycle handles cleanup automatically
|
|
- **Don't use for cross-tab communication** - use services or BroadcastChannel
|
|
|
|
### Cleanup Behavior
|
|
|
|
**Automatic Cleanup (Recommended):**
|
|
```typescript
|
|
// ✅ No manual cleanup needed - tab lifecycle handles it!
|
|
export class CustomerFlowComponent {
|
|
navState = inject(NavigationStateService);
|
|
|
|
async startFlow() {
|
|
this.navState.preserveContext({ returnUrl: '/home' });
|
|
// Context automatically cleaned up when tab closes
|
|
}
|
|
|
|
// No ngOnDestroy needed!
|
|
}
|
|
```
|
|
|
|
**Manual Cleanup (Rarely Needed):**
|
|
```typescript
|
|
// Use only if you need to explicitly clear contexts during tab lifecycle
|
|
export class ComplexFlowComponent {
|
|
navState = inject(NavigationStateService);
|
|
|
|
async cancelFlow() {
|
|
// Explicitly clear all contexts for this tab
|
|
const cleared = this.navState.clearScopeContexts();
|
|
console.log(`Cleared ${cleared} contexts`);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Related Documentation
|
|
|
|
- **Full API Reference**: See [libs/core/navigation/README.md](../../libs/core/navigation/README.md) for complete documentation
|
|
- **Usage Patterns**: Detailed examples and patterns in the library README
|
|
- **Testing Guide**: Full testing guide included in the library documentation
|
|
- **Migration Guide**: Instructions for migrating from SessionStorage approach in the library README
|