Skip to main content

Building Modern Angular Applications: From Components to Micro-frontends

· 6 min read
Shiaondo Orkuma
AI Engineer & Full Stack Developer @ Hash Dynamics

Angular has evolved significantly, offering powerful tools for building enterprise-scale applications. In this comprehensive guide, I'll walk you through modern Angular development practices, from component architecture to micro-frontend implementations.

Modern Angular Architecture

Component Design Patterns

Angular's component-based architecture promotes reusability and maintainability. Here's how to structure components effectively:

// Smart Component (Container)
@Component({
selector: 'app-user-dashboard',
template: `
<app-user-profile
[user]="user$ | async"
(userUpdated)="onUserUpdated($event)">
</app-user-profile>

<app-user-statistics
[statistics]="statistics$ | async">
</app-user-statistics>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserDashboardComponent implements OnInit {
user$ = this.store.select(selectCurrentUser);
statistics$ = this.store.select(selectUserStatistics);

constructor(
private store: Store<AppState>,
private userService: UserService
) {}

ngOnInit() {
this.store.dispatch(loadUser());
this.store.dispatch(loadUserStatistics());
}

onUserUpdated(user: User) {
this.store.dispatch(updateUser({ user }));
}
}

// Dumb Component (Presentational)
@Component({
selector: 'app-user-profile',
template: `
<div class="user-profile">
<h2>{{ user?.name }}</h2>
<img [src]="user?.avatar" [alt]="user?.name">

<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
<mat-form-field>
<input matInput formControlName="name" placeholder="Name">
</mat-form-field>

<mat-form-field>
<input matInput formControlName="email" placeholder="Email">
</mat-form-field>

<button mat-raised-button color="primary" type="submit">
Update Profile
</button>
</form>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserProfileComponent implements OnInit, OnChanges {
@Input() user: User | null = null;
@Output() userUpdated = new EventEmitter<User>();

profileForm = this.fb.nonNullable.group({
name: ['', [Validators.required, Validators.minLength(2)]],
email: ['', [Validators.required, Validators.email]]
});

constructor(private fb: FormBuilder) {}

ngOnInit() {
this.updateForm();
}

ngOnChanges(changes: SimpleChanges) {
if (changes['user'] && this.user) {
this.updateForm();
}
}

private updateForm() {
if (this.user) {
this.profileForm.patchValue({
name: this.user.name,
email: this.user.email
});
}
}

onSubmit() {
if (this.profileForm.valid && this.user) {
const updatedUser = {
...this.user,
...this.profileForm.value
};
this.userUpdated.emit(updatedUser);
}
}
}

Advanced State Management with NgRx

Implement robust state management for complex applications:

// State Definition
interface UserState {
users: User[];
currentUser: User | null;
loading: boolean;
error: string | null;
}

const initialState: UserState = {
users: [],
currentUser: null,
loading: false,
error: null
};

// Actions
export const loadUsers = createAction('[User] Load Users');
export const loadUsersSuccess = createAction(
'[User] Load Users Success',
props<{ users: User[] }>()
);
export const loadUsersFailure = createAction(
'[User] Load Users Failure',
props<{ error: string }>()
);

export const updateUser = createAction(
'[User] Update User',
props<{ user: User }>()
);

// Effects
@Injectable()
export class UserEffects {
loadUsers$ = createEffect(() =>
this.actions$.pipe(
ofType(loadUsers),
switchMap(() =>
this.userService.getUsers().pipe(
map(users => loadUsersSuccess({ users })),
catchError(error => of(loadUsersFailure({ error: error.message })))
)
)
)
);

updateUser$ = createEffect(() =>
this.actions$.pipe(
ofType(updateUser),
switchMap(({ user }) =>
this.userService.updateUser(user).pipe(
map(updatedUser => updateUserSuccess({ user: updatedUser })),
catchError(error => of(updateUserFailure({ error: error.message })))
)
)
)
);

constructor(
private actions$: Actions,
private userService: UserService
) {}
}

// Reducer
export const userReducer = createReducer(
initialState,
on(loadUsers, state => ({ ...state, loading: true, error: null })),
on(loadUsersSuccess, (state, { users }) => ({
...state,
users,
loading: false,
error: null
})),
on(loadUsersFailure, (state, { error }) => ({
...state,
loading: false,
error
})),
on(updateUser, state => ({ ...state, loading: true }))
);

// Selectors
export const selectUserState = createFeatureSelector<UserState>('users');

export const selectAllUsers = createSelector(
selectUserState,
state => state.users
);

export const selectCurrentUser = createSelector(
selectUserState,
state => state.currentUser
);

export const selectUserLoading = createSelector(
selectUserState,
state => state.loading
);

export const selectUserError = createSelector(
selectUserState,
state => state.error
);

Custom Directives and Pipes

Create reusable functionality with custom directives and pipes:

// Custom Directive for lazy loading images
@Directive({
selector: '[appLazyLoad]'
})
export class LazyLoadDirective implements OnInit, OnDestroy {
@Input('appLazyLoad') imageSrc!: string;
@Input() placeholder = '/assets/placeholder.png';

private observer?: IntersectionObserver;

constructor(private el: ElementReference<HTMLImageElement>) {}

ngOnInit() {
this.el.nativeElement.src = this.placeholder;

if ('IntersectionObserver' in window) {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadImage();
this.observer?.unobserve(this.el.nativeElement);
}
});
},
{ threshold: 0.1 }
);

