Building A Plugin-based Workflow Designer With Angular and Module Federation

The Microfrontend Revolution, Part 4

This article is part of a series:


In the previous article of this series, I showed how to use Dynamic Module Federation. This allows us to load micro frontends -- or remotes, which is the more general term in Module Federation -- not known at compile time. We don't even need to know the number of remotes upfront.

While the previous article leveraged the router for integrating remotes available, this article shows how to load individual components. The example used for this is a simple plugin-based workflow designer.

The Workflow Designer can load separately compiled and deployed tasks

The workflow designer acts as a so-called host loading tasks from plugins provided as remotes. Thus, they can be compiled and deployed individually. After starting the workflow designer, it gets a configuration describing the available plugins:

The configuration informs about where to find the tasks

Please note that these plugins are provided via different origins (http://localhost:3000 and http://localhost:3001), and the workflow designer is served from an origin of its own (http://localhost:5000).

As always, the source code can be found in my GitHub account.

Thanks to Zack Jackson and Jack Herrington, who helped me to understand the rater new API for Dynamic Module Federation.

Disclaimer: Module Federation is a brand-new technology that will come with webpack 5. As webpack 5 is currently in beta, it's not intended for production yet. Also, to make it work with Angular already today, my examples use a patched version of the Angular CLI and a custom webpack configuration. Once webpack 5 is final and the Angular CLI supports it, we won't need these workarounds anymore but have a more streamlined way for all of this. Nevertheless, investigating this technology already today gives us a sound idea of what's possible shortly.

Building the Plugins

The plugins are provided via separate Angular applications. For the sake of simplicity, all applications are part of the same monorepo. Their webpack configuration uses Module Federation for exposing the individual plugins as shown in the previous articles of this series:

new ModuleFederationPlugin({
  name: "mfe1",
  library: { type: "var", name: "mfe1" },
  filename: "remoteEntry.js",
  exposes: {
    './Download': './projects/mfe1/src/app/download.component.ts',
    './Upload': './projects/mfe1/src/app/upload.component.ts'
  shared: ["@angular/core", "@angular/common", "@angular/router"]

As also discussed in the previous articles, this configuration assigns the (container) name mfe1 to the remote. It shares the libraries @angular/core, @angular/common, and @angular/router with both, the host (=the workflow designer) and the remotes. Besides, it exposes a remote entry point remoteEntry.js which provides the host with the necessary key data for loading the remote.

Loading the Plugins into the Workflow Designer

For loading the plugins into the workflow designer, I'm using the helper function loadRemoteModule described in the previous article:

export type LoadRemoteModuleOptions = { 
    remoteEntry: string; 
    remoteName: string; 
    exposedModule: string

export async function loadRemoteModule(options: LoadRemoteModuleOptions): Promise<any> {

To load the above mentioned Download plugin, loadRemoteModule can be called this way:

const component = await loadRemoteModule({
    remoteEntry: 'http://localhost:3000/remoteEntry.js',
    remoteName: 'mfe1',
    exposedModule: './Download'

Providing Metadata About the Plugins

At runtime, we need to provide the workflow designer with key data about the plugins. The type used for this is called PluginOptions and extends the LoadRemoteModuleOptions shown in the previous section by a displayName and a componentName:

export type PluginOptions = LoadRemoteModuleOptions & {
    displayName: string;
    componentName: string;

While the displayName is the name presented to the user, the componentName refers to the TypeScript class representing the Angular component in question.

For loading this key data, the workflow designer leverages a LookupService:

@Injectable({ providedIn: 'root' })
export class LookupService {
    lookup(): Promise<PluginOptions[]> {
        return Promise.resolve([
                remoteEntry: 'http://localhost:3000/remoteEntry.js',
                remoteName: 'mfe1',
                exposedModule: './Download',

                displayName: 'Download',
                componentName: 'DownloadComponent'
        ] as PluginOptions[]);

For the sake of simplicity, the LookupService provides some hardcoded entries. In the real world, it would very likely request this data from a respective HTTP endpoint.

Dynamically Creating the Plugin Component

The workflow designer represents the plugins with a PluginProxyComponent. It takes a PluginOptions object via an input, loads the described plugin via Dynamic Module Federation and displays the plugin's component within a placeholder:

    selector: 'plugin-proxy',
    template: `
        <ng-container #placeHolder></ng-container>
export class PluginProxyComponent implements OnChanges {
    @ViewChild('placeHolder', { read: ViewContainerRef, static: true })
    viewContainer: ViewContainerRef;

      private injector: Injector,
      private cfr: ComponentFactoryResolver) { }

    @Input() options: PluginOptions;

    async ngOnChanges() {

        const component = await loadRemoteModule(this.options)
            .then(m => m[this.options.componentName]);

        const factory = this.cfr.resolveComponentFactory(component);

        this.viewContainer.createComponent(factory, null, this.injector);

Wiring Up Everything

Now, it's time to wire up the parts mentioned above. For this, the workflow designer's AppComponent gets a plugins and a workflow array. The first one represents the PluginOptions of the available plugins and thus all available tasks while the second one describes the PluginOptions of the selected tasks in the configured sequence:

@Component({ [...] })
export class AppComponent implements OnInit {

  plugins: PluginOptions[] = [];
  workflow: PluginOptions[] = [];
  showConfig = false;

    private lookupService: LookupService) {

  async ngOnInit(): Promise<void> {
    this.plugins = await this.lookupService.lookup();

  add(plugin: PluginOptions): void {

  toggle(): void {
    this.showConfig = !this.showConfig;

The AppComponent uses the injected LookupService for populating its plugins array. When a plugin is added to the workflow, the add method puts its PluginOptions object into the workflow array.

For displaying the workflow, the designer just iterates all items in the workflow array and creates a plugin-proxy for them:

<ng-container *ngFor="let p of workflow; let last = last">
    <plugin-proxy [options]="p"></plugin-proxy>
    <i *ngIf="!last" class="arrow right" style=""></i>

As discussed above, the proxy loads the plugin (at least, if it isn't already loaded) and displays it.

Also, for rendering the toolbox displayed on the left, it goes through all entries in the plugins array. For each of them it displays a hyperlink calling bound to the add method:

<div class="vertical-menu">
    <a href="#" class="active">Tasks</a>
    <a *ngFor="let p of plugins" (click)="add(p)">Add </a>

At runtime, we can add tasks now to our workflow. When looking into the browser's dev tools, we see that the application loades the plugins from their very own origin on demand:

Plugins are loaded on demand from their origin


While Module Federation comes in handy for implementing micro frontends, it can also be used for setting up plugin architectures. This allows us to extend an existing solution by 3rd parties. It also seems to be a good fit for SaaS applications, which needs to be adapted to different customers' needs.

Don't Miss Anything!

Subscribe to our newsletter to get all the information about Angular.

* By subscribing to our newsletter, you agree with our privacy policy.

Current Blog Articles

Only One Step Away!

Send us your inquery today - we help you with pleasure!

Jetzt anfragen!