openmrs-esm-config

API Docs
What is this?
This is the configuration library for OpenMRS Microfrontends.
It makes configurability easier for developers and configuring easier for implementers.
Contents
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-app": {
"logo": {
"src": "https://pbs.twimg.com/media/C1w_czvWgAAWONL.jpg"
}
},
"@openmrs/esm-home-app": {
"buttons": {
"enabled": false
}
}
}
Alternatively you can provide your config file as a Javascript file.
It will look just about the same, but with some magic words at the beginning:
exports = {};
exports.default = {
"@openmrs/esm-login-app": {
logo: {
src: "https://pbs.twimg.com/media/C1w_czvWgAAWONL.jpg"
}
},
"@openmrs/esm-home-app": {
buttons: {
enabled: false
}
}
}
How do I configure my OpenMRS implementation?
There are two methods for doing so.
The Simple Way
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.js[on]"
}
}
The Flexible Way
You can also provide config files programmatically. This technique
allows you to have multiple configuration files, which will be
merged together in an order that you specify.
To do this you need to create a (simple) custom module, which you will add to your
import map. Its name in the import map should be suffixed with -app.
This will ensure it is loaded.
You add your configuration files to this module, import them, and
provide them to the framework.
Example code:
import { provide } from "@openmrs/esm-framework";
import myOrgConfig from "./org-config.json";
import myOrgLocalConfig from "./org-local-config.json";
provide(myOrgConfig);
provide(myOrgLocalConfig);
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 highest priority. In the above example,
configuration elements in org-local-config.json will take priority over
those in org-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 part of the OpenMRS Frontend Framework to modules configurable.
Start with npm install --save-dev @openmrs/esm-framework. This is a runtime
dependency, so you should also include it in peerDependencies.
The main task is to create a config schema for your module. The config schema
is what tells the framework what configuration files should look like,
including defaults and validations.
Designing a schema
You'll probably start with some idea of what you want configs for your module
to look like. Try and put yourself in the implementer's shoes an imagine what
features they will expect to be configurable, and what they might expect the
configuration property to be called. Assume they don't know anything about
the internal workings of your module.
By way of example, let's say we're building a module for a virtual provider
functionality at a very futuristic hospital. Maybe we want an implementer to
be able to write the following in their config file:
"@openmrs/esm-hologram-doctor": {
"hologram": {
"color": true
},
"virtualProvider": {
"name": {
"given": ["Qui", "Gon"]
}
},
"robots": [
{ "name": "R2-D2", "homeworld": "Naboo" },
{ "name": "BB-8", "homeworld": "Hosnian Prime" }
]
}
In the following section, we'll see how to write a config schema that supports these config elements.
Defining a schema
We'll start with just that first nested config element from above, hologram.color.
We must provide defaults for all of the valuesâin OpenMRS Frontend 3.0, all
configuration is optional. All modules should do something reasonable out of the box.
import { defineConfigSchema, Type } from "@openmrs/esm-framework"
defineConfigSchema("@openmrs/esm-hologram-doctor", {
hologram: {
color: {
_type: Type.Boolean,
_default: false,
_description: "Whether the cologram supports color display."
}
}
}
Note that each configuration element should have an object for a value, and
that this object must define the properties for that element. Do not do this:
â
â defineConfigSchema("@openmrs/esm-hologram-doctor",
â hologram: {
â salutation: "Some friendly default salutation! ? this is wrong!"
â })
The words prefixed with _ are schema keywords. Do not prefix the names of your
config elements with underscores. Especially do not use a schema keyword as
a config element name.
â
â defineConfigSchema("@openmrs/esm-hologram-doctor",
â hologram: {
â salutation: {
â _default: {
â _default: "Greetings ? this is bad don't do it"
â }}})
Typing
While not strictly required in the current version, you should provide a type
for every config element you define. The _type keyword accepts values from
the Type enum.
These types are used both to validate input and to support special behavior
in the implementer tools.
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
on't work.
robot: {
name: {
_type: Type.String,
_default: "R2D2",
_description: "What to call the robot",
_validators: [
validator(n => /\d/.test(n), "Robots must have numbers in their names")
]
}
}
(Note that this piece of schema is not part of our above example. It only supports
a single robot, whereas we need to allow the implementer to provide an array of robots).
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.
You can even validate nested objects:
colorPicker: {
options: { _default: ["green", "red", "blue"] }
initial: { _default: "green" },
_description: "The color picker for lightsabers",
_validators: [
validator(o => o.options.includes(o.initial),
"Initial must be one of the options")
]
}
For convenience, some common validators are provided out of the box. See the
API / validators.
Arrays
You can accept and validate arrays, and arrays containing objects, in your
configuration schema. This is configured with the elements parameter, used
with _type: Type.Array. For example, a schema which would accept an array
of strings up to 30 characters long:
virtualProvider: {
name: {
given: {
_type: Type.Array,
_default: ["Obi", "Wan"]
_elements: {
_type: Type.String
_validators: [validator(n => n.length < 30, "Must be less than 30 characters")]
}
},
_description: "The name of the avatar. Does not have to be the name of the actual provider"
},
_description: "The avatar of the medical practitioner"
}
Here is an example of a schema that expects an array of objects structured in a particular way.
robots: {
_type: Type.Array,
_default: [
{ name: "R2-D2", homeworld: "Naboo" },
{ name: "C-3PO", homeworld: "Tatooine" }
],
_description: "The list of all available robots",
_elements: {
name: {
_type: Type.String,
_description: "What to call the robot",
_validators: [robotNameValidator]
},
homeworld: {
_type: Type.String,
_description: "Where the robot is from",
_default: null
}
}
}
This schema will require that any objects in the robots array must only have
the keys name and homeworld, and that name is required.
Objects within arrays do not
have to have defaults. If an object is supplied to the robots array that
does not have a name, an error will be thrown.
Freeform objects
In unusual scenarios you might want to accept an object without
validating its keys. To do this, you can specify the config element
like a normal non-object element.
beepsPerRobot: {
_type: Type.Object
_default: {
"R2-D2": 4,
"C-3P0": 0
},
_elements: {
_type: Type.Number
_validators: [validator(n => Number.isInteger(n), "Beeps must be integers")]
},
_description: "An object mapping robot names to number of beeps",
_validators: [
validator(o => Object.keys(o).every(n => /\d/.test(n)),
"Robots must have numbers in their names")
]
}
Note that this is the only situation in which you should ever use Type.Object.
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-framework"
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
moduleNameprovided to the
openmrsComponentDecorator in esm-react-utils
is used to look up the configuration elsewhere in the application.
export default openmrsRootDecorator({
featureName: "hologram doctor",
moduleName: "@openmrs/esm-hologram-doctor"
})(Root)
You can then get the config tree as an object using the useConfig React hook.
import { useConfig } from "@openmrs/esm-framework"
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! See "Contributing"
Schema Reference
_default
All config elements must have a default (excluding elements within arrays of objects).
The default does not necessarily need to satisfy the _type or the _validators of
the element, but this may change in future versions.
_type
One of the values from the Type enum. Used for validation and to help the
implementer tools work with the element.
Should always appear alongside _default.
_description
Helps implementers understand what the configuration element actually does and
how it is intended to be used.
Can be used anywhere within the schema structure.
_validators
An array of validator objects.
Some common validators are
provided: API / validators.
Custom validators should
be created with the validator function.
Can be used anywhere within the schema structure.
_elements
Only valid alongside _type: Type.Array or _type: Type.Object. A _default
must also be provided at this level. Value should be an object which is
a schema for the values contained in the array or object.
Contributing & Development
PRs welcome! See
OpenMRS Microfrontends RFC-20
for guidelines about contributing.
Setup local development environment for OpenMRS SPA.
Maintainer: Brandon Istenes (bistenes@pih.org)