this.observer.observe(this.el.nativeElement);
} else {
// Fallback for older browsers
this.loadImage();
}
}

ngOnDestroy() {
this.observer?.disconnect();
}

private loadImage() {
this.el.nativeElement.src = this.imageSrc;
}
}

// Custom Pipe for safe HTML
@Pipe({
name: 'safeHtml',
pure: true
})
export class SafeHtmlPipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) {}

transform(value: string): SafeHtml {
return this.sanitizer.bypassSecurityTrustHtml(value);
}
}

// Custom Pipe for highlighting search terms
@Pipe({
name: 'highlight',
pure: true
})
export class HighlightPipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) {}

transform(text: string, search: string): SafeHtml {
if (!search || !text) {
return text;
}

const pattern = search.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
const regex = new RegExp(pattern, 'gi');

const highlighted = text.replace(regex, (match) =>
`<mark class="highlight">${match}</mark>`
);

return this.sanitizer.bypassSecurityTrustHtml(highlighted);
}
}

Advanced HTTP Interceptors

Implement sophisticated HTTP handling:

// Authentication Interceptor
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(
private authService: AuthService,
private router: Router
) {}

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Skip authentication for public endpoints
if (this.isPublicEndpoint(req.url)) {
return next.handle(req);
}

return this.authService.getToken().pipe(
switchMap(token => {
if (token) {
const authReq = req.clone({
setHeaders: { Authorization: `Bearer ${token}` }
});
return next.handle(authReq);
}

// Redirect to login if no token
this.router.navigate(['/login']);
return throwError(() => new Error('No authentication token'));
})
);
}

private isPublicEndpoint(url: string): boolean {
const publicEndpoints = ['/api/auth/login', '/api/auth/register'];
return publicEndpoints.some(endpoint => url.includes(endpoint));
}
}

// Retry Interceptor
@Injectable()
export class RetryInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe(
retry({
count: 3,
delay: (error, retryCount) => {
// Exponential backoff
const delayMs = Math.pow(2, retryCount) * 1000;

// Only retry on specific error codes
if (error.status === 0 || error.status >= 500) {
return timer(delayMs);
}

// Don't retry client errors
return throwError(() => error);
}
})
);
}
}

// Loading Interceptor
@Injectable()
export class LoadingInterceptor implements HttpInterceptor {
private loadingRequests = new Set<string>();

constructor(private loadingService: LoadingService) {}

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const requestId = this.generateRequestId(req);

this.loadingRequests.add(requestId);
this.loadingService.setLoading(true);

return next.handle(req).pipe(
finalize(() => {
this.loadingRequests.delete(requestId);
if (this.loadingRequests.size === 0) {
this.loadingService.setLoading(false);
}
})
);
}

private generateRequestId(req: HttpRequest<any>): string {
return `${req.method}_${req.url}_${Date.now()}`;
}
}

Progressive Web App Implementation

Transform your Angular app into a PWA:

// Service Worker Configuration
import { SwUpdate, VersionReadyEvent } from '@angular/service-worker';

@Injectable({
providedIn: 'root'
})
export class UpdateService {
constructor(
private swUpdate: SwUpdate,
private snackBar: MatSnackBar
) {
if (this.swUpdate.isEnabled) {
this.checkForUpdates();
}
}

private checkForUpdates() {
// Check for updates every 6 hours
interval(6 * 60 * 60 * 1000).subscribe(() => {
this.swUpdate.checkForUpdate();
});

// Handle version ready events
this.swUpdate.versionUpdates
.pipe(filter((evt): evt is VersionReadyEvent => evt.type === 'VERSION_READY'))
.subscribe(() => {
this.showUpdateSnackBar();
});
}

private showUpdateSnackBar() {
const snackBarRef = this.snackBar.open(
'A new version is available!',
'Update',
{ duration: 0 }
);

snackBarRef.onAction().subscribe(() => {
this.swUpdate.activateUpdate().then(() => {
window.location.reload();
});
});
}
}

// Push Notification Service
@Injectable({
providedIn: 'root'
})
export class PushNotificationService {
constructor(private swPush: SwPush) {}

subscribeToNotifications(): Promise<PushSubscription | null> {
if (!this.swPush.isEnabled) {
return Promise.resolve(null);
}

return this.swPush.requestSubscription({
serverPublicKey: environment.vapidPublicKey
});
}

handleNotificationClick() {
return this.swPush.notificationClicks.pipe(
tap(event => {
// Handle notification click
console.log('Notification clicked:', event);
})
);
}
}

Micro-frontend Architecture

