Salesforce SPA using LWC
Hello there!
How to create a Single Page Application (SPA) using Lightning Web Components?
Code is really simple, extendable, and very efficient.
Let's begin!
Demo
Architecture
spaContainer
- can be dragged & drop to the lightning or the experience cloud page (internal, external usage). The container is a kind of static layout that does not change when navigating. The whole structure is loaded once.spaNavigation
- allows navigate between pages.spaPages
- contains page id to HTML template mapping. Content change dynamically based on pageId in URL.spaFooter
Code
The whole code you can find here: SPA - LWC and SPA - Apex.
Container
- The brain of Single Page Application.
- It can be treated as a Layout, that contains static components.
- Use
wiredCurrentPageReference
to get information about current page. It fires every time the URL will change. - Use
wiredPagesConfig
to get page configs from the apex. In the current solution, the configuration is hardcoded, but you can prepare custom metadata to store the whole config and manage the application from the Salesforce interface.
// spaContainer.html
<template>
<h1 class="title">Single Page Application</h1>
<div class="container" if:true={isLoaded}>
<c-spa-navigation class="navigation" current-page-id={currentPageId} menu-items={menuItems} current-page-reference={currentPageReference}></c-spa-navigation>
<c-spa-pages class="content" current-page-id={currentPageId}></c-spa-pages>
<c-spa-footer class="footer"></c-spa-footer>
</div>
</template>
// spaContainer.js
import { LightningElement, wire } from 'lwc';
import { CurrentPageReference } from 'lightning/navigation';
import getPagesConfig from '@salesforce/apex/SpaController.getPagesConfig';
export default class SpaContainer extends LightningElement {
currentPageReference;
pagesConfig;
menuItems;
currentPageId;
isCurrentPageReferenceLoaded = false;
isPagesConfigLoaded = false;
@wire(CurrentPageReference)
wiredCurrentPageReference(currentPageReference) {
if (!currentPageReference) {
return;
}
this.currentPageReference = currentPageReference;
this.isCurrentPageReferenceLoaded = true;
this.setCurrentPage();
}
@wire(getPagesConfig)
wiredPagesConfig({ data }) {
if (!data) {
return;
}
this.pagesConfig = data;
this.setNavigationMenu();
this.setLandingPage();
this.isPagesConfigLoaded = true;
this.setCurrentPage();
}
get isLoaded() {
return this.isCurrentPageReferenceLoaded && this.isPagesConfigLoaded;
}
setNavigationMenu() {
this.menuItems = [...this.pagesConfig]
.sort((a, b) => (a.menuOrder > b.menuOrder ? 1 : -1))
.map(page => ({
label: page.name,
pageId: page.pageId
}));
}
setLandingPage() {
this.landingPage = this.pagesConfig.find(page => page.isLandingPage);
}
setCurrentPage() {
if (!this.isLoaded) {
return;
}
this.currentPageId = this.currentPageReference?.state?.c__page || this.landingPage?.pageId;
}
}
Navigation
navigate
replacec__page
in url, what automatically fireswiredCurrentPageReference
fromspaContainer.js
.
// spaNavigation.html
<template>
<div class="navigation" if:true={menuItems}>
<lightning-vertical-navigation selected-item={currentPageId}>
<lightning-vertical-navigation-section label="Main Menu" >
<template for:each={menuItems} for:item="menuItem">
<lightning-vertical-navigation-item-icon key={menuItem.pageId} label={menuItem.label} data-page={menuItem.pageId} name={menuItem.pageId} onclick={navigate}>
</lightning-vertical-navigation-item-icon>
</template>
</lightning-vertical-navigation-section>
</lightning-vertical-navigation>
</div>
</template>
// spaNavigation.js
import { LightningElement, api } from 'lwc';
import { NavigationMixin } from 'lightning/navigation';
export default class SpaNavigation extends NavigationMixin(LightningElement) {
@api currentPageId;
@api menuItems;
@api currentPageReference;
navigate(event) {
this[NavigationMixin.Navigate](
Object.assign({}, this.currentPageReference, {
state: Object.assign({}, this.currentPageReference.state, {
c__page: event.target.dataset.page
})
}),
false // Push to browser history stack
);
}
}
Pages
- Simple mapping between pageId and template.
- Proper page is rendered based on
currentPageId
. - HTML templates can contain another LWC components.
- Unrecognized
currentPageId
will loadnotFound
template.
// spaPages.js
import { LightningElement, api } from 'lwc';
import home from './templates/home.html';
import services from './templates/services.html';
import about from './templates/about.html';
import contact from './templates/contact.html';
import notFound from './templates/404.html';
const PAGE_ID_TO_TEMPLATE = {
home,
services,
about,
contact,
notFound
};
export default class SpaPages extends LightningElement {
@api currentPageId;
render() {
return PAGE_ID_TO_TEMPLATE[this.currentPageId] || PAGE_ID_TO_TEMPLATE.notFound;
}
}
Use Cases
- Application for the Internal Users (Lightning Page) - You can drag & drop the SPA component to Lightning Page and enjoy a great user experience.
- Modals/Popups with a few pages. - Sometimes business process has a few pages where data needs to be provided. Build dynamic UI and improve navigation between pages.
- Application for the External Users (Experience Cloud) - You don't need to create a lot of pages. Create one, add a SPA component and that's all!
- Better user experience - Replace the solution where the user needs to jump between different pages and wait for loading.
Repository
If you have any questions feel free to ask in the comment section below. 🙂
Was it helpful? Check out our other great posts here.