Transforming Your Application into Micro Frontends with Native Federation for Angular — Part 2

Erick Zanetti
5 min readJul 30, 2024

--

Learn how to start structuring your application to work with MFE using Native Federation for Angular

Angular with Native Federation

Angular with Native Federation

In the first article, we learned how to set up the initial configuration of the projects and got them running. Now, let’s advance a bit further and learn how to better utilize routes within the MFEs.

Creating Components

First, let’s create a component that will encapsulate our entire MFE, removing this responsibility from the app.component:

ng g c crud
ng g c crud/create
ng g c crud/read
ng g c crud/update
ng g c crud/delete

Editing Routes

Next, edit the app.routes.ts:

import { Routes } from '@angular/router';

export const routes: Routes = [
{
path: 'crud',
loadComponent: () => import('./crud/crud.component').then(m => m.CrudComponent),
children: [
{
path: 'create',
loadComponent: () => import('./crud/create/create.component').then(m => m.CreateComponent)
},
{
path: 'read',
loadComponent: () => import('./crud/read/read.component').then(m => m.ReadComponent)
},
{
path: 'update',
loadComponent: () => import('./crud/update/update.component').then(m => m.UpdateComponent)
},
{
path: 'delete',
loadComponent: () => import('./crud/delete/delete.component').then(m => m.DeleteComponent)
},
{
path: '',
redirectTo: 'read',
pathMatch: 'full'
}
]
},
{
path: '',
redirectTo: 'crud',
pathMatch: 'full'
},
{
path: '**',
redirectTo: 'crud'
}
];

Styling the App Component

Edit app.component.ts to add styling:

<header>
<h1>Micro Frontend 1</h1>
</header>
<router-outlet></router-outlet>

And app.component.scss:

header {
background-color: #f1f1f1;
padding: 10px;
text-align: center;
}

CRUD Component

For crud.component.html:

<nav>
<a routerLink="create" routerLinkActive="active-link">Create</a>
<a routerLink="read" routerLinkActive="active-link">Read</a>
<a routerLink="update" routerLinkActive="active-link">Update</a>
<a routerLink="delete" routerLinkActive="active-link">Delete</a>
</nav>
<div class="content">
<router-outlet></router-outlet>
</div>

And crud.component.scss:

nav {
background-color: #f8f9fa;
padding: 1rem;
border-bottom: 1px solid #ddd;
}
nav a {
margin-right: 1rem;
text-decoration: none;
color: #007bff;
font-weight: bold;
}
nav a.active-link {
color: #0056b3;
text-decoration: underline;
}
.content {
padding: 2rem;
}

With this setup, we have an independent application where app.component serves as a shell for applying styles or other rules:

mfe works alone

Exporting the CRUD Component

To export only the crud.component, we will start by editing our federation.config.js and including our shared routes file:

const { withNativeFederation, shareAll } = require('@angular-architects/native-federation/config');

