Files
ISA-Frontend/libs/ui/progress-bar
Lorenz Hilpert 2b5da00249 feat(checkout): add reward order confirmation feature with schema migrations
- 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
2025-10-21 14:28:52 +02:00
..

@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 applied
  • ui-progress-bar__determinate or ui-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.