SPA/AJAX causing other javascript not to work

I’m using this repo to add SPA functionality to my site.

The general SPA feature is working well however my slider inside the content is not working. It appears that the Javascript for the slider does not reload on XHR load.

What can I do to reload this script (and others) on XHR load?

Cheers.

Hi @novella, you probably don’t need to reload any script – just define a function with the desired functionality, and call that again after populating the page with the new content.

BTW there's a bug in your code, which is preventing the `load()` function from getting called after clicking a link; in jQuery, there's no `classList.add()` (this would be the regular DOM API), the corresponding method would be `addClass()`:
$('#tabs-loader').addClass('load')

Edit: Ah scratch that, just noticed you defined your own $() function which does indeed return a DOM element.

1 Like

Thanks for your response,

How would I go about that?

Well what part exactly of the script do you want to run again? You mentioned a slider but there doesn’t appear to be one in your repo…

Sorry man, the repo I linked to was just the SPA functionality.

I’m using Siema.js for my slider. At the moment, the slider is called on page load with !(function (e, t) {.

What would I use to call it when the XHR loads?

No problem… well the documentation says the slider has still to be instantiated after loading the script:

<script src="siema.min.js"></script>
<script>
  new Siema();
</script>

So after loading new content, you don’t need to load siema.min.js again but just create new Siema() instances as necessary. Not sure if siema requires manual cleanup, but to be sure you might also destroy the existing slider beforehand:

let slider = new Siema()

function load (href, pushState) {
  const xhr = new XMLHttpRequest()

  slider.destroy()

  xhr.onload = function () {
    // ... populate content, then
    slider = new Siema()
  }
}

I’ve tried to implement this today and this either breaks Tabs or the Siema any way I do it.

Here is my the setup as of now:

(function () {
    'use strict';

    function load(href, pushState) {
        let slider = new siema();
        const content = $('main');
        const xhr = new XMLHttpRequest();
        slider.destroy();
        xhr.onload = function () {
            const d = xhr.responseXML;
            const dTitle = d.title || '';
            const dcontent = $('main', d);
            content.innerHTML = (dcontent && dcontent.innerHTML) || '';
            document.title = dTitle;
            if (pushState) {
                history.pushState({}, dTitle, href);
            }
            content.focus();
            window.scrollTo(0, 0);
            document.body.classList.remove("opened");
            slider = new Siema();
        };
        xhr.onerror = function () {
            // fallback to normal link behaviour (fail graciously)
            document.location.href = href;
            return;
        };
        xhr.open('GET', href);
        xhr.responseType = 'document';
        xhr.send();
    }

    function $(sel, con) {
        return (con || document).querySelector(sel);
    }

    /**
     * Search for a parent anchor tag outside a clicked event target
     *
     * @param {HTMLElement} el the clicked event target.
     * @param {number} maxNests max number of levels to go up.
     * @returns the anchor tag or null
     */
    function findAnchorTag(el, maxNests = 3) {
        for (let i = maxNests; el && i > 0; --i, el = el.parentNode) {
            if (el.nodeName === 'A') {
                return el;
            }
        }
        return null;
    }

    window.addEventListener('click', function (evt) {
        const el = findAnchorTag(evt.target);
        const href = el?.getAttribute('href');
        if (el && href) {
            if (
                href.startsWith('#') ||
                el.getAttribute('target') === '_blank' ||
                /\.\w+$/.test(href)
            ) {
                // eleventy urls in this configuration do not have .html extensions
                // if they have, or if target _blank is set, or they are a hash link,
                // then do nothing and load normally.
                return;
            }
            // initiate SPA
            evt.preventDefault();
            load(href, true);
        }
    });

    window.addEventListener('popstate', function (e) {
        load(document.location.pathname, false);
    });

})();

So this should be destroying Siema and then repopulating it on XHR. This is breaking the SPA functionality though so I have no way to identify if it is actually re-initing Siema.

Thanks for your help.

The constructor is misspelled here, and the let declaration is at the wrong place… have a closer look at my previous post, you’d need to keep a slider reference outside the load() function.

Okay so I have moved the reference out of load() function and this now breaks tabs.

(function () {
    'use strict';

    let slider = new Siema()

    function load(href, pushState) {
        const content = $('main');
        const xhr = new XMLHttpRequest();
        slider.destroy()
        xhr.onload = function () {
            slider = new Siema()
            const d = xhr.responseXML;
            const dTitle = d.title || '';
            const dcontent = $('main', d);
            content.innerHTML = (dcontent && dcontent.innerHTML) || '';
            document.title = dTitle;
            if (pushState) {
                history.pushState({}, dTitle, href);
            }
            content.focus();
            window.scrollTo(0, 0);
            document.body.classList.remove("opened");
        };
        xhr.onerror = function () {
            // fallback to normal link behaviour (fail graciously)
            document.location.href = href;
            return;
        };
        xhr.open('GET', href);
        xhr.responseType = 'document';
        xhr.send();
    }

    function $(sel, con) {
        return (con || document).querySelector(sel);
    }

    /**
     * Search for a parent anchor tag outside a clicked event target
     *
     * @param {HTMLElement} el the clicked event target.
     * @param {number} maxNests max number of levels to go up.
     * @returns the anchor tag or null
     */
    function findAnchorTag(el, maxNests = 3) {
        for (let i = maxNests; el && i > 0; --i, el = el.parentNode) {
            if (el.nodeName === 'A') {
                return el;
            }
        }
        return null;
    }

    window.addEventListener('click', function (evt) {
        const el = findAnchorTag(evt.target);
        const href = el?.getAttribute('href');
        if (el && href) {
            if (
                href.startsWith('#') ||
                el.getAttribute('target') === '_blank' ||
                /\.\w+$/.test(href)
            ) {
                // eleventy urls in this configuration do not have .html extensions
                // if they have, or if target _blank is set, or they are a hash link,
                // then do nothing and load normally.
                return;
            }
            // initiate SPA
            evt.preventDefault();
            load(href, true);
        }
    });

    window.addEventListener('popstate', function (e) {
        load(document.location.pathname, false);
    });

})();

Here is my Siema reference to enable multiple sliders:

  const siemas = document.querySelectorAll('.siema'); // allows for mutiple sliders on a page

  for (const siema of siemas) {
    new Siema({
      selector: siema,
      perPage: {
        0: 1,
        1024: 2,
        1280: 3
      },
      multipleDrag: true
    })
  }

Well where exactly are you doing this? In your load() function you’re now initializing a new Siema() before populating the content… so if you go back to post #6, it should be something like this:

content.innerHTML = (dcontent && dcontent.innerHTML) || ''
slider = new Siema()

When working with multiple carousels though, you’d need to store them in an array instead:

const sliders = []

function load () {
  const xhr = new XMLHttpRequest()

  sliders.forEach(slider => slider.destroy())

  xhr.onload = function () {
    // ... populate content, then
    sliders = Array.from(
      document.querySelectorAll('.siema'), 
      siema => new Siema({ selector: siema })
    )
  }
]

Carousel’s working okay now with Tabs however they do not initialize on page load too. Can I make it reference a new Siema on XHR load and initial page load.

This also breaks this code that references the Siema as well:

Surely I do not have to put this in under XHR load as well.

Then simply run the siema initialization code on page load as well… ideally put it in a function so as to not repeat yourself:

let sliders = []

function initSliders () {
  sliders.forEach(slider => slider.destroy())

  sliders = Array.from(
    document.querySelectorAll('.siema'), 
    siema => new Siema({ selector: siema })
  )
}

function load () {
  const xhr = new XMLHttpRequest()

  xhr.onload = function () {
    // ...
    initSliders()
  }
}

initSliders()