symfonybootstrap-5symfony-assetmapper

Executing JavaScript on page load in Symfony 7 using AssetMapper fails after following a link


I'm trying to implement BootStrap's color switcher in Symfony.

I've added BootStrap using AssetMapper and everything is working.

I've then copied the color switcher from BootStrap's examples page into a twig template along with it's CSS and JS code (as assets).

I have a Controller routed to / whose template includes the color switcher template.

Everything appears to work when I open the page in the browser, however if I add a link in that page - to itself or to a different page, doesn't matter - the color switcher won't work in the new page, unless I press F5 or reload the page in some other fashion.

I've discovered that the JavaScript code that initializes the color switcher is run on the initial page load or when I reload, but when changing pages by clicking a link it does not execute again on the destination page.

I've tried adding something like:

if (document.readyState !== 'loading') {
    console.log('document is already ready, just execute code here');
    initThemeSwitcher();
} else {
    document.addEventListener('DOMContentLoaded', function () {
        console.log('document was not ready, place code here');
        initThemeSwitcher();
    });
}

and whatching the console, again, only on initial page load or a forced reload will this code run.

So my question is: Why will this code only run when the file is fetched from the server and not on moving to a different page (or same page) by clicking a link?

EDIT Just noticed the same thing happens with Stimulus Controllers: They work on page load but the moment I follow a link, they stop working until I reload the page (F5 on keyboard for example)


Solution

  • I'm still not sure why adding an EventListener to DOMContentLoaded only works on the first page loaded, but I managed to get a workaround.

    Instead of using the code "as is" from the bootstrap site, I made a Stimulus Controller out of it - with a few changes to the code so it wouldn't use DOMContentLoaded.

    I ended up with the following Stimulus Controller:

    import { Controller } from '@hotwired/stimulus';
    
    export default class extends Controller {
        'use strict'
        connect() {        
            const preferredTheme = this.getPreferredTheme()
            this.setTheme(preferredTheme)
            this.showActiveTheme(preferredTheme, false)
    
            window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
                const storedTheme = this.getStoredTheme()
                if (storedTheme !== 'light' && storedTheme !== 'dark') {
                    this.setTheme(this.getPreferredTheme())
                }
            })
            
            document.querySelectorAll('[data-bs-theme-value]').forEach(toggle => {
                toggle.addEventListener('click', () => {
                    const theme = toggle.getAttribute('data-bs-theme-value')
                    this.setStoredTheme(theme)
                    this.setTheme(theme)
                    this.showActiveTheme(theme, true)
                })
            })
        }
        
        getStoredTheme() {
            return localStorage.getItem('theme')
        }
        
        setStoredTheme(theme) {
            localStorage.setItem('theme', theme)
        }
    
        getPreferredTheme() {
            const storedTheme = this.getStoredTheme()
            if (storedTheme) {
                return storedTheme
            }
    
            return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
        }
    
        setTheme(theme) {
            if (theme === 'auto') {
                document.documentElement.setAttribute('data-bs-theme', (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'))
            } else {
                document.documentElement.setAttribute('data-bs-theme', theme)
            }
        }
    
        showActiveTheme(theme, focus = false) {
            const themeSwitcher = document.querySelector('#bd-theme')
    
            if (!themeSwitcher) {
                return
            }
    
            const themeSwitcherText = document.querySelector('#bd-theme-text')
            const activeThemeIcon = document.querySelector('.theme-icon-active use')
            const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`)
            const svgOfActiveBtn = btnToActive.querySelector('svg use').getAttribute('href')
    
            document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
                element.classList.remove('active')
                element.setAttribute('aria-pressed', 'false')
            })
    
            btnToActive.classList.add('active')
            btnToActive.setAttribute('aria-pressed', 'true')
            activeThemeIcon.setAttribute('href', svgOfActiveBtn)
            const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`
            themeSwitcher.setAttribute('aria-label', themeSwitcherLabel)
    
            if (focus) {
                themeSwitcher.focus()
            }
        }
    }