Advanced Angular Techniques

Ashish Sharma
7 min readSep 5, 2024

--

In the rapidly evolving landscape of web development, Angular continues to stand out as a robust framework for building dynamic and complex applications. While many developers are familiar with Angular’s core features, there are advanced and less commonly known techniques that can elevate your Angular projects to new levels of efficiency, modularity, and scalability.

Photo by Goran Ivos on Unsplash

This guide explores five cutting-edge Angular techniques that go beyond traditional usage, offering innovative solutions for state management, component interoperability, micro-frontend architecture, and more. Whether you’re looking to optimize your current workflow or experiment with the latest advancements in Angular, these techniques provide a unique edge in crafting sophisticated applications.

1. Composable Angular State with Custom Reactive Stores

Purpose:

This technique involves creating a custom reactive store using RxJS and Angular’s Signals API. This allows for fine-grained control over state changes and provides a more flexible alternative to traditional state management libraries like NgRx. The key advantage is that it combines the power of RxJS for complex state management with the simplicity of Angular’s signals for reactive updates.

Use Cases:

  • Decoupled State Management: When you want to manage the state outside of Angular services or components while maintaining reactivity.
  • Fine-Grained Control: When you need to track individual slices of state with different update strategies.
  • Scalability: When your application grows, and you want to keep state management modular and easy to test.

Implementation Steps:

  1. Create a Store Service: Define a service that holds the application’s state using a BehaviorSubject for RxJS-based state management and a Signal for fine-grained control.
  2. Define State Methods: Implement methods to update and retrieve state, leveraging RxJS operators and Angular’s Signals API.
  3. Inject and Use the Store: Use the store in your components to manage state reactively.
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { signal } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class StoreService<T> {
private state$ = new BehaviorSubject<T>(null!);
private signalState = signal<T>(null!);

constructor(private initialState: T) {
this.setState(initialState);
}

setState(newState: T): void {
this.state$.next(newState);
this.signalState.set(newState);
}

getState(): Observable<T> {
return this.state$.asObservable();
}

select<K extends keyof T>(key: K): Observable<T[K]> {
return this.state$.pipe(map(state => state[key]));
}

// Usage of Signals
getSignalState(): T {
return this.signalState();
}
}

// Usage in a component
@Component({
selector: 'app-root',
template: `<div>{{ state | async | json }}</div>`,
})
export class AppComponent {
state: Observable<any>;

constructor(private store: StoreService<any>) {
this.state = this.store.getState();
}

updateState() {
this.store.setState({ key: 'newValue' });
}
}

Challenges:

  • Complexity: While powerful, managing both RxJS and Signals can be complex, especially for developers less familiar with reactive programming.
  • Debugging: Debugging reactive streams can be challenging, requiring a solid understanding of RxJS operators and their behavior.

2. Custom Elements (Web Components) with Angular’s Ivy Renderer

Purpose:

This technique leverages Angular’s Ivy Renderer to create custom elements (also known as Web Components) that are reusable across different frameworks or even vanilla JavaScript projects. This enables Angular components to be used in non-Angular environments, promoting reusability and interoperability.

Use Cases:

  • Cross-Framework Interoperability: When you need to share Angular components with teams using different frameworks (e.g., React, Vue).
  • Micro-Frontends: In a micro-frontend architecture, where different parts of an application are built with different frameworks.
  • Progressive Web Apps: When building PWAs that require highly reusable and self-contained components.

Implementation Steps:

  1. Create a Standalone Component: Define a simple Angular component that you want to expose as a custom element.
  2. Configure Ivy Renderer: Use the createCustomElement function to wrap the Angular component into a Web Component.
  3. Define Custom Element: Register the custom element with the browser’s customElements registry.
  4. Use in HTML: The resulting Web Component can be used in any HTML file, without requiring Angular.
import { Component, Injector } from '@angular/core';
import { createCustomElement } from '@angular/elements';
import { NgModule, DoBootstrap } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

