In the world of B2B commerce, intuitive and scalable navigation is everything. When dealing with a wide range of product categories and subcategories, a dynamic and hierarchical menu structure becomes critical.
In this post, weโll walk through building a multi-level navigational menu using Lightning Web Components (LWC) and Apex in Salesforce, based on product categories stored in Salesforce B2B Commerce.
๐ก Objective
To create a Lightning Web Component that:
- Retrieves product categories from Salesforce via Apex.
- Dynamically renders them in a multi-tiered, collapsible menu layout.
- Supports navigation to category-specific pages.
๐งฉ Apex Controller: MultiLevelMenuBarController
We start by creating an Apex class to retrieve the product categories marked as navigational:
public without sharing class MultiLevelMenuBarController{
@AuraEnabled(cacheable=true)
public static List<ProductCategory> getCategories() {
try {
return [SELECT ID, Name, ParentCategoryId, NumberOfProducts FROM ProductCategory WHERE IsNavigational = true];
} catch (Exception e) {
throw new AuraHandledException(e.getMessage());
}
}
}
๐ Key Highlights:
- The method is annotated with
@AuraEnabled(cacheable=true)
for efficient, reactive LWC data fetching. - The query filters categories that are marked navigational (
IsNavigational = true
).
๐จ LWC HTML Template
Here’s the HTML structure that recursively renders up to five levels of category hierarchy:
<template>
<lightning-layout class="menubar-layout">
<template for:each={parentCategories} for:item="category">
<lightning-layout-item key={category.Id} padding="around-small" class="parent-category">
<div class="categoryName" onclick={navigateToCategory} data-id={category.Id}>{category.Name}</div>
<div class="child-dropdown">
<lightning-layout multiple-rows class="slds-var-m-vertical_small">
<lightning-layout-item key={category.Id} for:each={category.child} for:item="children1" size="3" class="slds-var-p-bottom_x-small slds-var-p-left_small">
<div class="slds-var-p-bottom_x-small categoryName" onclick={navigateToCategory} data-id={children1.Id}><b>{children1.Name}</b></div>
<div key={children1.Id} for:each={children1.child} for:item="children2">
<div class="slds-var-p-bottom_x-small slds-var-p-left_medium slds-truncate categoryName" onclick={navigateToCategory} data-id={children2.Id}>{children2.Name}</div>
<div key={children2.Id} for:each={children2.child} for:item="children3">
<div class="slds-var-p-bottom_x-small slds-var-p-left_large slds-truncate categoryName" onclick={navigateToCategory} data-id={children3.Id}>{children3.Name}</div>
<div key={children3.Id} for:each={children3.child} for:item="children4">
<div class="slds-var-p-bottom_x-small slds-var-p-left_x-large slds-truncate categoryName" onclick={navigateToCategory} data-id={children4.Id}>{children4.Name}</div>
</div>
</div>
</div>
</lightning-layout-item>
</lightning-layout>
</div>
</lightning-layout-item>
</template>
</lightning-layout>
</template>
๐ง LWC JavaScript Logic
The JavaScript handles data transformation and UI logic.
import { LightningElement } from 'lwc';
import { track, wire } from 'lwc';
import { NavigationMixin } from 'lightning/navigation';
import getCategories from '@salesforce/apex/MultiLevelMenuBarController.getCategories';
export default class B2blMenuBar extends NavigationMixin(LightningElement) {
@track
parentCategories = [];
@track
categoriesJSON = [];
@wire(getCategories)
categories({ error, data }) {
if (data) {
console.log({data});
var allCategories = JSON.parse(JSON.stringify(data));
var tempparentCategories = allCategories.filter(tempdata => tempdata.ParentCategoryId == undefined);
console.log('temp parentCategories::', JSON.parse(JSON.stringify(tempparentCategories)));
let categoryJSON = {};
tempparentCategories.forEach((cat, index) => { //level 1
var childCate1 = allCategories.filter(tempcat => tempcat.ParentCategoryId == cat.Id);
console.log('childCate1::', childCate1);
if(childCate1.length>0){
var tempCat = JSON.parse(JSON.stringify(cat));
tempCat['child'] = childCate1;
tempparentCategories[index] = tempCat;
console.log('first level cat::', cat);
childCate1.forEach((cat1, index) =>{ //level 2
console.log('cat1::', JSON.parse(JSON.stringify(cat1)));
var childCate2 = allCategories.filter(tempcat2 => tempcat2.ParentCategoryId == cat1.Id);
console.log('childCate2::', childCate2);
if(childCate2.length>0){
var tempCat1 = JSON.parse(JSON.stringify(cat1));
tempCat1['child'] = childCate2;
childCate1[index] = tempCat1;
console.log('childCate::', childCate1);
childCate2.forEach((cat2, index) =>{ //level 3
var childCate3 = allCategories.filter(tempcat3 => tempcat3.ParentCategoryId == cat2.Id);
console.log('childCate3::', childCate3);
if(childCate3.length>0){
var tempCat2 = JSON.parse(JSON.stringify(cat2));
tempCat2['child'] = childCate3;
childCate2[index] = tempCat2;
console.log('childCate::', childCate2);
childCate3.forEach((cat3,index) =>{ //level 4
var childCate4 = allCategories.filter(tempcat4 => tempcat4.ParentCategoryId == cat3.Id);
console.log('childCate4::', childCate4);
if(childCate4.length>0){
var tempCat3 = JSON.parse(JSON.stringify(cat3));
tempCat3['child'] = childCate4;
childCate3[index] = tempCat3;
console.log('childCate::', childCate3);
childCate4.forEach((cat4, index) =>{ //level 5
var childCate5 = allCategories.filter(tempcat5 => tempcat5.ParentCategoryId == cat4.Id);
console.log('childCate5::', childCate5);
if(childCate5.length>0){
var tempCat4 = JSON.parse(JSON.stringify(cat4));
tempCat4['child'] = childCate5;
childCate4[index] = tempCat4;
console.log('childCate::', childCate4);
}
});
}
});
}
});
}
});
}
});
this.categoriesJSON = categoryJSON;
this.parentCategories = tempparentCategories;
console.log('final parentCategories::', JSON.parse(JSON.stringify(this.parentCategories)));
console.log('categoriesJSON::', JSON.parse(JSON.stringify(this.categoriesJSON)));
} else if (error) {
console.log({error});
}
}
navigateToCategory(event){
var catId = event.currentTarget.dataset.id;
console.log({catId});
this[NavigationMixin.Navigate]({
type: 'standard__webPage',
attributes: {
url: '/category/' + catId
}
})
}
}
๐ Highlights:
- Navigations are handled using
NavigationMixin
.
๐งพ Meta Configuration
The component is exposed to App Pages and Community Pages:
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>56.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__AppPage</target>
<target>lightningCommunity__Page</target>
<target>lightningCommunity__Default</target>
</targets>
</LightningComponentBundle>
๐ Final Thoughts
This solution allows you to:
- Dynamically display deeply nested categories.
- Reuse the component across multiple experiences (App/Page/Community).
- Ensure a scalable and navigable user experience.