module.exports = withNativeFederation({
name: 'mfe1',
exposes: {
"./routes": "./src/app/app.routes.ts",
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
skip: [
'rxjs/ajax',
'rxjs/fetch',
'rxjs/testing',
'rxjs/webSocket',
]
});

Updating Shell Routes

To make this work, we will need to go to the shell and update our app.routes.ts:

import { loadRemoteModule } from '@angular-architects/native-federation';
import { Routes } from '@angular/router';

export const routes: Routes = [
{
path: 'mfe1',
loadChildren: () => loadRemoteModule('mfe1', './routes').then((m) => m.routes),
},
{
path: 'mfe2',
loadComponent: () => loadRemoteModule('mfe1', './Component').then((m) => m.AppComponent),
},
];

With this we are able to import only the part that interests us

MFE works insede shell

But now we have a problem, the route inside the crud is loaded, and the component is initialized, but now when we try to navigate through the menu we created in mfe1 nothing happens. However, if we try to call the route directly in the shell it will work normally:

<div style="padding: 20px; background-color: #f0f0f0; text-align: center;">
<a routerLink="/mfe1/crud/read" style="color: #007bff; text-decoration: none; font-size: 18px;">go to read</a>
</div>
Navigate not works

Navigating with Events

To handle navigation issues, we will use Events. First, set a property in the window to know if we are running through the shell and add a HostListener in the app.component.ts of the shell to capture events and perform navigations:

import { Component, HostListener, OnInit } from '@angular/core';
import { Router, RouterOutlet } from '@angular/router';

declare global {
interface Window {
isShell: boolean;
}
}

@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent implements OnInit {

constructor(
private router: Router
) { }

ngOnInit() {
window.isShell = true;
}

@HostListener('window:childRouteChanged', ['$event'])
onChildRouteChanged(event: any) {
this.router.navigate([event.detail.route], event.detail.extras);
}
}

Custom Navigation Service

Create a custom navigation service in MFE1:

import { Injectable, signal } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { NavigationExtras, Router } from '@angular/router';
import { Observable } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class CustomNavigateService {

private SHELL_ROUTE = 'mfe1/'
private _navigation = signal(false);
private _navigation$!: Observable<boolean>;

get navigation(): Observable<boolean> {
return this._navigation$;
}

constructor(
private router: Router
) {
this._navigation$ = toObservable(this._navigation);
}

navigate(route: string, extras?: NavigationExtras) {
if (window.isShell) {
window.dispatchEvent(new CustomEvent('childRouteChanged', { detail: { route: `${this.SHELL_ROUTE}${route}`, extras } }));
} else {
this.router.navigate([route], extras);
}
this._navigation.set(true);
}
}

Custom Router Link Directive

Create a custom router link directive:

import { Directive, ElementRef, HostListener, Input, OnInit } from '@angular/core';
import { CustomNavigateService } from '../services/custom-navigate.service';

@Directive({
selector: '[customRouterLink]',
standalone: true
})
export class CustomRouterLinkDirective implements OnInit {
@Input() customRouterLink: any;
@Input() routerLinkActive: string = "";
@Input() queryParams: any;

constructor(
private customNavigateService: CustomNavigateService,
private el: ElementRef
) { }

ngOnInit(): void {
this.watchNavigation();
this.verifyActive();
}

@HostListener('click')
navigate(): void {
this.customNavigateService.navigate(this.customRouterLink, { queryParams: this.queryParams });
}

watchNavigation(): void {
this.customNavigateService.navigation.subscribe(() => {
this.verifyActive();
});
}

verifyActive(): void {
if (this.routerLinkActive) {
if (window.location.pathname === this.customRouterLink) {
this.el.nativeElement.classList.add(this.routerLinkActive);
} else {
this.el.nativeElement.classList.remove(this.routerLinkActive);
}
}
}
}

With this setup, the route redirection called inside the MFE will now work correctly.

Shell and mfes working

For a practical demonstration and to follow along with the implementation details, you can check out the complete source code on GitHub. The repository includes all the necessary configurations and code examples to help you set up and run the project seamlessly. Visit the repository here.

Conclusion

In this second part of our series on Transforming Your Application into Micro Frontends with Native Federation for Angular, we explored how to better structure our application to work with Micro Frontends, utilizing routes efficiently. We created specific CRUD components and adjusted our routes to load these components dynamically. Additionally, we covered how to style our components to enhance the user experience.

We also discussed how to export only the necessary parts of the application and integrate these parts into a shell using Native Federation. To address navigation issues within the Micro Frontends, we implemented an event-based approach, creating a custom navigation service and a custom router link directive.

Next Step

With these techniques, you can create more modular and scalable applications, making your projects easier to maintain and evolve. In the next part, we will delve into common issues and how to remotely import a component into any page. You can access part three here.

Follow me on LinkedIn: https://www.linkedin.com/in/erickzanetti

--

--

Erick Zanetti

Full Stack developer with angular and Java. Learning Go.