Implement micro-frontends using Angular Elements:

// Micro-frontend Component
@Component({
selector: 'mf-user-widget',
template: `
<div class="user-widget">
<h3>User Information</h3>
<div *ngIf="user">
<p>Name: {{ user.name }}</p>
<p>Email: {{ user.email }}</p>
</div>
</div>
`,
encapsulation: ViewEncapsulation.ShadowDom
})
export class UserWidgetComponent implements OnInit {
@Input() userId!: string;
user: User | null = null;

constructor(private userService: UserService) {}

ngOnInit() {
if (this.userId) {
this.loadUser();
}
}

private loadUser() {
this.userService.getUser(this.userId).subscribe(
user => this.user = user
);
}
}

// Module for Micro-frontend
@NgModule({
declarations: [UserWidgetComponent],
imports: [CommonModule, HttpClientModule],
providers: [UserService]
})
export class UserWidgetModule {
constructor(private injector: Injector) {}

ngDoBootstrap() {
const userWidgetElement = createCustomElement(UserWidgetComponent, {
injector: this.injector
});

customElements.define('mf-user-widget', userWidgetElement);
}
}

// Host Application Integration
@Component({
selector: 'app-dashboard',
template: `
<div class="dashboard">
<h1>Dashboard</h1>

<!-- Micro-frontend component -->
<mf-user-widget [userId]="currentUserId"></mf-user-widget>

<!-- Other dashboard content -->
</div>
`
})
export class DashboardComponent {
currentUserId = '123';
}

Performance Optimization

Implement advanced performance optimizations:

// Virtual Scrolling for Large Lists
@Component({
selector: 'app-large-list',
template: `
<cdk-virtual-scroll-viewport itemSize="50" class="viewport">
<div *cdkVirtualFor="let item of items" class="item">
{{ item.name }}
</div>
</cdk-virtual-scroll-viewport>
`
})
export class LargeListComponent {
items = Array.from({ length: 100000 }, (_, i) => ({
id: i,
name: `Item ${i}`
}));
}

// Track By Function for NgFor
@Component({
selector: 'app-optimized-list',
template: `
<div *ngFor="let item of items; trackBy: trackByFn" class="item">
{{ item.name }}
</div>
`
})
export class OptimizedListComponent {
items: Item[] = [];

trackByFn(index: number, item: Item): any {
return item.id; // Use unique identifier
}
}

// Preloading Strategy
export class CustomPreloadingStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
if (route.data?.['preload'] === true) {
return load();
}
return of(null);
}
}

// Route Configuration with Preloading
const routes: Routes = [
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule),
data: { preload: true }
},
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
data: { preload: false }
}
];

Testing Strategies

Implement comprehensive testing:

// Component Testing
describe('UserProfileComponent', () => {
let component: UserProfileComponent;
let fixture: ComponentFixture<UserProfileComponent>;
let mockUserService: jasmine.SpyObj<UserService>;

beforeEach(async () => {
const spy = jasmine.createSpyObj('UserService', ['updateUser']);

await TestBed.configureTestingModule({
declarations: [UserProfileComponent],
imports: [ReactiveFormsModule, MaterialModule],
providers: [
{ provide: UserService, useValue: spy }
]
}).compileComponents();

mockUserService = TestBed.inject(UserService) as jasmine.SpyObj<UserService>;
});

beforeEach(() => {
fixture = TestBed.createComponent(UserProfileComponent);
component = fixture.componentInstance;
});

it('should update user when form is submitted', () => {
const mockUser: User = { id: '1', name: 'John', email: 'john@example.com' };
component.user = mockUser;

fixture.detectChanges();

component.profileForm.patchValue({
name: 'John Updated',
email: 'john.updated@example.com'
});

const userUpdatedSpy = spyOn(component.userUpdated, 'emit');

component.onSubmit();

expect(userUpdatedSpy).toHaveBeenCalledWith({
...mockUser,
name: 'John Updated',
email: 'john.updated@example.com'
});
});
});

// Service Testing
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [UserService]
});

service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});

afterEach(() => {
httpMock.verify();
});

it('should fetch users', () => {
const mockUsers: User[] = [
{ id: '1', name: 'John', email: 'john@example.com' },
{ id: '2', name: 'Jane', email: 'jane@example.com' }
];

service.getUsers().subscribe(users => {
expect(users).toEqual(mockUsers);
});

const req = httpMock.expectOne('/api/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers);
});
});

Conclusion

Modern Angular development requires understanding of:

  1. Component Architecture: Smart vs dumb components, proper data flow
  2. State Management: NgRx for complex applications
  3. Performance: Change detection, lazy loading, virtual scrolling
  4. Progressive Enhancement: PWA capabilities, service workers
  5. Micro-frontends: Scalable architecture for large teams
  6. Testing: Comprehensive unit and integration testing

These patterns and practices will help you build maintainable, scalable Angular applications that can grow with your business needs.

Ready for more? Next, I'll dive into building real-time applications with Angular and WebSockets!