@Component({
selector: 'app-greeting',
template: `<p>Hello, {{ name }}!</p>`,
})
export class GreetingComponent {
name = 'Angular';
}

@NgModule({
declarations: [GreetingComponent],
imports: [BrowserModule],
entryComponents: [GreetingComponent],
})
export class AppModule implements DoBootstrap {
constructor(private injector: Injector) {}

ngDoBootstrap() {
const GreetingElement = createCustomElement(GreetingComponent, { injector: this.injector });
customElements.define('app-greeting', GreetingElement);
}
}

// This component can now be used as a custom element in any HTML:
// <app-greeting></app-greeting>

Challenges:

  • Browser Compatibility: While Web Components are widely supported, older browsers may require polyfills.
  • Limited Angular Features: Some Angular-specific features (like dependency injection) may not work seamlessly when the component is used outside Angular.

3. Reactive Event Bus with Angular and RxJS

Purpose:

The Reactive Event Bus technique involves creating an event bus service using RxJS to facilitate communication between loosely coupled components. This avoids the need for direct parent-child communication and allows components to broadcast and listen to events without being tightly coupled.

Use Cases:

  • Decoupling Components: When components need to communicate without being directly related, such as in a large, modular application.
  • Cross-Cutting Concerns: For handling global events like authentication, theme changes, or real-time data updates across different parts of an application.
  • Plugin Systems: When building systems where components need to subscribe to or emit events dynamically, like a plugin architecture.

Implementation Steps:

  1. Create an Event Bus Service: Define a service that uses an RxJS Subject to emit and listen to events.
  2. Emit Events: Use the service to emit events from any component.
  3. Subscribe to Events: Subscribe to events in components that need to react to these events.
  4. Filter Events: Use RxJS operators like filter to ensure that components only react to relevant events.
import { Injectable } from '@angular/core';
import { Subject, Observable } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class EventBusService {
private eventBus$ = new Subject<{ event: string; payload: any }>();

emit(event: string, payload: any) {
this.eventBus$.next({ event, payload });
}

on(event: string): Observable<any> {
return this.eventBus$.asObservable().pipe(
filter(e => e.event === event),
map(e => e.payload)
);
}
}

// Emitting an event
@Component({
selector: 'app-emitter',
template: `<button (click)="sendMessage()">Send Message</button>`,
})
export class EmitterComponent {
constructor(private eventBus: EventBusService) {}

sendMessage() {
this.eventBus.emit('customEvent', { message: 'Hello from Emitter!' });
}
}

// Listening to an event
@Component({
selector: 'app-listener',
template: `<p>{{ message }}</p>`,
})
export class ListenerComponent {
message: string;

constructor(private eventBus: EventBusService) {
this.eventBus.on('customEvent').subscribe(data => {
this.message = data.message;
});
}
}

Challenges:

  • Memory Leaks: If subscriptions are not properly managed, there’s a risk of memory leaks, especially in long-lived applications.
  • Debugging: Tracing events through an event bus can be challenging, especially when multiple components interact in complex ways.

4. Dynamic Module Federation with Angular and Webpack 5

Purpose:

Dynamic Module Federation is a technique that allows you to dynamically load Angular modules or components at runtime from remote applications. This enables a micro-frontend architecture where different parts of an application can be developed and deployed independently, even with different Angular versions.

Use Cases:

  • Micro-Frontend Architecture: When you want to split an application into smaller, independently deployable modules.
  • Multi-Team Development: When different teams work on different parts of an application, each with its own Angular setup.
  • Version Compatibility: When you need to mix modules built with different versions of Angular in a single application.

Implementation Steps:

  1. Configure Module Federation: In the Webpack configuration, set up the Module Federation plugin to expose and consume modules.
  2. Load Modules Dynamically: Use Angular’s loadRemoteModule or similar approaches to load modules at runtime based on user interaction.
  3. Ensure Shared Dependencies: Define shared dependencies between the host and remote modules to avoid loading multiple instances of Angular.
