A production-ready, multi-step registration form demonstrating modern Angular architecture, clean code principles, and enterprise-level patterns.
This project implements a production-ready 4-step registration wizard built with Angular 20 and modern frontend architecture principles. It focuses on scalability, maintainability, and real-world use cases, featuring state persistence, robust form validation, internationalization, and a flexible, configuration-driven design.
- Dynamic Step Configuration - Add/remove/reorder steps through configuration
- State Persistence - Automatic IndexedDB save/restore with fallback
- Mock API Support - Complete mock services with realistic delays
- Form Validation - Real-time validation with custom validators
- File Upload - Drag & drop with preview and validation
- Internationalization - Persian/English support with ngx-translate
- Responsive Design - Mobile-first approach with TailwindCSS
- ControlValueAccessor Pattern - Reusable form components
- OnPush Change Detection - Optimized performance
- Signals & Computed - Modern reactive state management
- Dynamic Component Loading - Configuration-driven step rendering
- Error Handling - Centralized FormErrorService with i18n
- Navigation Guards - Smart step validation and navigation rules
- Node.js 18.x or higher
- npm 9.x or higher
# Clone repository
git clone https://github.com/yourusername/angular-registration-wizard.git
cd angular-registration-wizard
# Install dependencies
npm install
# Run with mock API (default)
ng serve
# Run with real API
# 1. Update src/environments/environment.ts
# 2. Set useMockData: false
# 3. Configure apiUrl
ng serve
# Open http://localhost:4200# Production build
ng build --configuration production
# Output in dist/ folder
# Deploy to Netlify, Vercel, or any static hostingProblem Solved: Adding/removing/reordering steps required changes across multiple files (enum, template @switch, service validations).
Solution: Single configuration file with dynamic component loading.
// StepRegistryService - Single source of truth
private readonly stepConfigs: StepConfig[] = [
{
id: 'personal-info',
order: 0,
title: 'STEPS.PERSONAL_INFO',
component: PersonalInfoComponent,
isRequired: true,
canNavigateBack: false
},
// Add new step - just add to array!
{
id: 'payment-info',
order: 2,
title: 'STEPS.PAYMENT',
component: PaymentInfoComponent,
isRequired: true,
canNavigateBack: true
}
];Benefits:
- No template modifications needed
- Type-safe with
StepConfiginterface - Easy to maintain and scale
- Single responsibility principle
All form controls implement Angular's ControlValueAccessor for seamless Reactive Forms integration:
<app-text-input
formControlName="firstName"
label="First Name"
[required]="true"
/>Features:
- Works seamlessly with Reactive Forms
- Automatic validation integration
- Consistent error display with i18n
- OnPush change detection optimized
- Reusable across projects
// Automatic save on every change
updatePersonalInfo() → Signal Update → Effect → IndexedDB
// Automatic restore on page load
APP_INITIALIZER → loadInitialState() → Restore from IndexedDBEdge Cases Handled:
- Browser doesn't support IndexedDB (graceful fallback)
- Quota exceeded errors (clear old data)
- Corrupted data recovery (reset to defaults)
- Race conditions (debounced saves)
Toggle Between Mock and Real:
// environment.ts
export const environment = {
production: false,
useMockData: true // false for production
};
// app.config.ts - Conditional provider
{
provide: LocationHttpService,
useClass: environment.useMockData
? LocationMockHttpService
: LocationHttpService
}Benefits:
- Zero backend dependency
- Realistic network delays (300ms simulated)
- Same interface as real API
- Easy testing and demos
| Validator | Implementation | Use Case |
|---|---|---|
| Persian Text | Custom regex /^[\u0600-\u06FF\s]+$/ |
Names, addresses |
| National ID | Checksum algorithm (Luhn) | Iranian 10-digit ID |
| No Whitespace | Custom validator | Prevents only spaces |
| File Type | MIME type check | JPG/PNG validation |
| File Size | Byte comparison | Max 5MB |
// Switch language dynamically
this.translate.use('en'); // or 'fa'
// In templates
{{ 'ERRORS.REQUIRED' | translate }}
{{ 'ERRORS.MINLENGTH' | translate: {requiredLength: 5} }}Structure:
public/i18n/
├── fa.json # Persian (default)
└── en.json # English
- Capacity: 50MB+ vs 5MB
- Structure: Stores objects directly (no JSON parsing)
- Performance: Async, non-blocking operations
- Future-proof: Can handle large files if needed
- Simpler: Easier to understand and maintain
- Performance: Fine-grained reactivity, better change detection
- Modern: Angular's recommended approach
- Note: RxJS still used for HTTP (appropriate use case)
- Consistency: All form controls work identically
- Reusability: Use across multiple forms/projects
- Integration: Built-in Reactive Forms support
- Type Safety: Full TypeScript integration
- Performance: 50-90% fewer change detection cycles
- Best Practice: Modern Angular default strategy
- Scalability: Critical for large applications
- Explicit: Forces intentional state updates
// 1. Create component
ng g c features/registration-stepper/components/payment-info
// 2. Implement IStepForm interface
export class PaymentInfoComponent implements IStepForm {
isValid(): boolean {
return this.form.valid;
}
}
// 3. Add to StepRegistryService
{
id: 'payment-info',
order: 2, // Insert at desired position
title: 'STEPS.PAYMENT',
component: PaymentInfoComponent,
isRequired: true,
canNavigateBack: true
}
// 4. Add translations
// public/i18n/fa.json
"STEPS": {
"PAYMENT": "اطلاعات پرداخت"
}
// public/i18n/en.json
"STEPS": {
"PAYMENT": "Payment Information"
}That's it! No template changes, no enum updates, no additional logic.
// src/app/core/validators/custom.validator.ts
export function emailDomainValidator(allowedDomain: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (!control.value) return null;
const email = control.value.toLowerCase();
const isValid = email.endsWith(`@${allowedDomain}`);
return isValid ? null : { emailDomain: { allowedDomain } };
};
}
// Add translation
// public/i18n/fa.json
"ERRORS": {
"EMAILDOMAIN": "ایمیل باید از دامنه {{allowedDomain}} باشد"
}
// Usage in form
this.form = this.fb.group({
email: ['', [Validators.email, emailDomainValidator('company.com')]]
});// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
primary: {
50: '#eff6ff',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
}
}
}
}
}Contributions are welcome! Please:
- Fork the project
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'feat: Add amazing feature') - Push to branch (
git push origin feature/amazing-feature) - Open a Pull Request
- Follow Angular Style Guide
- Use TypeScript strict mode
- Write meaningful commit messages
This project is licensed under the MIT License - see LICENSE file for details.
Mehdi Hadizadeh
- Email: mehdi.hadizadeh.k@gmail.com
- LinkedIn: Mehdi Hadizadeh
⭐ If you found this project helpful, please give it a star!