Skip to content

Latest commit

 

History

History

angular-react-vue

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

Static Module Federation

Angular result example:

Angular result application

Init

application-name = angular-app | vue-app | react-app

Repeat in 3 terminals:

yarn:

cd `application-name`
yarn
yarn serve

npm:

cd `application-name`
npm i
npm run serve

apps will be served on:


Conclusion

  • React works very bad with imported Web Components, you can't listen custom events with onEvent template listeners. You can only use this way, but reactive prop re-render inside web component will be lost:

    only prop binding

    <vue-header title={wcTitle} />

    or eventListener with no reactivity

    const myEl = document.createElement('my-element');
    myEl.prop = 'hello';
    // or
    myEl.setAttribute('prop', 'hello');
    myEl.addEventListener('customEvent' func);
  • Angular and Vue works nice with Web Components, there are easy ways to pass props and listen to events:

    Angular:

    <vue-header [title]="wcTitle" (headerclick)="log($event)"></vue-header>

    Vue:

    <ng-header :title="wcTitle" @headerclick="log($event)"></ng-header>
  • Angular provides awesome api with detectChanges() function, so you can mutate your component on fly

Strange things / Unexpected behavior

  • To create Angular Web Component, you must import zone inside utils file. Also this behavior may be evaded. Check inside utils file.
  • To render React Element, you must import React inside utils file.
  • In Vue component, to use both Web Components and native render you must copy-paste your styles from <style> tag into styles[]

Exporting elements with Module Federation to foreign framework:

✅ - Ok

⚠️ - Input prop can be set only when rendered first time

Angular
type Render Input Output
Native
Web Components

with detectChanges() you can update your inputs on hot

Vue:
type Render Input Output
Native ⚠️
Web Components
React
type Render Input Output
Native ⚠️
Web Components

Web Components from React seems to work ok, but it's tricky way to implement this


How to implement

  1. Init apps

  2. Add some shared components

  3. Setup webpack

    Plugin config example:

    new ModuleFederationPlugin({
      name: 'vueApp',
      filename: 'remoteEntry.js',
      // elements that we importing
      remotes: {
        angularApp: 'angularApp@http://localhost:4201/remoteEntry.js',
        reactApp: 'reactApp@http://localhost:3001/remoteEntry.js',
      },
      // elements that we exporting
      exposes: {
        './Header': './src/components/Header',
        './utils': './src/utils',
      },
      shared: require('./package.json').dependencies,
    }),
    3.1 Angular:
    ng add @angular-architects/module-federation

    Select project if monorepo. All files will be created 😎, just edit webpack.config.js to configure ModuleFederationPlugin

    3.2 Vue:
    1. Install necessary packages
    yarn add webpack @vue/[email protected] @vue/[email protected] -D
    

    or

    npm i webpack @vue/[email protected] @vue/[email protected] -D

    If you use PWA, also install @vue/[email protected]

    1. Create vue.config.js that exports webpack configuration with ModuleFederationPlugin setup

    2. Create asynchronous boundary

    3.3 React:
    1. Update necessary packages
    npm i -D webpack webpack-cli webpack-server html-webpack-plugin webpack-dev-server
    npm i -D bundle-loader babel-loader @babel/preset-react @babel/preset-typescript

    or

    yarn add -D webpack webpack-cli webpack-server html-webpack-plugin webpack-dev-server
    yarn add -D bundle-loader babel-loader @babel/preset-react @babel/preset-typescript
    1. Create webpack.config.js with ModuleFederationPlugin setup.

    2. Create asynchronous boundary

    3. Add new scripts to your package.json

    "scripts": {
      "serve": "webpack-cli serve",
      "build": "webpack --mode production",
      "serve-build": "serve dist -p 3001"
    },
  4. Export components

    4.1 Angular:

    Using Angular render function:

    1. Create exporting file like utils.ts

    2. renderAngularComponent function creates inside self empty ngModule with ngDoBootstrap function that bootstraps passed component to passed DOM selector. This module have to be bootstrapped with platformBrowserDynamic fn:

    export const renderAngularComponent: IRenderAngularComponent = ({
      AngularComponent,
      selector,
    }) => {
      let componentRef: ComponentRef<typeof AngularComponent>;
    
      @NgModule({ imports: [BrowserModule] })
      class EmptyModule implements DoBootstrap {
        ngDoBootstrap(appRef: ApplicationRef) {
          componentRef = appRef.bootstrap(AngularComponent, selector);
        }
      }
    
      return platformBrowserDynamic()
        .bootstrapModule(EmptyModule)
        .then((props) => {
          const newProps = { ...props, componentRef } as TRenderReturn;
          return newProps;
        });
    };
    1. Function usage:
    renderAngularComponent({
      AngularComponent: AngularHeader,
      selector: '#ng-header',
    }).then(({ componentRef }) => {
      componentRef.instance.title = 'custom title';
      componentRef.changeDetectorRef.detectChanges();
    
      const sub = componentRef.instance.headerclick.subscribe(console.log);
      // also you can use unsubscribe function
      // sub.unsubscribe();
    });

    Using Web Components:

    1. Add @angular/elements
    ng add @angular/elements
    1. Create exporting file like utils.ts

    2. Add your file to ts compilation

    if you use some utils file to define helper functions, make sure, that inside your tsconfig.app.json file added new .ts files to prevent error "{file} is missing from the TypeScript compilation":

    tsconfig.app.json

    "files": [
      ...
      "src/utils.ts"
    ],
    1. Create empty module to define components by using platformBrowserDynamic fn
    @NgModule({ imports: [BrowserModule] })
    class EmptyModule implements DoBootstrap {
      ngDoBootstrap(appRef: ApplicationRef) {}
    }
    1. Export custom element define function
    export const defineAngularWebComponent = ({
      AngularComponent,
      name,
    }: {
      AngularComponent: Type<any>;
      name: string;
    }) => {
      platformBrowserDynamic()
        .bootstrapModule(EmptyModule)
        .then(({ injector }) => {
          const angularEl = createCustomElement(AngularComponent, { injector });
          customElements.define(name, angularEl);
        });
    };
    1. Pass AngularComponent and tag-name to defineAngularWebComponent. and use your tag-name inside html
    4.2 Vue:

    Using Web Components:

    If you want to use WC, your styles must be declared inside component styles array. Example.

    1. Inside utils.js define function
    export const defineVueWebComponent = ({ VueElement, name }) => {
      const customEl = defineCustomElement(VueElement);
      customElements.define(name, customEl);
    };
    1. Pass VueElement and tag-name to defineVueWebComponent. And use your tag-name inside html

    Using Vue render function:

    If you want to use Native mounting, your styles must be declared inside <style> tag. Example.

    1. Inside utils.js define function
    export const renderVueElement = ({ VueElement, selector, props }) => {
      const vueApp = createApp(VueElement, props);
      return { vueApp, vueEl: vueApp.mount(selector) };
    };

    return only for get more flexability

    1. Pass VueElement, DOM selector and props to renderVueElement. It will be rendered inside DOM selector

    also event listeners may be passed with converted name: event-occurs => onEventOccurs

    4.3 React:
    1. Inside utils.tsx define function that uses ReactDOM.render fn
    ReactDOM.render(
      React.createElement(ReactElement, props),
      document.querySelector(selector)
    );
    1. Pass imported ReactElement, props, DOM selector
  5. Import components

There are some ways to import federated modules:

import { Component } from 'someApp/Component';

or Promise import() function

import('someApp/Component').then(({ Component }) => {});

const { Component } = await import('someApp/Component');

Also @angular-architects/module-federation gives loadRemoteEntry and loadRemoteModule fns