// webpack.config.js
module.exports = {
output: {
uniqueName: 'angularApp',
},
optimization: {
runtimeChunk: false,
},
experiments: {
outputModule: true,
},
plugins: [
new ModuleFederationPlugin({
name: 'angularApp',
filename: 'remoteEntry.js',
exposes: {
'./Component': './src/app/app.component.ts',
},
shared: ['@angular/core', '@angular/common'],
}),
],
};

// Importing the remote module dynamically
@Component({
selector: 'app-root',
template: `<button (click)="loadComponent()">Load Remote Component</button><ng-container #container></ng-container>`,
})
export class AppComponent {
@ViewChild('container', { read: ViewContainerRef }) container!: ViewContainerRef;

constructor(private injector: Injector, private compiler: Compiler) {}

async loadComponent() {
const { RemoteComponent } = await import('remoteApp/Component');
const moduleFactory = await this.compiler.compileModuleAsync(RemoteComponent);
const moduleRef = moduleFactory.create(this.injector);
const compFactory = moduleRef.componentFactoryResolver.resolveComponentFactory(RemoteComponent);
this.container.createComponent(compFactory);
}
}

Challenges:

  • Complex Configuration: Setting up Webpack and Module Federation requires careful configuration, particularly around shared dependencies.
  • Performance: Dynamically loading modules can introduce latency; optimizations like preloading or caching strategies might be necessary.

5. Advanced Content Projection with ngTemplateContextGuard

Purpose:

Advanced Content Projection with ngTemplateContextGuard enables type-safe content projection in Angular, making it possible to pass strongly-typed context objects to projected templates. This improves reusability and maintainability by ensuring that the context passed to a template is type-checked.

Use Cases:

  • Reusable Components: When you need to create highly reusable components that can accept and project different types of content with strong typing.
  • Complex UIs: When building complex UI components that require dynamic content projection, such as dashboards, data grids, or form builders.
  • Component Libraries: For Angular component libraries where type safety and flexibility are critical, and users need to project various content types.

Implementation Steps:

  1. Create a Directive: Define a directive that uses ngTemplateContextGuard to enforce type safety on projected content.
  2. Define the Template Context: Use the directive to specify the type of the context object that the projected template will receive.
  3. Use the Directive in a Component: Apply the directive in a component template to project content, ensuring that the context passed to the template matches the expected type.
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
selector: '[appAdvancedProjection]'
})
export class AdvancedProjectionDirective<T> {
@Input() set appAdvancedProjection(context: T) {
this.viewContainer.clear();
this.viewContainer.createEmbeddedView(this.templateRef, context);
}

static ngTemplateContextGuard<T>(dir: AdvancedProjectionDirective<T>, ctx: any): ctx is T {
return true;
}

constructor(private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef) {}
}

// Usage in a component
@Component({
selector: 'app-root',
template: `
<ng-template #content let-name let-age="age">
<p>Name: {{ name }}</p>
<p>Age: {{ age }}</p>
</ng-template>

<div *appAdvancedProjection="{ $implicit: 'John', age: 30 }" [ngTemplateOutlet]="content"></div>
`,
})
export class AppComponent {}

Challenges:

  • Learning Curve: Understanding and correctly implementing ngTemplateContextGuard can be tricky, especially for developers new to advanced Angular features.
  • Limited Documentation: As this is a more advanced feature, there may be less documentation and fewer examples available compared to more common Angular patterns.

These advanced techniques push the boundaries of what’s possible with Angular, offering powerful tools for building scalable, modular, and maintainable applications. Each technique comes with its own set of challenges and learning curves, but mastering them can significantly enhance your ability to create complex, high-performance applications with Angular.

--

--

Ashish Sharma
Ashish Sharma

Written by Ashish Sharma

Enterprise Architect & ADD supportive framework Designer. Helping MSMEs to build their echo system using HI and AI.

Responses (7)