Adding a left panel
The left panel (opens in a new tab) provides navigation within an App in O3. It is based on Carbon's UI shell left panel (opens in a new tab), and is positioned below the header, fixed to the left edge of the page. It is possible to add a left panel to any page in O3 if you need to leverage its navigation capabilities.
This guide will walk you through the process of adding a left panel to the Bed Management app (opens in a new tab), which is a frontend module that's part of the UgandaEMR+ instance. The bed management app handles the management of beds in a hospital, and is a good candidate for a left panel because it has multiple pages that need to be navigated to.
Example: Adding a left panel to the Bed Management app
The Bed Management app has the following screens that need to be navigated to:
- The landing screen, which shows a summary of the number of beds in each ward.
- A detail screen for a specific ward, which shows the number of beds in that ward and their current status.
- A ward allocation screen, which allows you to add, edit and delete beds, as well as allocating beds to wards.
We'll need to setup the following navigation links in the left panel:
- A
Summarylink for the landing screen and the ward detail screen. - An
Administrationlink for the bed management administration screen.
Below is a screenshot of how the app looks like:
To achieve this, we're going to follow the steps below:
Step 1: Set up the left panel
Begin by updating your root component (or whatever component your app uses to set up routing) as follows:
import React, { useEffect } from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { LeftNavMenu, setLeftNav, unsetLeftNav } from "@openmrs/esm-framework";
import BedAdministrationTable from "./bed-administration/bed-administration-table.component";
import Home from "./home.component";
import WardWithBeds from "./ward-with-beds/ward-with-beds.component";
import styles from "./root.scss";
const Root: React.FC = () => {
const spaBasePath = window.spaBase;
useEffect(() => {
setLeftNav({
name: "bed-management-left-panel-slot",
basePath: spaBasePath,
});
return () => unsetLeftNav("bed-management-left-panel-slot");
}, [spaBasePath]);
return (
<BrowserRouter basename={`${window.getOpenmrsSpaBase()}bed-management`}>
<LeftNavMenu />
<main className={styles.container}>
<Routes>
<Route path="/summary" element={<Home />} />
<Route path="/ward/:wardUuid" element={<WardWithBeds />} />
<Route path="/ward-allocation" element={<BedAdministrationTable />} />
</Routes>
</main>
</BrowserRouter>
);
};
export default Root;Some key things to note here are:
-
We're importing the
LeftNavMenucomponent from the@openmrs/esm-frameworkpackage. This component renders the left panel. We're also importing thesetLeftNavandunsetLeftNavfunctions from the same package. These functions are used to register and unregister the left panel with the LeftNav store (opens in a new tab) respectively. The LeftNav store is a Zustand (opens in a new tab) store that keeps track of all the left panels that have been registered in the app. -
We're calling the
setLeftNavfunction in auseEffecthook. This function takes an object with two properties:name: The name of the slot that the left panel should be rendered in. This is the same name that you'll use when adding the left panel to theextensionsarray of yourroutes.jsonfile.basePath: The base path of the app (/openmrs/spaby default). This is used to ensure that the left panel links are relative to the app's base path.
-
We're calling the
unsetLeftNavfunction in auseEffectcleanup function. This function takes the name of the slot that the left panel was rendered in. This ensures that the left panel gets unregistered when the component unmounts. -
We're rendering the
LeftNavMenucomponent as the first child of theBrowserRoutercomponent. This ensures that the left panel gets rendersd in all the routes of the app. -
We're setting up three routes for the Bed Management app:
- A
/summaryroute that renders theHomecomponent. - A
/ward/:wardUuidroute that renders theWardWithBedscomponent. The:wardportion of the route is a URL parameter that gets passed to theWardWithBedscomponent as a prop. This component is used to render the detail page for a specific ward. - An
/ward-allocationroute that renders theBedAdministrationTablecomponent.
- A
Step 2: Wiring up the root page
Next, we'll create a named export for the Root component inside the index.ts file. This is the component that we'll use to wire up the Root component to the bed-management route so that it gets rendered when you navigate to that route.
Add the following to your index.ts file:
import rootComponent from './root.component';
export const root = getSyncLifecycle(rootComponent), options);Next, modify your routes.json file to include the following page definition:
{
"$schema": "https://json.openmrs.org/routes.schema.json",
"backendDependencies": {
"fhir2": "^1.2.0",
"webservices.rest": "^2.24.0"
},
"pages": [
{
"component": "root",
"route": "bed-management"
}
]
}This page definition tells O3 to render the Root component when you navigate to the bed-management route.
Step 3: Add links to the left panel
Next, we'll want to add the two links that we mentioned earlier to the left panel:
- A
Summarylink for the landing screen. - A
Ward allocationlink for the ward allocation screen.
To do this, we'll need to create two extensions that render the links. We'll then add those extensions to the extensions array of our routes.json file. The named exports of those extensions will be used as the component property of the extensions, and look like the following:
export const summaryLeftPanelLink = getSyncLifecycle(
createLeftPanelLink({
name: "summary",
title: t("summary", "Summary"),
}),
options
);
export const wardAllocationLeftPanelLink = getSyncLifecycle(
createLeftPanelLink({
name: "ward-allocation",
title: t("wardAllocation", "Ward Allocation"),
}),
options
);The createLeftPanelLink function is a higher-order function that takes a name and a title property. The name property is the unique path that the URL segment gets matched against. If the name matches the last portion of the URL, then the matching link gets some special styling to indicate that it is the active link. The title property gets rendered as the link text.
We're importing this createLeftPanelLink function from a separate file called left-panel-link.component.tsx that looks like the following:
import React, { useMemo } from "react";
import last from "lodash-es/last";
import { BrowserRouter, useLocation } from "react-router-dom";
import { ConfigurableLink } from "@openmrs/esm-framework";
export interface LinkConfig {
name: string;
title: string;
}
function LinkExtension({ config }: { config: LinkConfig }) {
const { name, title } = config;
const location = useLocation();
let urlSegment = useMemo(() => decodeURIComponent(last(location.pathname.split("/"))), [location.pathname]);
const isUUID = (value) => {
const regex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/;
return regex.test(value);
};
if (isUUID(urlSegment)) {
urlSegment = "summary";
}
return (
<ConfigurableLink
to={`${window.getOpenmrsSpaBase()}bed-management${name ? `/${name}` : ""}`}
className={`cds--side-nav__link ${name === urlSegment && "active-left-nav-link"}`}
>
{title}
</ConfigurableLink>
);
}
export const createLeftPanelLink = (config: LinkConfig) => () =>
(
<BrowserRouter>
<LinkExtension config={config} />
</BrowserRouter>
);Some key things to note here are:
- We're using the ConfigurableLink (opens in a new tab) component from the
@openmrs/esm-frameworkpackage to render the link. Thetitleproperty gets rendered as the link text. The link also gets some special styling if thenameproperty matches the last portion of the URL that distinguishes it as the active link. - We're using the
useLocationhook from thereact-router-dompackage to get the current URL. We then extract the last segment of the current pathname and decode it. This segment is stored in theurlSegmentvariable. We then use theurlSegmentvariable to determine whether the link is active or not. - We're using the
createLeftPanelLinkhigher-order function to create a component that renders the link. This function takes anameand atitleproperty. Thenameproperty is the unique path that the URL segment gets matched against. If the name matches the last portion of the URL, then the matching link gets some special styling to indicate that it is the active link. Thetitleproperty gets rendered as the link text. - When the last segment of the URL is a UUID (which is the case when you click on a ward card on the landing page), we're setting the
urlSegmentvariable tosummary. This ensures that both the landing and detail screen s get theSummarylink highlighted as the active link. - Finally, we're exporting the
createLeftPanelLinkfunction so that it can be used to create theSummaryandWard allocationlinks.
Step 4: Add the extensions to the routes.json file
Finally, we'll need to add the two extensions that we created in the previous step to the extensions array of our routes.json file as shown below:
"extensions": [
{
"component": "adminCardLink",
"name": "bed-management-admin-card-link",
"slot": "system-admin-page-card-link-slot"
},
{
"component": "summaryLeftPanelLink",
"name": "bed-management-left-panel-link",
"slot": "bed-management-left-panel-slot",
"order": 0
},
{
"component": "wardAllocationLeftPanelLink",
"name": "ward-allocation-left-panel-link",
"slot": "bed-management-left-panel-slot"
}
]Step 5: Profit!
That's it! When you navigate to the bed-management route, you should see the landing screen of the Bed Management app, which should look like the following:

Clicking on the General Men Ward card navigates you to a detail page for a specific ward, which should look like the following:

Finally, clicking the Ward Allocation link in the left panel leads you to the Ward Allocation page, which looks like the following:
