Angular Federation

@angular-architects/native-federation​​

Setup

Micro FE

Shell

localhost:4200

Home

localhost:4201

Login

localhost:4202

// federation.config.js

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

module.exports = withNativeFederation({

  name: 'home',

  exposes: {
    './Component': './apps/home/src/app/app.component.ts',
  },

  shared: {
    ...shareAll(
      { 
        singleton: true, 
        strictVersion: true, 
        requiredVersion: 'auto'
      }
    ),
  },

  skip: [
    'rxjs/ajax',
    'rxjs/fetch',
    'rxjs/testing',
    'rxjs/webSocket',
    // Add further packages you don't need at runtime
  ]

  // Please read our FAQ about sharing libs:
  // https://shorturl.at/jmzH0
});

Home FE

Home

localhost:4201

...

// federation.config.js

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

module.exports = withNativeFederation({
  
  name: 'login',

  exposes: {
    './routes': './apps/login/src/app/app.routes.ts',
  },

  shared: {
    ...shareAll({
      singleton: true,
      strictVersion: true,
      requiredVersion: 'auto',
    }),
  },

  skip: [
    'rxjs/ajax',
    'rxjs/fetch',
    'rxjs/testing',
    'rxjs/webSocket',
    // Add further packages you don't need at runtime
  ],

  // Please read our FAQ about sharing libs:
  // https://shorturl.at/jmzH0
});

Login FE

Login

localhost:4202

...

// federation.config.js

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

module.exports = withNativeFederation({
  shared: {
    ...shareAll({
      singleton: true,
      strictVersion: true,
      requiredVersion: 'auto',
    }),
  },

  skip: [
    'rxjs/ajax',
    'rxjs/fetch',
    'rxjs/testing',
    'rxjs/webSocket',
    // Add further packages you don't need at runtime
  ],

  // Please read our FAQ about sharing libs:
  // https://shorturl.at/jmzH0
});

Shell FE (Host)

Shell

localhost:4200

...

// public/federation.manifest.json

{
  "home": "http://localhost:4201/remoteEntry.json",
  "login": "http://localhost:4202/remoteEntry.json"
}

Application Flow

Forgot password

Home

localhost:4201

Login

localhost:4202

App

Login

Forgot Password

Forgot Password Success

Shell

localhost:4200

App

Shell -> Home

Shell -> Login

              Login ---> Forgot Password

              Login ---> Forgot Password Success

Shell <- Login

Shell routes

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

export const appRoutes: Route[] = [
  {
    path: '',
    loadComponent: () =>
      loadRemoteModule('home', './Component')
        .then((m) => m.AppComponent),
  },
  {
    path: 'login',
    loadChildren: () =>
      loadRemoteModule('login', './routes')
        .then((m) => m.appRoutes),
  },
];


Shell -> Home

Shell -> Login

 

Login routes

import { Route } from '@angular/router';
import { LoginComponent } from './login/login.component';

export const appRoutes: Route[] = [
  {
    path: '',
    component: LoginComponent,
  },
  {
    path: 'forgot-password',
    loadComponent: () =>
      import('./forgot-password/forgot-password.component').then(
        (m) => m.ForgotPasswordComponent
      ),
  },
  {
    path: 'forget-password-success',
    loadComponent: () =>
      import(
        './forgot-password-success/forgot-password-success.component'
      ).then((m) => m.ForgotPasswordSuccessComponent),
  },
];

Login ---> Forgot Password

Login ---> Forgot Password Success

Forgot Password

...

@Component({
  selector: 'app-forgot-password',
  standalone: true,
  imports: [CommonModule, SfcModule],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  templateUrl: './forgot-password.component.html',
  styleUrl: './forgot-password.component.css',
})
export class ForgotPasswordComponent {
  private router = inject(Router);
  private activatedRoute = inject(ActivatedRoute);

  back() {
    this.router.navigate(['../'], { relativeTo: this.activatedRoute });
  }

  submit() {
    this.router.navigate(['../forget-password-success'], {
      relativeTo: this.activatedRoute,
    });
  }
}

Back to login sibling

Continue to forgot-password-success sibling

Application State

Login

Home

localhost:4201

Login

localhost:4202

App

Login

Shell

localhost:4200

App

AuthService

isLoggedIn$

isLoggedIn$


login(username, password)

user$

Login

...
// Automatically shared since it's a Nx Lib
import { AuthService } from '@frontend/auth';

@Component({
  selector: 'app-login',
  standalone: true,
  imports: [CommonModule, SfcModule, RouterModule],
  templateUrl: './login.component.html',
  styleUrl: './login.component.css',
})
export class LoginComponent {
  private authService = inject(AuthService);

  private router = inject(Router);
  private activatedRoute = inject(ActivatedRoute);

  constructor() {
    this.authService.isLoggedIn$
      .pipe(
        filter((loggedIn) => loggedIn === true),
        takeUntilDestroyed()
      )
      .subscribe(() => {
        this.router.navigate(['../'], { relativeTo: this.activatedRoute });
      });
  }

  login(username: string, password: string) {
    this.authService.login(username, password);
  }
}

Back to shell

Shared service

Web Components

Lit

...
import { html, unsafeCSS } from 'lit';
import { customElement, property } from 'lit/decorators.js';

@customElement('sbb-teaser-tile')
export class TeaserTile extends SbbLitElement {
  static styles = [unsafeCSS(styles)];

  @property({ type: String }) title!: string;
  @property({ type: String }) content!: string;
  @property({ type: String }) direction: 'top' | 'left' | 'right' = 'right';
  @property({ type: String }) linkText?: string;
  @property({ type: String }) src?: string;
  @property({ type: String }) alt?: string;
  @property({ type: Object }) routingOptions!: SbbRoutingOptions;

  render() {
    return html`
      ...
    `;
  }
}
declare global {
  interface HTMLElementTagNameMap {
    'sbb-teaser-tile': TeaserTile;
  }
}

Define 'sbb-teaser-tile' automatically in the customElements registry

Potential solution 1

Define 'sbb-teaser-tile' automatically in the customElements registry

// federation.config.js

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

module.exports = withNativeFederation({

  name: 'home',

  exposes: {
    './Component': './apps/home/src/app/app.component.ts',
  },

  shared: {
    ...shareAll(
      { 
        singleton: true, 
        strictVersion: true, 
        requiredVersion: 'auto'
      }
    ),
  },

  skip: [
    'rxjs/ajax',
    'rxjs/fetch',
    'rxjs/testing',
    'rxjs/webSocket',
    // Add further packages you don't need at runtime
  ]

  // Please read our FAQ about sharing libs:
  // https://shorturl.at/jmzH0
});

Figure out if federation can prevent loading Lit components multiple times.

Potential solution 2

Current setup

Home

localhost:4201

Login

localhost:4202

App

Login

Shell

localhost:4200

App

CDN

localhost:9080

<script type="module" data-sbb="teaser-tile">
  import { TeaserTile } from 'http://localhost:9080/core/teaser-tile/index.js';
  customElements.define('sbb-teaser-tile', TeaserTile);
</script>

If the following script is not present, add it:

Angular Federation

By rachnerd

Angular Federation

  • 58