Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Info

This is an experimental feature, currently available on the branch Bahmni-IPD-master of openmrs-module-bahmniapps. To follow this guide, you need to be on this branch as of now. Some details may change over time.

This guide will go step by step through implementing your own MFE with Bahmni. For example purposes, let’s say you want to create a micro-frontend named diagnostics which will export a single component called DiagnosticsDashboard.

Step 1: Create a MFE repository

You can clone <URL> bahmni-microfrontend-reference repository for a solid starting point. The README.md will contain a checklist for you to go through and set-up the repository with essential information.

Once you follow through the checklist, you should end up with a module name of bahmni_diagnostics in the ModuleFederation plugin and DiagnosticsDashboard should be part of the exposes property of the same.


Step 2: Decide on a serving strategy for your MFE

On running yarn build, you should get built files in dist/federation/ folder. Files within this need to be served from a web server. This would be our remote URL.

2.1: Docker container with proxy

The reference repository assumes that you want to serve your MFE from within the bahmni-docker setup as a single container based on a reverse proxy. If you go through this route, follow the steps below

2.1.1: Update the GitHub workflow and Dockerfile to build an image named bahmni/microfrontend-diagnostics.

2.1.2: Add appropriate entry into the docker-compose setup that you are using. This could be bahmni-docker or some fork of it as per your use case. You should end up with a container named (for example) diagnostics. An example entry with volume mount for local development is shown below.

Code Block
languageyaml
diagnostics:
  image: bahmni/microfrontend-diagnostics:latest
  profiles: ["emr", "bahmni-lite", "bahmni-mart"]
  volumes:
  - "${DIAGNOSTICS_PATH:?}/dist/federation/:/usr/local/apache2/htdocs/diagnostics"

2.1.3: Add a proxy entry for the container in your proxy setup. Assuming you are using bahmni-proxy, add a reverse proxy rule to resources/bahmni-proxy.conf

Code Block
# diagnostics
ProxyPass /diagnostics http://diagnostics/diagnostics
ProxyPassReverse /diagnostics http://diagnostics/diagnostics

This make the apache folder accessible at /diagnostics proxy. Confirm that https://<bahmni-host>/diagnostics/remoteEntry.js is accessible.


Step 3: Create MFE module in openmrs-module-bahmniapps

3.1: Create source files to hold new angular module

Code Block
languagebash
mkdir micro-frontends/src/diagnostics
touch micro-frontends/src/diagnostics/index.js
Code Block
languagejs
/* micro-frontends/src/diagnostics/index.js */

// Follow the below module naming convention
angular.module('bahmni.mfe.diagnostics', [/* add any dependencies */]);

3.2: Add WebPack entry point for new module

Code Block
languagejs
/* micro-frontends/webpack.config.js */

module.exports = {
  // ... other stuff
  entry: {
    // ...
    diagnostics: "./src/diagnostics/index.js"
  }
}

