Skip to content

Latest commit

 

History

History

README.md

ng-xtend logo Dynamic Plugin Example

This example shows how to load plugins at runtime from a remote server using Native Federation. Instead of bundling xt-plugin-intl and xt-plugin-finance with the application, they are fetched from a remote URL on startup.


Step 1: Remove static plugin dependencies

Open package.json and compare with the store example — the xt-plugin-intl, xt-plugin-finance, and countries-ts packages are not listed here. They will be loaded dynamically instead.

The dynamic example adds:

"dependencies": {
  "@softarc/native-federation-runtime": "3.3.6",
  "@angular-architects/native-federation": "20.1.7",
  "es-module-shims": "2.6.2"
}

These packages enable Module Federation at the application level, allowing the Angular build to import remote modules that were not present at compile time.


Step 2: Configure Native Federation

federation.config.js tells the build system which libraries to share between the host and remote plugins:

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

module.exports = withNativeFederation({
  shared: {
    ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
  },
  skip: ['chart.js/auto', 'primeng/chart', 'primeicons']
});

shareAll ensures that Angular, ng-xtend libraries, and other core dependencies are loaded as singletons — the host and plugins share the same instance rather than each bundling their own copy. This is required for plugins to seamlessly integrate into the host's rendering pipeline.


Step 3: Use bootstrap.ts for async initialisation

Native Federation requires asynchronous bootstrapping because the remote plugin manifests must be fetched before Angular starts. The flow in src/main.ts:

import { initFederation } from '@angular-architects/native-federation';

initFederation()
  .catch(err => console.error(err))
  .then(_ => import('./bootstrap'))
  .catch(err => console.error(err));

initFederation() fetches the remote federation manifests, then bootstrap.ts kicks off the regular Angular bootstrapping. This is the only structural change required — the rest of the application code is identical to the store example.


Step 4: Load plugins dynamically

In src/app/app.ts, the default plugin is still registered statically (primitives must always be available), but the international and finance plugins are loaded at runtime:

import {ConfigManagerService} from '../config-manager/config-manager.service';

export class App {
  protected readonly configService = inject(ConfigManagerService);

  constructor() {
    registerDefaultPlugin(this.resolverService);

    this.configService.loadConfig(
      [{
        plugin: 'International Plugin',
        url: 'https://test.dont-code.net/apps/latest/intl-plugin/remoteEntry.json'
      }, {
        plugin: 'Finance Plugin',
        url: 'https://test.dont-code.net/apps/latest/finance-plugin/remoteEntry.json'
      }],
      {
        buyType: { on: 'date', at: 'string', price: 'money-amount' },
        'Example Book': {
          bookName: 'string', author: 'string',
          nationality: 'country', bought: 'buyType', read: 'boolean'
        }
      }
    );
  }
}

Why a ConfigManagerService? The ConfigManagerService orchestrates loading order — plugins must be registered before types are declared, because type registration references plugin-provided type names like 'country' and 'money-amount'. If types were registered first, the resolver would fail to find components for those types.

// Inside ConfigManagerService
loadConfig(pluginConfig, types) {
  this.loadPlugins(pluginConfig).then(() => {
    this.resolverService.registerTypes(types);
    this.configLoaded.set(true);
  }).catch((err) => {
    this.errorMsg.set(err.toString());
  });
}

Step 5: Handle loading state in the template

Since plugins arrive asynchronously, the template (src/dynamic-display/dynamic-display.html) has three states:

@if (config.configLoaded()) {
  <!-- Plugins loaded — render normally -->
  <xt-render displayMode="LIST_VIEW" valueType="Example Book" ...>
} @else if (config.errorMsg() != null) {
  <!-- Plugin loading failed -->
  <h2>Error loading plugins</h2>
  <span>{{config.errorMsg()}}</span>
} @else {
  <!-- Still loading -->
  <h2>Loading plugins...</h2>
}

The DynamicDisplay component also uses linkedSignal for the form, so it only builds the form group once configLoaded is true.


Summary

Concept What changes
Native Federation Enables loading Angular modules (plugins) from remote URLs at runtime
federation.config.js Declares shared singleton dependencies between host and plugins
initFederation() + bootstrap.ts Async bootstrap required before Angular can start
ConfigManagerService Orchestrates plugin loading → type registration → UI readiness
Loading/error states Template handles the async lifecycle — loading, ready, or error
Plugin independence Remote plugins can be updated and deployed without rebuilding the host application