GitHub: openmrs-esm-module-config
npm: @openmrs/esm-module-confg
What is this?
openmrs-esm-module-config is the configuration library for OpenMRS microfrontends. It strives to establish configuration as a first-class concern of OpenMRS components. To this end, it provides mechanisms to make configurability easier for developers and configuring easier for implementers.
What does an OpenMRS frontend configuration file look like?
OpenMRS frontend configuration files are JSON files containing module names as top-level elements. All configuration elements are optional. The available configuration elements for each module should be documented in the module's wiki page.
Here's an example!
{
"@openmrs/esm-login": {
"logo": {
"src": "https://pbs.twimg.com/media/C1w_czvWgAAWONL.jpg"
}
},
"@openmrs/esm-home": {
"search": {
"numResultsPerPage": 20
}
}
}
How do I configure my OpenMRS implementation?
There are two methods for doing so.
The Simple Way
This is the method to use if you do not have an esm-root-config override. All you need to do is upload your configuration file and add its URL to your import map as a module named config-file
. If you are serving your microfrontends from your OpenMRS server, you can simply add your config file to your server's frontend/
directory. Your import map will then look like
{
"imports": {
...
"config-file": "/openmrs/frontend/config.json"
...
}
}
The Flexible Way
This method requires you have an esm-root-config override. This allows you to have multiple configuration files, which will be merged together in an order that you specify. You add your configuration files to your root override module, import
them, and provide them to esm-module-config
. All this must happen before you register your applications.
Example code:
import { provide } from "@openmrs/esm-module-config";
import pihConfig from "./pih-config.json";
import pihMexicoConfig from "./pih-mexico-config.json";
provide(pihConfig);
provide(pihMexicoConfig);
All provided configs will be merged, with elements provided by later calls to provide
taking priority. The import map config file, config-file
, will also be merged, and will take the lowest priority. In the above example, configuration elements in pih-mexico-config.json
will take priority over those in pih-config.json
.
You can break up your configuration files into hierarchies, or per module, or per groups of modules.
I'm developing an ESM module. How do I make it configurable?
You should use this module, esm-module-config, to make your modules configurable. Getting set up is easy.
Defining a schema
You need to tell esm-module-config what configuration files for your module should look like. You will also provide defaults for all of the values — in OpenMRS ESMs, all configuration is optional.
import { defineConfigSchema, validators } from "@openmrs/esm-module-config"
defineConfigSchema("@openmrs/esm-hologram-doctor", {
hologram: {
color: {
default: false,
validators: [validators.isBoolean]
}
}
}
The above would expect a configuration file structured like
{
"@openmrs/esm-hologram-doctor": {
"hologram: {
"color": true
}
}
}
Note that each configuration element should have an object for a value, and that this object must define the default for that element. Do not do this:
? defineConfigSchema("@openmrs/esm-hologram-doctor",
? hologram: {
? salutation: "Some friendly default salutation! ? this is wrong!"
? })
The following names are reserved and cannot be used as config keys: default
, validators
, and arrayElements
. Doing so will result in undefined behavior. Do not do this:
? defineConfigSchema("@openmrs/esm-hologram-doctor",
? hologram: {
? salutation: {
? default: {
? default: "Greetings ? this is bad don't do it"
? }}})
Validators
You should provide validators for your configuration elements wherever possible. This reduces the probability that implementers using your module will have hard-to-debug runtime errors. It gives you, the module developer, the opportunity to provide implementers with very helpful explanations about why their configuration won't work.
robot: {
name: {
default: "R2D2",
validators: [
validators.isString,
validator(n => /\d/.test(n), "all robots must have numbers in their names")
]
}
}
A validator can be created using the validator
function, as above.
The first argument is a function that takes the config value as its only argument. If the function returns something truthy, validation passes. If the function returns something falsy, an error is thrown with the second argument as an explanation.
For convenience, some common validators are provided out of the box. These are
- isBoolean
- isString
Arrays
You can accept and validate arrays, and arrays containing objects, in your configuration schema. This is configured with the arrayElements
parameter. For example, a schema which would accept an array of strings:
virtualProvider: {
name: {
given: {
default: ["Obi", "Wan"]
arrayElements: {
validators: [validators.isString]
}}}}
Here is an example of a schema that expects an array of objects structured in a particular way.
robots: {
default: [
{ name: "R2-D2", homeworld: "Naboo" },
{ name: "C-3PO", homeworld: "Tatooine" }
],
arrayElements: {
name: { validators: [robotNameValidator] },
homeworld: {
default: null // not required
validators: [validators.isString]
}
}
}
This schema will require that any objects in the robots
array must only have the keys name
and homeworld
.
Using config values
The generic way
The config is fetched asynchronously using getConfig(moduleName)
. Continuing the above example, we would have something like
import { getConfig } from "@openmrs/esm-module-config"
async function doctorGreeting() {
const config = await getConfig("@openmrs/esm-hologram-doctor")
return "Hello, my name is Dr. " + config.virtualProvider.name.family
}
The content of config
will be pulled from the config files, falling back to the defaults for configuration elements for which no values have been provided.
React support
A React Hook is provided to hide the asynchronicity of config loading. The moduleName
provided to the openmrsRootDecorator is used to look up the configuration elsewhere in the application.
export default openmrsRootDecorator({
featureName: "hologram doctor",
moduleName: "@openmrs/esm-hologram-doctor"
})(Root)
given an application root component named Root
.
You can then get the config tree as an object using the useConfig
React hook.
import { useConfig } from "@openmrs/esm-module-config"
export default function DoctorGreeting() {
const config = useConfig()
const greeting = "Hello, my name is Dr. " + config.virtualProvider.name.family
return (
<div>{ greeting }</div>
)
}
The content of config
will be pulled from the config files, falling back to the defaults for configuration elements for which no values have been provided.
Support in other frameworks (Angular, Vue, Svelte, etc.)
This hasn't been implemented yet, but we would like to implement it! Please reach out to bistenes@pih.org and/or open an issue on the OpenMRS Jira.