single-spa-angular
Project Status
The single-spa-angular project is overseen by the single-spa core team, but largely maintained by the community using it. This is because the single-spa core team is stronger in other frameworks and often doesn't have the expertise to fix bugs in a timely manner. There are a few community members who help us actively maintain the project, which we greatly appreciate. If you have Angular experience, we'd appreciate your help in maintaining the repo. To do so, set up notifications by watching the single-spa-angular Github repository. Also, please join the #maintainers
and #angular
channels in Slack.
Introduction
single-spa-angular is a library for creating Angular microfrontends.
Each microfrontend (single-spa application) is an Angular CLI project that can use its own version of Angular and be deployed separately from any other. They all come together into a single web page where one or more single-spa applications is active at any time.
The documentation here is extensive, so use the sidenav on the right. 👉👉👉
Community
Join the #angular
channel in single-spa's Slack workspace.
Demo
https://coexisting-angular-microfrontends.surge.sh
Starter repo
https://github.com/joeldenning/coexisting-angular-microfrontends
Contributing
For instructions on how to test this locally before creating a pull request, see the Contributing docs.
Angular versions
AngularJS
AngularJS is supported by single-spa-angularjs, instead of single-spa-angular. See AngularJS docs.
Angular 2
Angular 2 is supported by single-spa-angular@3.
The single-spa-angular schematics are not supported by Angular 2, so you'll have to follow the steps for manual installation. The single-spa helpers work with Angular 2.
Angular 3
Angular 3 never existed.
Angular 4
Angular 4 is supported by single-spa-angular@3.
The single-spa-angular schematics are not supported by Angular 4, so you'll have to follow the steps for manual installation. The single-spa helpers work with Angular 4.
Angular 5
Angular 5 is supported by single-spa-angular@3.
The single-spa-angular schematics are not supported by Angular 5, so you'll have to follow the steps for manual installation. The single-spa helpers work with Angular 5.
Angular 6
Angular 6 is supported by single-spa-angular@3.
The single-spa-angular schematics are not supported by Angular 6, so you'll have to follow the steps for manual installation. The single-spa helpers work with Angular 6.
Angular 7
Angular 7 is supported by single-spa-angular@3.
Both the single-spa-angular schematics and the single-spa helpers work with Angular 7. Follow the Angular CLI instructions.
Note that the schematics for Angular 7 use an Angular Builder that is no longer used in the Angular 8 schematics.
Angular 8
Angular 8 is supported by single-spa-angular@3.
Both the single-spa-angular schematics and the single-spa helpers work with Angular 8. Follow the Angular CLI instructions.
Note that the schematics for Angular 8 do not use the custom Angular builder, but instead use @angular-builders/custom-webpack.
Angular 9
Angular 9 is supported by single-spa-angular@4.
Both the single-spa-angular schematics and the single-spa helpers work with Angular 9. Follow the Angular CLI instructions.
Note that the schematics for Angular 9 do not use the custom Angular builder, but instead use @angular-builders/custom-webpack.
Angular 10
Angular 10 is supported by single-spa-angular@4.
Both the single-spa-angular schematics and the single-spa helpers work with Angular 10. Follow the Angular CLI instructions.
Note that the schematics for Angular 10 do not use the custom Angular builder, but instead use @angular-builders/custom-webpack.
Angular 11
Angular 11 is supported by single-spa-angular@4.
Both the single-spa-angular schematics and the single-spa helpers work with Angular 11. Follow the Angular CLI instructions.
Note that the schematics for Angular 11 do not use the custom Angular builder, but instead use @angular-builders/custom-webpack.
Angular 12
Angular 12 is supported by single-spa-angular@5.
Both the single-spa-angular schematics and the single-spa helpers work with Angular 12. Follow the Angular CLI instructions.
Angular 13
Angular 13 is supported by single-spa-angular@6.
Both the single-spa-angular schematics and the single-spa helpers work with Angular 13. Follow the Angular CLI instructions.
Angular 14
Angular 14 is supported by single-spa-angular@7.
Both the single-spa-angular schematics and the single-spa helpers work with Angular 14. Follow the Angular CLI instructions.
Angular 15
Angular 15 is supported by single-spa-angular@8.
Both the single-spa-angular schematics and the single-spa helpers work with Angular 15. Follow the Angular CLI instructions.
Angular 16
Angular 16 is supported by single-spa-angular@9.
Both the single-spa-angular schematics and the single-spa helpers work with Angular 16. Follow the Angular CLI instructions.
Angular 17
Angular 17 is supported by single-spa-angular@9.
Both the single-spa-angular schematics and the single-spa helpers work with Angular 17. Follow the Angular CLI instructions.
Angular 18
Angular 18 is supported by single-spa-angular@9.
Both the single-spa-angular schematics and the single-spa helpers work with Angular 18. Follow the Angular CLI instructions.
Angular CLI
You may use Angular CLI and single-spa together with any version of Angular. However, the Angular CLI schematics only work if you're using Angular >= 7. If you're using an older version of Angular, follow the manual installation instructions.
Installation
First, create an angular application. This requires installing Angular CLI. Note that the --prefix
is important so that when you have multiple angular applications their component selectors won't have the same names.
ng new my-app --routing --prefix my-app
cd my-app
⚠️ Please read this before proceeding ⚠️
Starting from Angular 17, when running ng new
, a project with the ESBuild builder is created by default ("builder": "@angular-devkit/build-angular:application"
). However, ESBuild does not support all of the required features for single-spa, such as SystemJS output. Therefore, we cannot simply provide the ESBuild plugin in the same way we do for Webpack with config transformer. Before running schematics or performing any updates, it is necessary to change the builder
property inside the angular.json
to @angular-devkit/build-angular:browser
, and the browser
property (typically pointing to src/main.ts
) should be changed to main
.
In the root of your Angular CLI application run the following:
# If you use any Angular version lower than 14.
ng add single-spa-angular
# If you use Angular 14 you have to specify the project name.
# This is because the `defaultProject` option has been deprecated by Angular CLI.
ng add single-spa-angular --project my-cool-app
Schematics
Angular schematics are processed when you run ng add single-spa-angular
or ng add single-spa-angular --project my-cool-app
.
The single-spa-angular schematics perform the following tasks:
- Install single-spa-angular.
- Generate a
main.single-spa.ts
in your projectsrc/
. - Generate
single-spa-props.ts
insrc/single-spa/
- Generate
asset-url.ts
insrc/single-spa/
- Generate an EmptyRouteComponent in
src/app/empty-route/
, to be used in app-routing.module.ts. - Add an npm script
npm run build:single-spa
. - Add an npm script
npm run serve:single-spa
. - For Angular 7 only, create a new entry in the project's architect called
single-spa
, which is a preconfigured Angular Builder.
Finish installation
Now you must configure routes. Then you can serve and build.
Manual Installation
The manual installation instructions should be used if you are not using Angular CLI or if you are using Angular 6 or older.
Installation
npm install single-spa-angular
# Or if you're using yarn
yarn add single-spa-angular
# Or if you're using pnpm
pnpm install single-spa-angular
Manually apply schematics
Since the single-spa-angular schematics didn't run, you'll need to make the following changes:
- Create all of the files that would have been created by the schematic. See schematics files. Be sure to get the files in the subdirectories, too.
- Add
build:single-spa
andserve:single-spa
to the scripts in your package.json. SeeaddNPMScripts
function. - Use the angular builder, as described in the next section.
Use Angular Builder
Note that this only applies to Angular versions pre Angular 8. Up until Angular 8, we maintained an angular builder that allowed us to control the webpack config, but since Angular 8 we use @angular-builders/custom-webpack instead. See documentation for using the custom webpack builder with single-spa-angular and Angular 8+.
If you installed this library with Angular 7 using the Angular Schematic, this is already configured and you don't need to change it. Otherwise, you might need to do this manually.
If you don't use Angular CLI, skip this section.
To build your Angular CLI application as a single-spa app do the following.
- Open
angular.json
- Locate the project you wish to update.
- Navigate to the
architect > build
property. - Set the
builder
property tosingle-spa-angular:build
. - Run
ng build
and verify your dist contains one asset,main.js
.
Example Configuration:
{
"architect": {
"build": {
"builder": "single-spa-angular:build",
"options": {
"libraryName": "hello"
}
},
"serve": {
"builder": "single-spa-angular:dev-server",
"options": {}
}
}
}
ng build options
Configuration options are provided to the architect.build.options
section of your angular.json.
Name | Description | Default Value |
---|---|---|
libraryName | (optional) Specify the name of the module | Angular CLI project name |
libraryTarget | (optional) The type of library to build see available options | "UMD" |
singleSpaWebpackConfigPath | (optional) Path to partial webpack config to be merged with angular's config. Example: extra-webpack.config.js | undefined |
ng serve options
Configuration options are provided to the architect.serve.options
section of your angular.json.
Name | Description | Default Value |
---|---|---|
singleSpaWebpackConfigPath | (optional) Path to partial webpack config to be merged with angular's config. Example: extra-webpack.config.js | undefined |
Use Custom Webpack
Starting with Angular 8, single-spa-angular's schematics install and use @angular-builders/custom-webpack
to modify the webpack config. The schematics also create an extra-webpack.config.js
file in your project where you can modify the configuration further.
The extra-webpack.config.js file should include the following:
const singleSpaAngularWebpack =
require("single-spa-angular/lib/webpack").default;
module.exports = (config, options) => {
const singleSpaWebpackConfig = singleSpaAngularWebpack(config, options);
// Feel free to modify this webpack config however you'd like to
return singleSpaWebpackConfig;
};
Older versions of single-spa-angular@3 and single-spa-angular@4 created extra-webpack.config.js files that did not pass options
into singleSpaAngularWebpack
. When you upgrade to newer versions, you'll need to pass in the options as shown above.
In addition to modifying the webpack config directly, you may alter some of single-spa-angular's behavior by changing the angular.json. Configuration options are provided to the architect.build.options.customWebpackConfig
section of your angular.json.
Name | Description | Default Value |
---|---|---|
path | (required) Path to the the above extra-webpack.config.js file. | N/A |
libraryName | (optional) Specify the name of the module | Angular CLI project name |
libraryTarget | (optional) The type of library to build see available options | "UMD" |
excludeAngularDependencies | (optional) Excludes Angular dependencies from the bundle by adding them to Webpack externals | false |
If you're using SystemJS, you may want to consider changing the webpack output.libraryTarget to be "system"
, for better interop with SystemJS.
Routing
Configure routes
To get single-spa working, you'll need to manually modify a few files.
- Add
providers: [{ provide: APP_BASE_HREF, useValue: '/' }]
toapp-routing.module.ts
. See angular docs for more details about APP_BASE_HREF. - Add
{ path: '**', component: EmptyRouteComponent }
to yourapp-routing.module.ts
routes. The EmptyRouteComponent is part of the single-spa-angular schematics. This route makes sure that when single-spa is transitioning between routes that your Angular application doesn't try to show a 404 page or throw an error. See angular docs for more details about routes. - Add a declaration for EmptyRouteComponent in
app.module.ts
. See angular docs for more details about app.module.ts.
APP_BASE_HREF should have the same value that the used url for mount the Angular app defined in the single-spa root application. But doing this causes strange behaviours in Angular Router when navigate between registered apps.
In order to avoid this is recommended using '/' as APP_BASE_HREF and repeat the url prefix for your Angular app in every route component and router links. If you set /angular in your Angular app activity function for mount when the url starts with this value you'll have to add /angular prefix in all links.
You can see several discussions about this issue in single-spa-angular GitHub repo: Router not working without APP_BASE_HREF and How to handle router links between different single-spa application subrouters
Linking between applications
To link between applications, simply use routerLink
like normal.
<a routerLink="/other-app"> Link to other app </a>
Nested routes
Nested routes work exactly the same as they normally do. To create a nested route, add it to your app-routing.module.ts.
To link to a nested route, use routerLink
the same way you normally do.
<a routerLink="/my-app/nested-route"> Link to nested route </a>
Enabling hash mode
You need to firstly enable hash mode in root-config.
If you are using layout html with single-spa-router
, add mode="hash"
<single-spa-router mode="hash"> ... </single-spa-router>
If you are registering each route manually, use location.hash
registerApplication({
name: "@orgName/app1",
app: () => System.import("@orgName/app1"),
activeWhen: (location) => location.hash.startsWith("#/app1"),
});
Then, enable hash mode in your routing module of angular micro frontend application.
@NgModule({
imports: [RouterModule.forRoot(routes, {useHash: true})],
exports: [RouterModule]
})
Serving
Run the following:
npm run serve:single-spa
This will not open up an HTML file, since single-spa applications all share one html file. Instead, go to http://single-spa-playground.org and follow the instructions there to verify everything is working and for instructions on creating the shared HTML file.
Building
Run npm run build:single-spa
, which will create a dist
directory with your compiled code.
In order for the webpack public path to be correctly set for your assets, you should use Angular CLI's --deploy-url
option. For more information, see this Stack Overflow answer which shows a few options for how to do that.
The single-spa helpers
Introduction
"single-spa helpers" refers to the in-browser portion of single-spa-angular. The helpers are used by all versions of Angular and regardless of whether you are using Angular CLI or not. This is the core of the single-spa-angular library that makes it possible for Angular applications to bootstrap, mount, and unmount. See single-spa lifecycles for more information.
Migrating from single-spa-angular@3.x to single-spa-angular@4.x
Migrating from 3.x to 4.x requires only few API updates.
Packages
npm install single-spa-angular@4.0.0
# Or if you're using yarn
yarn add single-spa-angular@4.0.0
# Or if you're using pnpm
pnpm install single-spa-angular@4.0.0
API Updates
single-spa-angular
doesn't have a default export anymore, instead you have to import a named singleSpaAngular
function. Given the following code:
import singleSpaAngular from "single-spa-angular"; // single-spa-angular@3.x
import { singleSpaAngular } from "single-spa-angular"; // single-spa-angular@4.x
Also, if your application uses routing then you have to import the getSingleSpaExtraProviders
function. Let's look at the following example, this is how it was in single-spa-angular@3.x
:
import { NgZone } from '@angular/core';
import { Router, NavigationStart } from '@angular/router';
import singleSpaAngular, { getSingleSpaExtraProviders } from 'single-spa-angular';
const lifecycles = singleSpaAngular({
bootstrapFunction: singleSpaProps => {
singleSpaPropsSubject.next(singleSpaProps);
return platformBrowserDynamic(getSingleSpaExtraProviders()).bootstrapModule(AppModule);
},
template: '<app-root />',
Router,
NavigationStart,
NgZone,
});
And this is how it should be in single-spa-angular@4.x
:
import { NgZone } from '@angular/core';
import { Router, NavigationStart } from '@angular/router';
import { singleSpaAngular, getSingleSpaExtraProviders } from 'single-spa-angular';
const lifecycles = singleSpaAngular({
bootstrapFunction: singleSpaProps => {
singleSpaPropsSubject.next(singleSpaProps);
return platformBrowserDynamic(getSingleSpaExtraProviders()).bootstrapModule(AppModule);
},
template: '<app-root />',
Router,
NavigationStart,
NgZone,
});
Basic usage
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { NgZone } from "@angular/core";
import { Router, NavigationStart } from "@angular/router";
import {
singleSpaAngular,
getSingleSpaExtraProviders,
} from "single-spa-angular";
import { AppModule } from "./app/app.module";
const lifecycles = singleSpaAngular({
bootstrapFunction: (singleSpaProps) => {
return platformBrowserDynamic(getSingleSpaExtraProviders()).bootstrapModule(
AppModule,
);
},
template: "<app-root />",
Router,
NavigationStart,
NgZone,
});
export const bootstrap = lifecycles.bootstrap;
export const mount = lifecycles.mount;
export const unmount = lifecycles.unmount;
Full Example
See this schematic file for a good example of how to use the single-spa helpers.
Options
Options are passed to single-spa-angular via the opts
parameter when calling singleSpaAngular(opts)
. This happens inside of your main.single-spa.ts
file.
The following options are available:
bootstrapFunction
: (required) A function that is given custom props as an argument and returns a promise that resolves with a resolved Angular module that is bootstrapped. Usually, your implementation will look like this:bootstrapFunction: (customProps) => platformBrowserDynamic().bootstrapModule()
. See custom props documentation for more info on the argument passed to the function.template
: (required) An HTML string that will be put into the DOM Element returned bydomElementGetter
. This template can be anything, but it is recommended that you keeping it simple by making it only one Angular component. For example,<app-root />
is recommended, but<div><app-root /><span>Hello</span><another-component /></div>
is allowed. Note thatinnerHTML
is used to put the template onto the DOM. Also note that when using multiple angular applications simultaneously, you will want to make sure that the component selectors provided are unique to avoid collisions. When migrating to single-spa, this template is what is inside of your index.html file's<body>
element.Router
: (optional) The angular router class. This is required when you are using@angular/router
.AnimationModule
: (optional) The animation module class. This is required when you are using BrowserAnimationsModule. Example way to import this:import { eAnimationEngine as AnimationModule } from '@angular/animations/browser';
. See Issue 48 for more details. Note that AnimationModule is no longer needed in Angular 12, so this option can be ignored in Angular >= 12.domElementGetter
: (optional) A function that takes in no arguments and returns a DOMElement. This dom element is where the Angular application will be bootstrapped, mounted, and unmounted. It's recommended to omit this and let single-spa-angular's defaults create and use a container div.
Concepts
ZoneJS
zone.js is the library that Angular uses for change detection. You absolutely must have exactly one instance of the zone.js library on the page. zone.js will throw errors if you have more than one instance of zone.js on the page.
The preferred way to ensure only one instance of zone.js is loaded on your page is with a script tag in your root-config's HTML file. You should load zone.js upfront a single time, before any of your microfrontends.
<script src="https://cdn.jsdelivr.net/npm/zone.js@0.10.3/dist/zone.min.js"></script>
The latest versions of zone.js
may not be published to a CDN. As such, you might need to import zone.js
in your root-config JS file:
async function start() {
await import("zone.js");
const { registerApplication } = await System.import("single-spa");
registerApplication(...);
}
start();
Note that having only one instance of zone.js is different than having only one zone within that instance. single-spa-angular automatically will ensure that each of your Angular applications has its own isolated, separate zone.
Multiple applications
When you have multiple apps running side by side, you'll need to make sure that their
component selectors are unique. When creating a new
project, you can have angular-cli do this for you by passing in the --prefix
option:
ng new --prefix app2
If you did not use the --prefix
option, you should set the prefix manually:
- For an application called app2, add
"prefix": "app2"
toprojects.app2
inside of the angular.json. - Go to
app.component.ts
. Modifyselector
to beapp2-root
. - Go to
main.single-spa.ts
. Modifytemplate
to be<app2-root>
.
Additionally, make sure that reflect-metadata
is only imported once in the root application and is not imported again in the child applications.
Otherwise, you might see an No NgModule metadata found
error.
See issue thread for more details.
Custom Props
Custom props are a way of passing auth or other data to your single-spa applications. The custom props are available inside of the bootstrapFunction passed to singleSpaAngular(). Additionally, if you use the angular cli schematic, you may subscribe to the singleSpaPropsSubject in your component, as shown below:
// An example showing where you get access to the single-spa props:
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { singleSpaAngular } from "single-spa-angular";
const lifecycles = singleSpaAngular({
bootstrapFunction(singleSpaProps) {
// Here are the custom props
console.log(singleSpaProps);
return platformBrowserDynamic().bootstrapModule(AppModule);
},
// add the other options to singleSpaAngular, too. See "Basic usage" for more info
});
// If you're using the singleSpaPropsSubject generated by the single-spa-angular schematics,
// here's an example component that uses the custom props
import { Component, OnInit, OnDestroy } from "@angular/core";
import { Subscription } from "rxjs";
import {
singleSpaPropsSubject,
SingleSpaProps,
} from "src/single-spa/single-spa-props";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"],
})
export class AppComponent implements OnInit, OnDestroy {
singleSpaProps: SingleSpaProps;
subscription: Subscription;
ngOnInit(): void {
this.subscription = singleSpaPropsSubject.subscribe(
(props) => (this.singleSpaProps = props),
);
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
// OR if you don't need to access `singleSpaProps` inside the component
// then create `Observable` property and use it in template with `async` pipe.
singleSpaProps$ = singleSpaPropsSubject.asObservable();
}
Angular assets
Angular assets are handled differently within single-spa than within
other Angular applications. The schematics file called asset-url.ts
helps you do load assets in a way that works both ways.
This won't work
// Doesn't work with single-spa
const imageUrl = "/assets/yoshi.png";
Do this instead
import { assetUrl } from "src/single-spa/asset-url";
// Works great with single-spa
const imageUrl = assetUrl("yoshi.png");
Within HTML templates
Option 1
Add the asset url to your component's class and reference it from the template. See here and here.
Option 2
Create an Angular Pipe that lets you calculate the asset url inside of an HTML template:
import { Pipe, PipeTransform } from "@angular/core";
import { assetUrl } from "src/single-spa/public-path";
@Pipe({ name: "assetUrl" })
export class AssetUrlPipe implements PipeTransform {
transform(value: string): string {
return assetUrl(value);
}
}
Then use it in your template:
<img [src]="'yoshi.png' | assetUrl" />
Scripts
Scripts in your angular.json are not loaded by single-spa. This is because single-spa applications have to all share an HTML file. (read more). You can remove the scripts from your angular.json because they have no impact on your single-spa build.
Option 1
Add the script tags directly into your root HTML file. This way is easiest. The downside is that all of the scripts get loaded even for routes that don't need them. However, that is generally okay and this is the preferred way to do it.
Option 2
If you want the scripts to only be loaded when needed, you can add a custom bootstrap lifecycle to your code.
Note that lazy loading these scripts can actually be worse for performance if you always need them, since they will start downloading later than if you put them right into the root HTML file.
// main.single-spa.ts
// Modify the bootstrap function like so
export const bootstrap = [
() =>
Promise.all([
loadScript(
"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js",
),
loadScript(
"https://cdnjs.cloudflare.com/ajax/libs/datepicker/0.6.5/datepicker.min.js",
),
]),
lifecycles.bootstrap,
];
function loadScript(url: string) {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = url;
function onLoad() {
resolve();
cleanup();
}
function onError(event: Event) {
reject(event);
cleanup();
}
function cleanup() {
script.removeEventListener("load", onLoad);
script.removeEventListener("error", onError);
}
script.addEventListener("load", onLoad);
script.addEventListener("error", onError);
document.head.appendChild(script);
});
}
Styles
Styles in your angular.json will automatically be loaded by single-spa-angular's webpack config, without you having to configure anything.
Your component styles will also be loaded like normal without you having to configure anything.
Unmountable styles
⚠️ This is an advanced part that requires knowledge of the Angular CLI Webpack configuration.
You can have unmountable <style>
elements that are removed from the DOM's <head>
whenever the app is destroyed. However, this can only be done by modifying the Webpack configuration and adding custom rules. We can't provide this out of the box because it would tie our configuration to Angular's Webpack configuration. If their config changes, ours would fail; thus, we shift the responsibility to the consumer.
Consider that we want to mount and unmount the main.scss
file, which is located near the main.single-spa.ts
file inside the src
folder. The style-loader
supports mounting and unmounting styles manually; see the docs. We need to find the rule that targets .scss
files using config.module.rules.find
. This will give us the following object:
{
test: /\.(?:scss)$/i,
rules: [
{
oneOf: []
},
{
use: [],
}
]
}
We have to update the first object in the rules
list; thus, we need to take rules[0]
. In the code below, we unshift the rule that uses Angular loaders for the scss
files first (the sass-loader
with options), and then we place the style-loader
with the lazySingletonStyleTag
injection type:
const singleSpaConfig = require("../../lib/lib/webpack");
module.exports = (config) => {
config = singleSpaConfig.default(config);
// https://github.com/angular/angular-cli/blob/b65ef44cbe36416271521eba0ba5fc0b4442af55/packages/angular_devkit/build_angular/src/tools/webpack/configs/styles.ts#L235-L255
const scssRule = config.module.rules.find((rule) =>
rule.test.toString().includes("scss"),
).rules[0];
scssRule.oneOf.unshift({
resourceQuery: /\?unmountable/,
use: [
{
loader: "style-loader",
options: {
injectType: "lazySingletonStyleTag",
},
},
// Take the other loaders that Angular uses internally.
// Since we replaced the `mini-css-extract-plugin` loader with the
// `style-loader` (which is at index 0), we slice the list by 1 index.
...scssRule.oneOf[0].use.slice(1),
],
});
return config;
};
Note the resourceQuery
. This will only target files that have the ?unmountable
query at the end. Next, let's update the main.single-spa.ts
file:
// @ts-ignore
import unmountableStyles from './main.scss?unmountable';
const lifecycles = singleSpaAngular({
bootstrapFunction: async () => {
unmountableStyles.use();
const appRef = await bootstrapApplication(AppComponent, appConfig);
appRef.onDestroy(() => unmountableStyles.unuse());
return appRef;
},
template: '<standalone-root />',
Router,
NavigationStart,
NgZone,
});
Polyfills
Polyfills in your angular.json are JavaScript code that make your project work in older browsers, such as IE11.
The polyfills that you specify in your angular.json file will not be loaded automatically. This is because we should only load polyfills once in the root HTML file, instead of once per application.
To load polyfills, you'll need to follow the instructions in the Angular documentation for non-CLI users. Even if you are using Angular CLI, you will need to follow those instructions, since your single-spa root HTML file is not using Angular CLI and that's where the polyfills need to go.
If you're looking for a quick one-liner, try adding this line near the top of your index.html.
<script src="https://unpkg.com/core-js-bundle/minified.js"></script>
To correct the error It looks like your application or one of its dependencies is using i18n
.
Install @angular/localize
in your root-config module
npm install @angular/localize
# Or if you're using yarn
yarn add @angular/localize
# Or if you're using pnpm
pnpm install @angular/localize
Add following import to your root-config.js
import "@angular/localize/init";
Internet Explorer
If you need to support IE11 or older, do the following:
- Add core-js polyfill
- Remove arrow functions from index.html (example)
- Change angular.json
target
toes5
(example)
Full example commit to get IE11 support
Shared Angular
Sharing one or more instances of Angular between microfrontends provides the following benefits:
- Performance improvement, due to reduced amount of javascript to load.
- Cross-microfrontend imports of angular components are possible. (Without a shared instance of Angular, you can still use cross-microfrontend imports of single-spa parcels)
There are two techniques for sharing Angular: SystemJS in-browser modules and Module Federation.
The below guide uses SystemJS for sharing dependencies.
⚠️ Angular 13 has few breaking changes. It dropped View Engine support, and this means there's a single template compiler right now (Ivy). They also introduced changes to the Angular Package Format. UMD bundles are no longer generated when building libraries. The
ng-packagr
now emits only ES2015 and ES2020 bundles. See this article for more info: https://blog.angular.io/angular-v13-is-now-available-cce66f7bc296.
You can use the following esm-bundle packages, which provide Ivy-compatible versions and can be shared via SystemJS. These packages are available in the repository provided below:
The single-spa-angular is also available in SystemJS format:
Let's imagine that we have 2 Angular applications and a root-config application. The Angular applications want to share dependencies. First, we need to exclude Angular dependencies from their bundles. This can be done using Webpack's externals
property, but single-spa-angular@6.2.0+
offers an option to exclude rxjs
, @angular/*
, and single-spa-angular/*
packages:
"build": {
"builder": "@angular-builders/custom-webpack:browser",
"options": {
"customWebpackConfig": {
"libraryTarget": "system",
"excludeAngularDependencies": true,
"path": "..."
}
}
}
Note that we set the libraryTarget
to system
and excludeAngularDependencies
to true
.
Next we need to replace enableProdMode
from @angular/core
with enableProdMode
from single-spa-angular
in our applications:
import { enableProdMode } from "single-spa-angular";
if (environment.production) {
enableProdMode();
}
This is because Angular's enableProdMode
throws an error if it is called multiple times. However, when dependencies are shared, it will indeed be called multiple times. single-spa-angular
calls the original enableProdMode
but suppresses the error.
We also need to consider the platform injector, which will be reused between applications. Angular creates a platform injector when it bootstraps an application; it is created only once and then reused. Each Angular application has its own platform injector when dependencies are not shared, but will use a single platform injector when dependencies are shared. This means that providedIn: 'platform'
services will be part of the single injector and thus shared between applications.
single-spa-angular
exports the getSingleSpaExtraProviders
function, which adds SingleSpaPlatformLocation
to the platform injector. This function should be called in each application, even if that application doesn't use routing. We also need to share the single-spa-angular
package because applications should reference the same SingleSpaPlatformLocation
class. Suppose app1
is created before app2
, and app1
doesn't call getSingleSpaExtraProviders()
when bootstrapping the root module, but app2
does. The platform injector is created eagerly when platformBrowser()
is called for the first time. Consequently, app2
will try to retrieve the SingleSpaPlatformLocation
, but it will not be available in the platform injector since app1
didn't declare this dependency.
The root-config should have a SystemJS import map with all of the required packages:
{
"imports": {
"app1": "http://localhost:4200/main.js",
"app2": "http://localhost:4201/main.js",
"single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/5.9.3/system/single-spa.dev.js",
"rxjs": "https://cdn.jsdelivr.net/npm/@esm-bundle/rxjs/system/es2015/rxjs.min.js",
"rxjs/operators": "https://cdn.jsdelivr.net/npm/@esm-bundle/rxjs/system/es2015/rxjs-operators.min.js",
"@angular/compiler": "https://cdn.jsdelivr.net/npm/@esm-bundle/angular/system/es2022/angular-compiler.js",
"@angular/core/primitives/signals": "https://cdn.jsdelivr.net/npm/@esm-bundle/angular/system/es2022/angular-core-primitives-signals.js",
"@angular/core/primitives/event-dispatch": "https://cdn.jsdelivr.net/npm/@esm-bundle/angular/system/es2022/angular-core-primitives-event-dispatch.js",
"@angular/core": "https://cdn.jsdelivr.net/npm/@esm-bundle/angular/system/es2022/angular-core.js",
"@angular/common": "https://cdn.jsdelivr.net/npm/@esm-bundle/angular/system/es2022/angular-common.js",
"@angular/common/http": "https://cdn.jsdelivr.net/npm/@esm-bundle/angular/system/es2022/angular-common-http.js",
"@angular/animations": "https://cdn.jsdelivr.net/npm/@esm-bundle/angular/system/es2022/angular-animations.js",
"@angular/animations/browser": "https://cdn.jsdelivr.net/npm/@esm-bundle/angular/system/es2022/angular-animations-browser.js",
"@angular/platform-browser": "https://cdn.jsdelivr.net/npm/@esm-bundle/angular/system/es2022/angular-platform-browser.js",
"@angular/platform-browser/animations": "https://cdn.jsdelivr.net/npm/@esm-bundle/angular/system/es2022/angular-platform-browser-animations.js",
"@angular/platform-browser-dynamic": "https://cdn.jsdelivr.net/npm/@esm-bundle/angular/system/es2022/angular-platform-browser-dynamic.js",
"@angular/router": "https://cdn.jsdelivr.net/npm/@esm-bundle/angular/system/es2022/angular-router.js",
"single-spa-angular/internals": "https://cdn.jsdelivr.net/npm/@esm-bundle/single-spa-angular@9.1.2/system/es2022/single-spa-angular-internals.js",
"single-spa-angular": "https://cdn.jsdelivr.net/npm/@esm-bundle/single-spa-angular@9.1.2/system/es2022/single-spa-angular.js"
}
}
Production bundles also don't use the @angular/compiler
and @angular/platform-browser-dynamic
packages. You should use minified packages when the root configuration is built in production mode. One approach is to use if-else
conditions within the root configuration .ejs
template:
<% if (htmlWebpackPlugin.options.isDevelopment) { %>
<script type="systemjs-importmap">
{
"imports": {
"@angular/compiler": "..."
}
}
</script>
<%} else { %>
<script type="systemjs-importmap">
{
"imports": {
"@angular/core": "angular-core.min.js"
}
}
</script>
<% } %>
Note: the isDevelopment
can be provided when creating the HtmlWebpackPlugin
:
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = (env, { mode }) => {
const isDevelopment = mode !== "production";
return {
plugins: [
new HtmlWebpackPlugin({
template: "...",
isDevelopment,
}),
],
};
};
The mode
will be set to production when you run webpack --mode production
.
The root configuration can then load single-spa
, register applications, and start single-spa
so that the applications can be mounted:
System.import("single-spa").then(({ registerApplication, start }) => {
registerApplication({
name: "app1",
app: () => System.import("app1"),
activeWhen: (location) => location.pathname.startsWith("/app1"),
});
registerApplication({
name: "app2",
app: () => System.import("app2"),
activeWhen: (location) => location.pathname.startsWith("/app2"),
});
start();
});
Angular Elements
This feature is available starting from single-spa-angular@4.4.0
. You also may need to become familiar with Angular Elements documentation.
Let's start with installing the @angular/elements
:
npm install @angular/elements
# Or if you're using yarn
yarn add @angular/elements
# Or if you're using pnpm
pnpm install @angular/elements
The next step is to edit main.single-spa.ts
:
import { enableProdMode } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { singleSpaAngularElements } from "single-spa-angular/elements";
import { AppModule } from "./app/app.module";
import { environment } from "./environments/environment";
if (environment.production) {
enableProdMode();
}
const lifecycles = singleSpaAngularElements({
template: "<app-custom-element />",
// We can actually not rely on the `zone.js` library, our custom element
// will behave itself as a zone-less application.
bootstrapFunction: () =>
platformBrowserDynamic().bootstrapModule(AppModule, { ngZone: "noop" }),
});
export const bootstrap = lifecycles.bootstrap;
export const mount = lifecycles.mount;
export const unmount = lifecycles.unmount;
Note that the app-custom-element
selector will be used when defining our custom element.
After that, you'll have to edit app.module.ts
and define a custom tag:
import { NgModule, Injector, DoBootstrap } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { createCustomElement } from "@angular/elements";
import { AppComponent } from "./app.component";
@NgModule({
imports: [BrowserModule],
declarations: [AppComponent],
})
export class AppModule implements DoBootstrap {
constructor(private injector: Injector) {}
ngDoBootstrap(): void {
customElements.define(
// This tag we've have provided in `options.template` when called `singleSpaAngularElements`.
"app-custom-element",
createCustomElement(AppComponent, { injector: this.injector }),
);
}
}
The following options are available to be passed when calling singleSpaAngularElements
:
bootstrapFunction
(required)template
(required)domElementGetter
(optional)
See options for detailed explanation.
Parcels
We encourage you to get familiar with the documentation, namely Parcels overview and Parcels API. This documentation will give you a basic understanding of what parcels are.
Additionally, single-spa-angular provides a <parcel>
component to make using framework agnostic single-spa parcels easier. This allows you to put the parcel into your component's template, instead of calling mountRootParcel()
by yourselves.
single-spa-angular/parcel
package exports the ParcelModule
which exports the <parcel>
component:
// Inside of src/app/app.module.ts
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { ParcelModule } from "single-spa-angular/parcel";
import { AppComponent } from "./app.component";
@NgModule({
imports: [BrowserModule, ParcelModule],
declarations: [AppComponent],
bootstrap: [AppComponent],
})
export class AppModule {}
The example below shows how you can render React parcels:
// Inside of src/app/app.component.ts
import { ChangeDetectionStrategy, Component } from "@angular/core";
import { mountRootParcel } from "single-spa";
import { config } from "./ReactWidget/ReactWidget";
@Component({
selector: "app-root",
template:
'<parcel [config]="config" [mountParcel]="mountRootParcel"></parcel>',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
config = config;
mountRootParcel = mountRootParcel;
}
For React, you will need to create a file with the extension .tsx
:
// Inside of src/app/ReactWidget/ReactWidget.tsx
import * as React from "react";
import * as ReactDOM from "react-dom";
import singleSpaReact from "single-spa-react";
const ReactWidget = () => <div>Hello from React!</div>;
export const config = singleSpaReact({
React,
ReactDOM,
rootComponent: ReactWidget,
});
Loading External Components Asynchronously
If your code is part of an import map and you include a global reference to
systemjs, you can dynamically import the code using an async
method.
For example, if your import map includes the ReactComponent
.
{
imports: {
"@org/react-component": '/org-react-component.js'
}
}
You can dynamically load the component by setting the config
as an
asynchronous method that fetches the component.
Since some versions of webpack use SystemJS under the hood, you'll need to reference the global version.
// Inside of src/app/app.component.ts
import { ChangeDetectionStrategy, Component } from "@angular/core";
import { mountRootParcel } from "single-spa";
@Component({
selector: "app-root",
template:
'<parcel [config]="config" [mountParcel]="mountRootParcel"></parcel>',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
async config() {
return window.System.import("@org/react-component");
}
mountRootParcel = mountRootParcel;
}
Passing Custom Props
You can pass any custom props to the parcel by passing an object of props using
the customProps
attribute.
For example, if you're rendering a React parcel from an Angular component, you can pass a click handler from Angular into the React parcel:
// Inside of src/app/ReactWidget/ReactWidget.tsx
import * as React from "react";
import * as ReactDOM from "react-dom";
import singleSpaReact from "single-spa-react";
const ReactWidget = ({ handleClick }) => (
<button onClick={handleClick}>Click Me!</button>
);
export const parcelConfig = singleSpaReact({
React,
ReactDOM,
rootComponent: ReactWidget,
});
You can pass a function (or any other value) as a custom prop. To ensure that the functions you pass to the parcel are bound with the correct javascript context, use the handleClick = () => {
syntax when defining your functions.
// Inside of src/app/app.component.ts
import { ChangeDetectionStrategy, Component } from "@angular/core";
import { mountRootParcel } from "single-spa";
import { config } from "./ReactWidget/ReactWidget";
@Component({
selector: "app-root",
template:
'<parcel [config]="config" [customProps]="parcelProps" [mountParcel]="mountRootParcel"></parcel>',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
config = config;
mountRootParcel = mountRootParcel;
handleClick = () => {
alert("Hello World");
};
parcelProps = {
handleClick = this.handleClick,
};
}
Zone-less applications
This feature is available starting from single-spa-angular@4.1
.
It's possible to develop Angular applications that don't rely on zone.js
library, these applications are called zone-less. You have to run change detection manually in zone-less applications through ApplicationRef.tick()
or ChangeDetectorRef.detectChanges()
. You can find more info in Angular NoopZone docs.
The point is that you do not need to load zone.js
library in your root HTML file. As Angular docs mention that you should have a comprehensive knowledge of change detection to develop such applications. Let's start by nooping zone when bootstrapping module:
import { singleSpaAngular } from "single-spa-angular";
const lifecycles = singleSpaAngular({
bootstrapFunction: () =>
platformBrowserDynamic().bootstrapModule(AppModule, { ngZone: "noop" }),
template: "<app-root />",
NgZone: "noop",
});
We must specify noop
twice: when bootstrapping AppModule
, and setting NgZone
property to noop
. This tells Angular and single-spa-angular that we're not going to use zones.
Routing in zone-less applications
Since routing is managed by single-spa and there is no zone that tells Angular that some asynchronous event has occured, then we need to tell Angular when to run change detection if routing occurs. Let's look at the below code:
import { ApplicationRef } from "@angular/core";
import { Router, NavigationStart } from "@angular/router";
import { singleSpaAngular } from "single-spa-angular";
const lifecycles = singleSpaAngular({
bootstrapFunction: async () => {
const ngModuleRef = await platformBrowserDynamic().bootstrapModule(
AppModule,
{ ngZone: "noop" },
);
const appRef = ngModuleRef.injector.get(ApplicationRef);
const listener = () => appRef.tick();
window.addEventListener("popstate", listener);
ngModuleRef.onDestroy(() => {
window.removeEventListener("popstate", listener);
});
return ngModuleRef;
},
template: "<app-root />",
NgZone: "noop",
Router,
NavigationStart,
});
single-spa-angular@4.x
requires calling getSingleSpaExtraProviders
function in applications that have routing. Do not call this function in zone-less application.
Inter-app communication via RxJS
First of all, check out this Inter-app communication guide.
It's possible to setup a communication between microfrontends via RxJS using cross microfrontend imports.
We can not create complex abstractions, but simply export the Subject
:
// Inside of @org/api
import { ReplaySubject } from "rxjs";
import { User } from "@org/models";
// `1` means that we want to buffer the last emitted value
export const userSubject$ = new ReplaySubject<User>(1);
And then you just need to import this Subject
into the microfrontend application:
// Inside of @org/app1 single-spa application
import { userSubject$ } from "@org/api";
import { User } from "@org/models";
userSubject$.subscribe((user) => {
// ...
});
userSubject$.next(newUser);
Also, you should remember that @org/api
should be an "isolated" dependency, for example the Nrwl Nx library, where each library is in the "libs" folder and you import it via TypeScript paths.
Every application that uses this library should add it to its Webpack config as an external dependency:
const singleSpaAngularWebpack =
require("single-spa-angular/lib/webpack").default;
module.exports = (config, options) => {
const singleSpaWebpackConfig = singleSpaAngularWebpack(config, options);
singleSpaWebpackConfig.externals = [/^@org\/api$/];
return singleSpaWebpackConfig;
};
But this library should be part of root application bundle and shared with import maps, for example:
{
"imports": {
"@org/api": "http://localhost:8080/api.js"
}
}