Dave L
Dave L

Reputation: 3535

How Can I Reliably Hide a Bootstrap 5.3 Modal on Back Navigation in a Multi‑Page App?

I'm using a Bootstrap 5.3 modal as a preloader when a user submits a form in my multi‑page application. The modal shows while waiting for the server response and then the user is taken to a new page. However, when the user clicks the browser’s back button to return to the form page, the modal is still visible—even though it should be hidden.

The modal has the fade class, so I assume there may be a race condition in play here.

I've tried several approaches (including listening for popstate and pageshow events) and handling potential race conditions with the modal’s shown.bs.modal event.

I have seen a few similar posts on this subject, but they are either for older versions of Bootstrap, rely on jQuery, or involve disabling the first page's cache.

Example Code:

// ... form validation code here

// Define the modal element
const modalEl = document.getElementById( "processingModal" );

// Create a new modal instance in Bootstrap
const modalInstance = new bootstrap.Modal( this.modalEl, {
    backdrop: 'static',
    focus: true,
    keyboard: false
} );

// show the modal, letting the user know the form is processing
modalInstance.show();

// ... submit the form

Upvotes: 0

Views: 33

Answers (1)

Dave L
Dave L

Reputation: 3535

After a lot of experimentation, I found that handling Bootstrap 5.3 modals in multi‑page apps requires some extra care. The key issues were:

  • BFCache and Navigation: When a user navigates back, the page can be restored from the BFCache. In these cases, events like popstate might not fire, but pageshow does (with event.persisted set to true). This allows you to detect that the page was restored.

  • Race Conditions: There was a race condition where the modal’s shown.bs.modal event fired after I requested a hide(). I solved this by using a flag (shouldBeHidden) and a one‑time listener on shown.bs.modal so that if the modal finishes showing while a hide was requested, it is immediately hidden.

  • Fade Animation: I temporarily remove the fade class in the force‑hide method to ensure that any ongoing animation doesn’t interfere with closing the modal. In this example, I want the modal to fade-in, but not fade out.

I hope this helps anyone facing similar issues with Bootstrap 5 modals in a multi‑page application! If you have a more elegant or reliable solution, please share so we can all learn from one another.

Tested this solution on: Windows: Chrome, Firefox, Android: Chrome, Firefox IOS: Safari

// PreloaderModal.js (JS Module)
export default class PreloaderModal {
    
    /**
     * Initialize the modal.
     */
    constructor() {
        // Define the modal element
        this.modalEl = document.getElementById( "processingModal" );
        
        // Create a new modal instance in Bootstrap
        this.modalInstance = new bootstrap.Modal( this.modalEl, {
            backdrop: 'static',
            focus: true,
            keyboard: false
        } );

        // This flag to determine if the modal should be hidden, defaults to no.
        this.shouldBeHidden = false;

        // Bind event handlers to ensure proper context
        this.onPageShow = this.onPageShow.bind( this );
    }

    /**
     * Show the modal.
     */
    show() {
        // Reset the flag
        this.shouldBeHidden = false;

        // add fade from the classlist, just in case it was removed
        this.modalEl.classList.add( 'fade' );
        
        // Show the modal.
        this.modalInstance.show();

        // Add event listeners.
        window.addEventListener( 'pageshow', this.onPageShow );

        // attach a one-time listener to the modal for the shown event.
        // there's a possibility of a race condition where the modal is shown but should be hidden
        // if that happens, we hide it immediately.
        this.modalEl.addEventListener('shown.bs.modal', () => {
            if ( this.shouldBeHidden ) {
                this._forceHide();
            }
        }, { once: true });
    }

    /**
     * Hide the modal.
     */
    hide() {
        // Set a flag so that if the shown event fires later, we hide immediately.
        this.shouldBeHidden = true;
        this._forceHide()
    }

    /**
     * Force-hide the modal.
     */
    _forceHide() {
        
        // remove fade from the classlist
        this.modalEl.classList.remove( 'fade' );
        
        // Using internal state check if necessary; note _isShown is not public API.
        if (this.modalInstance._isShown) {
            this.modalInstance.hide();
        }
        
        // As a fallback, force-remove the "show" class and backdrop.
        this.modalEl.classList.remove('show');
        const backdrop = document.querySelector('.modal-backdrop');
        if ( backdrop ) {
            backdrop.remove();
        }
    }

    /**
     * When a pageshow event is detected, check to see if it was persisted (user clicked back/forward button)
     * if so, hide the modal
     * @param {*} event 
     */
    onPageShow( event ) {
        if ( event.persisted ) {
            this.hide();
        }
    }
}
// Using the JS Module
import PreloaderModal from "./components/PreloaderModal";

( function() {

    const preloader = new PreloaderModal();

    // ... do form validation etc.

    preloader.show();

    // ... submit the form

})();

Upvotes: 0

Related Questions