Piotr Gajek
written byPiotr Gajek
posted on August 27, 2022
Technical Architect and Full-stack Salesforce Developer. He started his adventure with Salesforce in 2017. Clean code lover and thoughtful solutions enthusiast.

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

salesforce spa demo

Architecture

spa in lwc 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 replace c__page in url, what automatically fires wiredCurrentPageReference from spaContainer.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 oncurrentPageId.
  • HTML templates can contain another LWC components.
  • Unrecognized currentPageId will load notFound 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

SPA - LWC
SPA - Apex


If you have any questions feel free to ask in the comment section below. 🙂

Was it helpful? Check out our other great posts here.


References

Buy Me A Coffee