Turning an Angular app to a HV module¶
Dependencies¶
Required additional packages besides the ones that Angular installs on it's own:
@angular-architects/module-federation@babel/cli@babel/core@babel/preset-envcss-loadermini-css-extract-pluginngx-build-pluspostcss-loadersass-loaderstyle-loaderwebpackwebpack-cliwebpack-manifest-plugin
Run the following command to install dependencies:
yarn add -D @angular-architects/module-federation @babel/cli @babel/core \
@babel/preset-env css-loader mini-css-extract-plugin ngx-build-plus \
postcss-loader sass-loader style-loader webpack webpack-cli webpack-manifest-plugin
angular.json Configuration¶
In order to build a federated module with Angular, you need to use a custom webpack config in your Angular build target. To do that, you have to add extraWebpackConfig property to:
{
...
"projects
"base-ng-hv-module": {
...
"architect": {
...
"build": {
...
"options": {
"main": "src/main.ts", // you have to rename the browser property to main
...
"extraWebpackConfig": "webpack.config.js" // either here
},
"configurations": {
"production": {
...
"extraWebpackConfig": "webpack.config.js" // or here
}
}
}
}
}
}
It’s also necessary to use the ngx-build-plus:browser builder instead of the default @angular-devkit/build-angular:application.
webpack.config.js Configuration¶
Let’s take care of the object that you’ll assign to module.exports:
You have to set the library to be a dynamic module and the target has to be set to window.
These setting below ensure that it’ll be sufficient to import your remoteEntry.js (or whatever your main file name is going to be) file to HortiView in order to run the module
Make sure your styles are built into a single file that has a consistent name. Later on you’ll have to add it to the document manually in the loadApp.ts file.
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
...
plugins: [
...
new MiniCssExtractPlugin({
filename: 'styles.css',
})
],
};
Here’s how to fill out the ModuleFederationPlugin object. If you’re importing any modules to your application, make sure to add them into the share object parameter that’s assigned to shared property, otherwise it might not end up built with the rest of the module.
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const {share} = require("@angular-architects/module-federation/webpack");
const mf = require("@angular-architects/module-federation/webpack");
const sharedMappings = new mf.SharedMappings();
sharedMappings.register(
path.join(__dirname, 'tsconfig.json'),
[]);
const wmfSharedConfig = { singleton: true, strictVersion: true, eager: true, requiredVersion: 'auto' };
module.exports = {
...
plugins: [
...
new ModuleFederationPlugin({
name: "base-ng-hv-module", // not relevant, can be anything
filename: "remoteEntry.js", // has to be the same as
library: {
type: 'window', // has to be “window” because of how HV is running modules
name: 'base-ng-hv-module' // has to be consistent with xxx
},
exposes: {
'./BaseNgHvModule': './src/loadApp.ts', // ./BaseNgHvModule is the exposed component and ./src/loadApp.ts will be the entry file
},
shared: share({
"@angular/common": { ...wmfSharedConfig },
"@angular/common/http": { ...wmfSharedConfig },
"@angular/core": { ...wmfSharedConfig },
"@angular/router": { ...wmfSharedConfig },
...sharedMappings.getDescriptors()
})
}),
],
};
src/loadApp.ts¶
This file is exposed in the webpack config in ModuleFederationPlugin. It's goal is to expose a mount(e?: DynamicComponentCustomProps) function that will be executed by HortiView once the module code loads. It's main goal is to perform anything that Angular (and your application) has to do at the very beginning. It certainly should contain whatever normally is in the src/main.ts file, which is enabling the production mode and bootstrapping your main module or standalone component:
export function mount(e?: DynamicComponentCustomProps):void {
enableProdMode();
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));
};
The value provided to the function is worth saving in a global variable or something else where it will be easily accessible. token property's value will be used to authenticate against HortiView's API.
interface DynamicComponentCustomProps {
token?: string;
organizationId?: string;
basePath?: string;
}
export var customProps: DynamicComponentCustomProps | undefined;
export const mount = (e?: DynamicComponentCustomProps) => {
customProps = e;
enableProdMode();
// If you're dealing with singleton components
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));
// If you have a main module
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
}
Remember to add the file to TS compilation:
Routing¶
Routing will not be able to work out of the box in HortiView, because it's going to add some segments to the URL before getting to your module's route. To allow Angular work with routing properly you have to create a base element and set it's href to whatever was delivered to the mount function as a parameter in the basePath property (so it's best to include that code in the mount function):
const href = e?.basePath ?? '';
const baseElements = document.getElementsByTagName('base');
if (baseElements.length) {
baseElements[0].setAttribute('href', href);
} else {
const base = document.createElement('base');
base.setAttribute('href', href);
document.head.prepend(base);
}
Switching routes itself works just as it works normally in Angular.
Adding styles¶
HortiView won't automatically add styling from your output folder. You can add them in the loadApp.ts file either in mount function or outside it.
// Get the url to your build folder
let scripts = document.getElementsByTagName('script');
let path = scripts[scripts.length - 1].src.split('?')[0];
export const mainDir = path.split('/').slice(0, -1).join('/');
// Add the styles.css file to the document
const styleElement = document.createElement('link');
styleElement.href = mainDir + '/styles.css';
styleElement.rel = 'stylesheet';
Running as a standalone¶
This setup and the aforementioned build target won’t let you run the application without HortiView, you’ll have to create separate build targets for that. You can use those that were generated by Angular by default.
{
...
"projects": {
"base-ng-hv-module": {
...
"architect": {
"build_standalone": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/base-ng-hv-module",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "base-ng-hv-module:build_standalone:production"
},
"development": {
"buildTarget": "base-ng-hv-module:build_standalone:development"
}
},
"defaultConfiguration": "development",
"options": {
"port": 3002,
"publicHost": "http://localhost:3002"
}
}
}
}
}
}
ng serve will run the application as normally.