SPA with Lightning Web Components (LWC)

Hi!
Today I would like to show you Singe Page Application using Lightning Web Components (LWC).

I created some Salesforce Community and a few components:

  • Container – It contains all pages, is some kind of a root element.
  • Page A, Page B, Page C – Test pages, I switch between them.
  • Base – Component contains whole logic, which is using by Page A, B, and C. Base is a parent component.
  • Utils – Here we have pages settings.

    Check Architecture Overview below.

Architecture Overview

App Demo

We have three pages: A, B, C. Each page has the possibility to redirect a user to another page. You can also click “back” arrow or “forward” arrow to navigate between pages or use your browser navigation buttons.

NOTE!
– It works with browse navigation arrows as well.
– It works even if you refresh the page.

Implementation

As I said earlier I used Lightning Web Components (LWC). I focused only on logic, to pages styles I used SLDS.

utils.js

const PAGES = {
    A: { cmp: 'a-cmp', id: 'A' },
    B: { cmp: 'b-cmp', id: 'B' },
    C: { cmp: 'c-cmp', id: 'C' }
}

export {
    PAGES
};

base.js

import { LightningElement } from 'lwc';
import { PAGES } from 'c/utils';

export default class Base extends LightningElement {
    
    handleRedirect(e) {
        this.redirectUserToPage(e.target.dataset.page);
    }

    redirectUserToPage(pageId) {
        this.setBrowserHistory(PAGES[pageId]);
        this.fireRedirectEvent(PAGES[pageId]);
    }

    handleMoveForward() {
        history.forward();
    }

    handleGoBack() {
        history.back();
    }

    setBrowserHistory(selectedPage) {
        let pageURLParam = '?page=' + selectedPage.id;
        history.pushState(selectedPage, selectedPage.title, pageURLParam);
    }

    fireRedirectEvent(selectedPage) {
        this.dispatchEvent(new CustomEvent('pagechange', { detail: selectedPage.id}));
    }

}

aPage.html

<template>
    <div class="slds-box slds-align_absolute-center">
        <div class="slds-text-heading_large slds-m-around_small">Page A</div>
        <lightning-button-icon icon-name="utility:back" onclick={handleGoBack} class="slds-m-around_small" ></lightning-button-icon>
        <lightning-button-icon icon-name="utility:forward" onclick={handleMoveForward} class="slds-m-around_small" ></lightning-button-icon>
        <button class="slds-button slds-button_neutral slds-m-around_small" data-page='B' onclick={handleRedirect}>Go to Page B</button>
        <button class="slds-button slds-button_neutral slds-m-around_small" data-page='C' onclick={handleRedirect}>Go to Page C</button>
    </div>
</template>

aPage.js

import Base from 'c/base';

export default class APage extends Base {}

bPage.html

<template>
    <div class="slds-box slds-align_absolute-center">
        <div class="slds-text-heading_large slds-m-around_small">Page B</div>
        <lightning-button-icon icon-name="utility:back" onclick={handleGoBack} class="slds-m-around_small" ></lightning-button-icon>
        <lightning-button-icon icon-name="utility:forward" onclick={handleMoveForward} class="slds-m-around_small" ></lightning-button-icon>
        <button class="slds-button slds-button_neutral slds-m-around_small" data-page='A' onclick={handleRedirect}>Go to Page A</button>
        <button class="slds-button slds-button_neutral slds-m-around_small" data-page='C' onclick={handleRedirect}>Go to Page C</button>
    </div>
</template>

bPage.js

import Base from 'c/base';

export default class BPage extends Base {}

cPage.html

<template>
    <div class="slds-box slds-align_absolute-center">
        <div class="slds-text-heading_large slds-m-around_small">Page C</div>
        <lightning-button-icon icon-name="utility:back" onclick={handleGoBack} class="slds-m-around_small" ></lightning-button-icon>
        <lightning-button-icon icon-name="utility:forward" onclick={handleMoveForward} class="slds-m-around_small" ></lightning-button-icon>
        <button class="slds-button slds-button_neutral slds-m-around_small" data-page='B' onclick={handleRedirect}>Go to Page B</button>
        <button class="slds-button slds-button_neutral slds-m-around_small" data-page='A' onclick={handleRedirect}>Go to Page A</button>
    </div>
</template>

cPage.js

import Base from 'c/base';

export default class CPage extends Base {}

container.html

<template>

    <template if:true={isPageA}>
        <c-a-page onpagechange={handleChildComponentsRedirection}></c-a-page>
    </template>
    
    <template if:true={isPageB}>
        <c-b-page onpagechange={handleChildComponentsRedirection}></c-b-page>
    </template>
    
    <template if:true={isPageC}>
        <c-c-page onpagechange={handleChildComponentsRedirection}></c-c-page>
    </template>

</template>

container.js

import { LightningElement, track } from 'lwc';
import { PAGES } from 'c/utils';

const DEFAULT_PAGE = PAGES.A.id;

export default class Container extends LightningElement {

    @track currentPage;

    constructor() {
        super();
        //Page refresh
        this.currentPage = this.getPageParamFromUrl();
        //JS History changes listner - go, back
        window.onpopstate = (event) => {
            this.currentPage = event.state && event.state.id ? event.state.id : DEFAULT_PAGE;
        }
    }

    handleChildComponentsRedirection(e) {
        this.currentPage = e.detail;
    }
    
    get isPageA() {
        return this.checkIfIsCurrentPage(PAGES.A.id);
    }

    get isPageB() {
        return this.checkIfIsCurrentPage(PAGES.B.id);
    }

    get isPageC() {
        return this.checkIfIsCurrentPage(PAGES.C.id);
    }

    checkIfIsCurrentPage(pageId) {
        return this.currentPage === pageId;
    }

    getPageParamFromUrl() {
        let urlPageId = new URL(window.location.href).searchParams.get("page");
        if (urlPageId && PAGES[urlPageId]) {
            return urlPageId;
        }
        return  DEFAULT_PAGE;
    }
}

If you want to add a new page, make the following steps:

  • Create a new page component e.g: dPage
  • Import and extend it by Base
  • Configure PAGES variable in utils.js
  • Add new get method to container.js
get isPageD() {
   return this.checkIfIsCurrentPage(PAGES.D.id);
}
  • Add a new if:true statement to container.html
    <template if:true={isPageD}>
        <d-d-page onpagechange={handleChildComponentsRedirection}></d-d-page>
    </template>
  • Add your own logic and use redirectUserToPage(pageId) to redirect user.

Repository

https://github.com/pgajek2/SPA-in-LWC

Summary

This type of logic can be useful for a small project where you don’t need very complicated routing. You can use it also for LWC in Visualforce pages (e.g you need to add a new future to community-based on VF, you can create a new VF page and embed there LWC).

If you have some suggestions or questions, feel free to ask me in the comments section below! Maybe we can together improve this solution!

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

Resource

5 8 votes
Article Rating
Subscribe
Notify of
guest
6 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments
Rat
Rat
1 year ago

Hi Piotr , Is it possible to get the source code/ github link for this example .

peter
peter
1 year ago

awesome

SFDC fork
SFDC fork
1 year ago

I Implemented the same code but its not working. Both forward and back buttons are not working. For understanding i just copy paste your code

Close Menu
6
0
Would love your thoughts, please comment.x
()
x