Transforming Your Application into Micro Frontends with Native Federation for Angular — Part 2
Learn how to start structuring your application to work with MFE using Native Federation for Angular
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:
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
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>
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.
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