- 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
12 KiB
@isa/ui/progress-bar
A lightweight Angular progress bar component supporting both determinate and indeterminate modes.
Overview
The Progress Bar library provides a simple, performant component for visualizing progress or loading states. It supports two modes: determinate (with specific progress value) and indeterminate (continuous loading animation), making it suitable for various use cases from file uploads to background processing indicators.
Table of Contents
Features
- Two display modes - Determinate (percentage-based) and indeterminate (continuous animation)
- Customizable progress - Configurable value and max value for flexible percentage calculations
- Standalone component - Fully standalone, no module imports required
- Signal-based inputs - Reactive updates using Angular signals
- Computed width - Automatically calculates bar width based on value/maxValue ratio
- OnPush change detection - Optimized for performance
- ViewEncapsulation.None - Allows global styling customization
Quick Start
1. Import the Component
import { Component, signal } from '@angular/core';
import { ProgressBarComponent, ProgressBarMode } from '@isa/ui/progress-bar';
@Component({
selector: 'app-upload',
standalone: true,
imports: [ProgressBarComponent],
template: '...'
})
export class UploadComponent {
uploadProgress = signal(0);
}
2. Determinate Mode (Default)
<!-- Show specific progress percentage -->
<ui-progress-bar
[value]="uploadProgress()"
[maxValue]="100"
></ui-progress-bar>
3. Indeterminate Mode
<!-- Show continuous loading animation -->
<ui-progress-bar
[mode]="'indeterminate'"
></ui-progress-bar>
4. Custom Value Range
<!-- Progress out of custom max value -->
<ui-progress-bar
[value]="processedItems()"
[maxValue]="totalItems()"
></ui-progress-bar>
API Reference
ProgressBarComponent
Standalone component that displays a visual progress indicator.
Selector: ui-progress-bar
Inputs
| Input | Type | Default | Description |
|---|---|---|---|
mode |
ProgressBarMode |
'determinate' |
Display mode: 'determinate' or 'indeterminate' |
value |
number |
50 |
Current progress value (used in determinate mode) |
maxValue |
number |
100 |
Maximum value for percentage calculation |
ProgressBarMode Type
export const ProgressBarMode = {
Determinate: 'determinate', // Percentage-based progress
Indeterminate: 'indeterminate' // Continuous loading animation
} as const;
export type ProgressBarMode =
(typeof ProgressBarMode)[keyof typeof ProgressBarMode];
Computed Properties
modeClass(): string
Returns the CSS class for the current mode:
'ui-progress-bar__determinate'- For determinate mode'ui-progress-bar__indeterminate'- For indeterminate mode
width(): string
Calculates the progress bar width:
- Indeterminate mode: Returns
'100%' - Determinate mode: Returns
(value / maxValue * 100)%
Host Classes
ui-progress-bar- Always appliedui-progress-bar__determinateorui-progress-bar__indeterminate- Based on mode
Template Structure
<div class="ui-progress-bar__bar" [style.width]="width()"></div>
Usage Examples
File Upload Progress
import { Component, signal } from '@angular/core';
import { ProgressBarComponent } from '@isa/ui/progress-bar';
@Component({
selector: 'app-file-upload',
standalone: true,
imports: [ProgressBarComponent],
template: `
<div class="upload-container">
<h3>Uploading {{ fileName() }}</h3>
<ui-progress-bar
[value]="uploadProgress()"
[maxValue]="100"
data-what="upload-progress-bar"
></ui-progress-bar>
<p>{{ uploadProgress() }}% complete</p>
</div>
`
})
export class FileUploadComponent {
fileName = signal('document.pdf');
uploadProgress = signal(0);
async uploadFile(file: File): Promise<void> {
this.fileName.set(file.name);
this.uploadProgress.set(0);
// Simulate upload progress
const interval = setInterval(() => {
this.uploadProgress.update(p => {
if (p >= 100) {
clearInterval(interval);
return 100;
}
return p + 10;
});
}, 500);
}
}
Loading Indicator
import { Component, signal } from '@angular/core';
import { ProgressBarComponent, ProgressBarMode } from '@isa/ui/progress-bar';
@Component({
selector: 'app-data-loader',
standalone: true,
imports: [ProgressBarComponent],
template: `
@if (isLoading()) {
<ui-progress-bar
[mode]="'indeterminate'"
data-what="loading-indicator"
></ui-progress-bar>
}
<div class="content">
@for (item of data(); track item.id) {
<div>{{ item.name }}</div>
}
</div>
`
})
export class DataLoaderComponent {
isLoading = signal(true);
data = signal<DataItem[]>([]);
async loadData(): Promise<void> {
this.isLoading.set(true);
try {
const result = await this.dataService.fetch();
this.data.set(result);
} finally {
this.isLoading.set(false);
}
}
}
Batch Processing Progress
import { Component, computed, signal } from '@angular/core';
import { ProgressBarComponent } from '@isa/ui/progress-bar';
@Component({
selector: 'app-batch-processor',
standalone: true,
imports: [ProgressBarComponent],
template: `
<div class="batch-progress">
<h3>Processing Items</h3>
<ui-progress-bar
[value]="processedCount()"
[maxValue]="totalCount()"
></ui-progress-bar>
<p>
{{ processedCount() }} of {{ totalCount() }} items processed
({{ percentComplete() }}%)
</p>
</div>
`
})
export class BatchProcessorComponent {
processedCount = signal(0);
totalCount = signal(100);
percentComplete = computed(() => {
const total = this.totalCount();
if (total === 0) return 0;
return Math.round((this.processedCount() / total) * 100);
});
async processItems(items: Item[]): Promise<void> {
this.totalCount.set(items.length);
this.processedCount.set(0);
for (const item of items) {
await this.processItem(item);
this.processedCount.update(c => c + 1);
}
}
}
Multi-Step Progress
import { Component, computed, signal } from '@angular/core';
import { ProgressBarComponent } from '@isa/ui/progress-bar';
@Component({
selector: 'app-wizard',
standalone: true,
imports: [ProgressBarComponent],
template: `
<div class="wizard">
<ui-progress-bar
[value]="currentStep()"
[maxValue]="totalSteps()"
></ui-progress-bar>
<div class="step-indicator">
Step {{ currentStep() }} of {{ totalSteps() }}
</div>
<div class="step-content">
@switch (currentStep()) {
@case (1) {
<div>Step 1: Basic Information</div>
}
@case (2) {
<div>Step 2: Address Details</div>
}
@case (3) {
<div>Step 3: Payment Method</div>
}
@case (4) {
<div>Step 4: Review & Confirm</div>
}
}
</div>
<div class="actions">
<button
[disabled]="currentStep() === 1"
(click)="previousStep()"
>
Previous
</button>
<button
[disabled]="currentStep() === totalSteps()"
(click)="nextStep()"
>
Next
</button>
</div>
</div>
`
})
export class WizardComponent {
currentStep = signal(1);
totalSteps = signal(4);
nextStep(): void {
this.currentStep.update(s => Math.min(s + 1, this.totalSteps()));
}
previousStep(): void {
this.currentStep.update(s => Math.max(s - 1, 1));
}
}
Styling and Customization
Default Classes
<!-- Determinate mode -->
<ui-progress-bar class="ui-progress-bar ui-progress-bar__determinate">
<div class="ui-progress-bar__bar" style="width: 50%;"></div>
</ui-progress-bar>
<!-- Indeterminate mode -->
<ui-progress-bar class="ui-progress-bar ui-progress-bar__indeterminate">
<div class="ui-progress-bar__bar" style="width: 100%;"></div>
</ui-progress-bar>
Custom Styling
// Base progress bar container
.ui-progress-bar {
width: 100%;
height: 4px;
background-color: #e0e0e0;
border-radius: 2px;
overflow: hidden;
}
// The moving/filling bar
.ui-progress-bar__bar {
height: 100%;
background-color: #2196f3;
transition: width 0.3s ease-in-out;
}
// Determinate mode specific styles
.ui-progress-bar__determinate .ui-progress-bar__bar {
background: linear-gradient(90deg, #1976d2 0%, #2196f3 100%);
}
// Indeterminate mode animation
.ui-progress-bar__indeterminate .ui-progress-bar__bar {
animation: indeterminate-progress 2s linear infinite;
background: linear-gradient(
90deg,
transparent 0%,
#2196f3 50%,
transparent 100%
);
}
@keyframes indeterminate-progress {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
// Color variants
.ui-progress-bar--success .ui-progress-bar__bar {
background-color: #4caf50;
}
.ui-progress-bar--warning .ui-progress-bar__bar {
background-color: #ff9800;
}
.ui-progress-bar--error .ui-progress-bar__bar {
background-color: #f44336;
}
Testing
The library uses Jest for testing.
Running Tests
# Run tests for progress-bar
npx nx test ui-progress-bar --skip-nx-cache
# Run tests with coverage
npx nx test ui-progress-bar --code-coverage --skip-nx-cache
# Run tests in watch mode
npx nx test ui-progress-bar --watch
Test Coverage
Tests should cover:
- Mode switching - Determinate and indeterminate modes
- Width calculation - Correct percentage calculation from value/maxValue
- Boundary conditions - 0%, 100%, values exceeding maxValue
- Signal reactivity - Updates when value/maxValue change
- Class application - Correct mode classes applied
Dependencies
Required Libraries
@angular/core- Angular framework (v20.1.2)
Path Alias
Import from: @isa/ui/progress-bar
Peer Dependencies
- Angular 20.1.2 or higher
- TypeScript 5.8.3 or higher
Best Practices
1. Use Appropriate Mode
Choose the mode based on whether progress is measurable:
<!-- Measurable progress - use determinate -->
<ui-progress-bar [value]="uploadedBytes" [maxValue]="totalBytes"></ui-progress-bar>
<!-- Unknown duration - use indeterminate -->
<ui-progress-bar [mode]="'indeterminate'"></ui-progress-bar>
2. Provide Context
Always accompany the progress bar with text:
<div class="progress-container">
<label>Uploading...</label>
<ui-progress-bar [value]="progress" [maxValue]="100"></ui-progress-bar>
<span>{{ progress }}%</span>
</div>
3. Handle Edge Cases
Validate values to prevent division by zero or negative percentages:
percentComplete = computed(() => {
const max = this.maxValue();
const val = this.value();
if (max === 0) return 0;
return Math.min(Math.max((val / max) * 100, 0), 100);
});
4. Smooth Updates
For smoother visual transitions, update progress in reasonable increments:
// Good: Update every 5-10%
updateProgress(newValue: number) {
if (newValue - this.progress() >= 5) {
this.progress.set(newValue);
}
}
// Bad: Update every tiny increment
updateProgress(newValue: number) {
this.progress.set(newValue); // Updates too frequently
}
License
Internal ISA Frontend library - not for external distribution.