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!