Completing the above steps adds a new WebPack module which will be built into ui/app/micro-frontends-dist/diagnostics.min.js and `ui/app/micro-frontends-dist/diagnostics.min.css.

3.3: Mock out your module for tests

Assuming that your MFE will have full test coverage in your own repository, you should mock out this module for unit tests in Bahmni to make things easier. This needs to be done in ui/test/__mocks__/micro-frontends.js

Code Block
languagejs
/* ui/test/__mocks__/micro-frontends.js */

// add an empty module as a mock
angular.module('bahmni.mfe.diagnostics', []);

Step 4: Create a hosting React component

4.1: Add ModuleFederation config for your MFE

This sets up the remote URL accessible for dynamic imports

Code Block
breakoutModewide
languagejs
/* micro-frontends/webpack.config.js */

module.exports = {
  // ... other stuff
  plugins: [
    // ...
    new ModuleFederationPlugin({
      // ...
      remotes: {
        // ...
        
        // IF using proxy, do this
        // Add your MFE with the federated module name and proxy path
        "@openmrs-mf/diagnostics": remoteProxiedAtHostDomain({ name: "bahmni_diagnostics", path: "diagnostics"})
        
        // if not using proxy, add the static URL instead of using remoteProxiedAtHostDomain
      }
    })
  ]
}

4.2: Create a hosting React component loading the remote component

Code Block
breakoutModewide
languagejsx
/* micro-frontends/src/diagnostics/Dashboard.jsx */

import PropTypes from "prop-types";
import React, { Suspense, lazy } from "react";

export function Dashboard(props) {
  // on run, this will fetch remoteEntry.js and load the DiagnosticsDashboard component from there
  const LazyComp = lazy(() => import("@openmrs-mf/diagnostics/DiagnosticsDashboard"));

  return (
    <>
      <Suspense fallback={<p>Loading...</p>}>
        <LazyComp
          hostData={props.hostData}
          hostApi={props.hostApi}
        />
      </Suspense>
    </>
  );
}

// Without propTypes, react2angular won't render the component
Dashboard.propTypes = {
  hostData: PropTypes.shape({
    // Set up your input data shape as needed here, below is an example
    patient: PropTypes.shape({
      uuid: PropTypes.string.isRequired,
    }).isRequired,
  }),
  hostApi: PropTypes.shape({
    // Set up your output (callbacks) shape as needed. below is an example
    onConfirm: PropTypes.func,
  }),
};

Step 5: Register angular component

The angular module created in Step 3 would need a wrapper angular component added to, which will interface with the hosting component created in Step 4. There are two ways to achieve this

option A (recommended): Use React2AngularBridgeBuilder abstraction

We have created a good abstraction which simplifies the wrapper build process using conventions. Check it out at micro-frontends/src/utils/bridge-builder.js

Code Block
languagejs
/* micro-frontends/src/diagnostics/index.js */
import { React2AngularBridgeBuilder } from "../utils/bridge-builder";
import { Dashboard } from 'Dashboard.jsx';

// Extract module name to make it easier
const MODULE_NAME = "bahmni.mfe.diagnostics"
angular.module(MODULE_NAME, []);

const builder = new React2AngularBridgeBuilder({
  moduleName: MODULE_NAME,
  // use the following convention mfe<camelCaseNameOfModule>
  componentPrefix: "mfeDiagnostics",
});

builder.createComponent("Dashboard", Dashboard);
// The above registers an angular component named 'mfeDiagnosticsDashboard'

option B: Manual

If you need more control over the wrapper build process, you can do it manually, following what the abstraction in micro-frontends/src/utils/bridge-builder.js does internally.

Code Block
languagejs
/* micro-frontends/src/diagnostics/index.js */
import { Dashboard } from 'Dashboard.jsx';
import { react2angular } from "react2angular";

// Extract module name to make it easier
const MODULE_NAME = "bahmni.mfe.diagnostics"

angular.module(MODULE_NAME, []);

angular.module(MODULE_NAME)
  .component('mfeDiagnosticsDashboard', react2angular(Dashboard));
  
// The above registers an angular component named 'mfeDiagnosticsDashboard'

Step 6: Use your MFE in an angular module

To use your MFE in a given angular module, you just need to include the the module as a dependency and add the required built files along with react to the html entry point. Let’s take the example of the clinical module.

Code Block
languagehtml
<!-- ui/app/clinical/index.html -->
<html>
<head>
  <!-- common mfe styles -->
  <link rel="stylesheet" href="../micro-frontends-dist/shared.min.css"/>
  
  <!-- global specific styles of diagnostics mfe -->
  <link rel="stylesheet" href="../micro-frontends-dist/diagnostics.min.css"/>
  
  <!-- build:css clinical.min.css -->
  <!-- DO NOT ADD MFE STYLES INSIDE THE GRUNT BUILD BLOCK -->
  <!-- endbuild -->
</head>
<body>

  <!-- Adding react since an micro-frontend will be used -->
  <script src="../components/react/react.production.min.js"></script>
  <script src="../components/react-dom/react-dom.production.min.js"></script>

  <!-- form-controls also needs react so make sure you include react before this -->
  <script src="../components/bahmni-form-controls/helpers.js"></script>
  <script src="../components/bahmni-form-controls/bundle.js"></script>

  <!-- shared js including polyfills -->
  <script src="../micro-frontends-dist/shared.min.js"></script>

  <!-- load the mfe js -->
  <script src="../micro-frontends-dist/diagnostics.min.js"></script>
  
  <!-- build:js clinical.min.js -->
  <!-- DO NOT ADD MFE STYLES INSIDE THE GRUNT BUILD BLOCK -->
  <!-- endbuild -->
</body>
</html>

Code Block
languagetext



---------- Add shared css first


---------- Then add mfe css

---------- IMPORTANT: Make sure no mfe css is inside a build block like this






---------- Include the react packages if not already present







---------- Add the shared JS for mfes


---------- Add the JS of the mfe


---------- IMPORTANT: Make sure no mfe js is inside a build block like this. All MFE code needs
           to be ABOVE this block
           
           
Info

It is very important to correctly order and place the javascript and css files within the index.html. There are build marker positions in here which will make Grunt pick up minified files within and try to minify again. This will fail with the mfe build. Follow the instructions in the comments given

The last step after this is to include the module as a dependency in your init.js file.

Code Block
languagejs
/* ui/app/clinical/init.js */

// ...
angular.module('bahmni.clinical', [
  // other dependencies
  'bahmni.mfe.diagnostics'
])

Which then allows you to render your component in a template like this

Code Block
languagehtml
<!-- ui/app/clinical/dashboard/views/dashboard.html -->

<mfe-diagnostics-dashboard host-data="hostData" host-api="hostApi">
</mfe-diagnostics-dashboard>

Managing information flow

Currently, we have not set up any kind of event bus architecture for communication between host and MFE components. As a convention, we will use hostData and hostApi keys to do this communication.

Passing data from Host to MFE

The remote MFE can receive data from the host angular application though standard React props. As a matter of convention, it has been decided to wrap these inputs in a single prop object called hostData. An example usage would look like

Code Block
languagejs
// in the controller
$scope.hostData = {
  patient: {
    // patient info
  },
  otherData: "// other data"
}
Code Block
languagehtml
<!-- in the template -->
<mfe-example-component1 host-data="hostData"></mfe-example-component1>
Code Block
languagejsx
// in the React entry component
export function Component1(props) {
  return <div>{props.hostData.patient.name}</div>
}

Note: Remember, angularJs maps camelCase bindings to kebab-case so the hostData prop will be bound to host-data binding in the html.

Host reacting to events from MFE

For now, we have decided to rely on the host app passing callbacks to the MFE through a conventional prop named hostApi which needs to be an object. An example usage would look like

Code Block
languagejs
// in the controller
$scope.hostApi = {
  onClose: function() {
    closeModal();
  },
  onConfirm: function(id) {
    service.confirmValues(id);
  }
}
Code Block
languagehtml
<!-- in the template -->
<mfe-example-component1 host-api="hostApi"></mfe-example-component1>
Code Block
languagejsx
// in the React entry component
export function Component1(props) {
  // *See notes
  return <button onClick={props.hostApi?.onClose?.()>Close</button>
}

Important note regarding hostApi

For some reason, the react2angular adapter take an extra tick to resolve the functions within hostApi binding. Due to this, even if your propTypes mark the hostApi functions as required, React will throw reference errors for accessing functions in undefined and similar. I recommend using the optional chaining operator (?.) to get around this