An application with significant functional depth has a multi-level menu. The menu tree reflects the application's functional architecture, and its coherence is the foundation of overall ergonomics.
We're all familiar with some sophisticated applications where we really wonder why such and such a menu entry is in such and such a place. And if you're unfamiliar with these applications, you'll always spend some time browsing the menu entries before finding what you're looking for. And yet, let there be no doubt, the developers have made a real effort to arrange all this in an optimal way.
It's not always easy to systematically structure the different views, especially when there are strong adhesions between functional sets or when there are certain exceptions to integrate.
The user must be able to navigate easily within the application and, in addition to the menu structure itself, it's important to understand where you are at the moment. This is the purpose of the breadcrumb trail, which is a visual reminder of the path followed within the menu tree.
With PrimeNG we have these two components, Menubar and Breadcrumb, which can be linked to offer clear navigation. This is shown in this screenshot taken with the examples provided on PrimeNG site.
this being said, all this takes up a lot of screen space, and this can be a problem for certain views that require the maximum amount of screen space. Not to mention that, in terms of ergonomics, it's always preferable to limit the amount of visual information to that which is imperative for the immediate actions to be carried out on the view in question. With the menu and breadcrumb, there's already a lot of visual noise before any data has even been displayed. So, as you can see, it's a never-ending battle to find the right balance.
One option would be to merge the two components into a single one by coding a breadcrumb that also serves as a menu. The Windows file explorer address bar works exactly like this, which I find really handy.
It's a little exercise I came up with as part of my research on ecsy and I'd like to share it with you here. The full code can be found in this repo. It's obviously not intended to be used as is in production, but you can easily optimize it and adapt it to your context. Here it is in action. I've used the same demo dataset as the PrimeNG Menubar component to make comparison easy.
As usual, you can try it out for yourself:
clone the repo
npm install
npm run start
UI
There are two very simple ng components:
- BreadcrumbMenuComponent which is the global wrapper
- BreadcrumbMenuSetComponent, which is a segment of the breadcrumb and carries a menu set.
BreadcrumbMenuComponent is a loop that displays a succession of segments separated by a right chevron:
<div class="flex align-items-center">
@for (item of model.path; track $index) {
@if (item.depth > 0) {
<i class="pi pi-angle-right"></i>
}
<breadcrumb-menu-set [model]="item"/>
}
</div>
BreadcrumbMenuSet is a PrimeNG Menu popup component with a template to display a tick on the selected item:
<p-menu #menu [model]="model.menuItems" [popup]="true" (onShow)="onShow()" (onHide)="onHide()">
<ng-template pTemplate="i" let-i>
<div class="flex justify-content-between p-2 cursor-pointer">
<span> </span>
@if (i.selected) {
<i class="pi pi-check" style="color: #00a500"></i>
}
</div>
</ng-template>
</p-menu>
<p-button #menuButton [text]="true" [severity]="model.hasSelected ? '' : 'warning'" (click)="menu.toggle($event)"></p-button>
Note I deliberately omitted the parentheses in the extract above on due to a build issue with 11ty/liquid, nothing to do with our topic.
Model
The dataset used is that of PrimeNG, but I needed additional properties, the item's parentId and its depth in the tree. My menu item is therefore an extension of the PrimeNG MenuItem.
export interface ExtendedMenuItem extends MenuItem {
id: string;
parentId?: string;
depth: number;
label: string;
items?: ExtendedMenuItem[];
icon?: string | undefined;
command?: any;
}
The data source is initialized in AppComponent.ngOnInit()
, as are the datasets for the Menubar (mbItems), Breadcrumb (bcItems) and BreadcrumbMenu (breadcrumbMenu).
The mbItems and bcItems properties are simple arrays. They are used here unprocessed to compare behaviors.
BreadcrumbMenu
The breadcrumbMenu property, on the other hand, is a BreadcrumbMenu class that embeds the component's logic. It is initialized from the flattening of the mbItems array:
this.breadcrumbMenu = new BreadcrumbMenu(this.flatMbItems);
The mbItems array is flattened using a classic recursive function:
flattenMenuItems(items: ExtendedMenuItem[]): ExtendedMenuItem[] {
const result: ExtendedMenuItem[] = [];
items.forEach(i => {
result.push(i);
if (i.items) {
result.push(...this.flattenMenuItems(i.items));
}
});
return result;
}
The BreadcrumbMenuComponent expects a model of type BreadcrumbMenu...
export class BreadcrumbMenuComponent {
@Input() model: BreadcrumbMenu | undefined;
}
... and this model is provided by the parent AppComponent :
<div class="flex flex-column gap-2">
...
<p-panel header="Menubar & Breadcrumb mix">
...
<breadcrumb-menu [model]="breadcrumbMenu"/>
</p-panel>
</div>
The BreadcrumbMenu class is built from an external data source which is an array of items of type ExtendedMenuItem. In a real project, the source may come from an api call.
The tricky thing here is structuring the dataset from the source. To be able to navigate the menu via the breadcrumb, it needs to "know" all the possible paths. Each segment of the breadcrumb is a list of items, some of which are end points that trigger the display of a view, others of which are waypoints to lower levels.
It's this structuring of the source dataset into breadcrumb segments that ultimately mobilizes the most code in the BreadcrumnMenu class:
private buildSets(flatMenuItems: ExtendedMenuItem[], items: AppMenuItem[], parentSet: AppMenuSet): void {
...
}
private addSet(id: string, depth: number, parent: AppMenuSet, menuItems: AppMenuItem[]): AppMenuSet {
...
}
private addMenuSet(m: AppMenuItem, parent: AppMenuSet): AppMenuSet {
...
}
private addSingleSets(parent: AppMenuSet, menuItems: AppMenuItem[]): void {
...
}
private getMenuItem(id: string, label: string, depth: number, deadEnd: boolean): AppMenuItem {
...
}
There must be some way of simplifying this for real-life use.
Selecting a menu item adds a segment to the breadcrumb, and if the item is a waypoint, the list of lower levels opens automatically to facilitate input. Always on the hunt for clicks 😁.
In short, the model has the following structure:
- AppMenuItem is the basic menu item
- AppMenuSet is the set of basic elements making up a breadcrumb segment.
- BreadcrumbMenu portrays the segment hierarchy and the currently selected path.
I won't go into the details of the model, the code is very simple and speaks for itself.
Il y a sûrement moyen de simplifier tout ça en vue d'une utilisation réelle.
To conclude
So that's it, in the end this work remained in the draft state and I've never used it in a real situation, so far. It would be great if you could let me know if you implement it somewhere.
Todo
The thing that's missing from this demo is a little chevron icon that tells the user whether a drop-down menu entry has children or not. This way, the user knows in advance what's going to happen if he clicks on it, whether to display a sub-menu and thus extend the breadcrumb or display a view.
⌨️ Get coding!