How I fixed htmx redeclaration of let element with hx-boost=true

 · 10 min read
 · Baseplate-Admin
Last updated: June 19, 2023
Table of contents

htmx is gaining popularity in django ecosystem. For example, I have created my latest project with django + htmx. The sites user page was previously written using svelte but I was not using django to it's full capability. ( django templating language is such a blessing | I dont even have to deal with tokens and refresh and all other nonsenses )

Thats when I started rewriting user from svelte to django + django_cripsy_forms

The problem

When I was writing the user code in django I planned to do realtime lookup of username availablity. So thats what I did.

Heres the django code :

<!--signup.html-->
<!--Bunch-of-code-rendered-by-django-crispy-forms-->

<!--This is the problematic code block-->
<script>
    let el = document.querySelector('#id_username');

    el.addEventListener('input', async () => {
        // Do a bunch of works here
    });
</script>

Soon enough I was running into this error ( with hx-boost=true in the div after body tag ) whenever I revisited the page.

Uncaught SyntaxError: redeclaration of let el
    <anonymous> http://127.0.0.1:8000/user/login/ line 1 > injectedScript:1
    ct http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    ht http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    Y http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    ht http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    ue http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    o http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    Y http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    o http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    setTimeout handler*o http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    fr http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    onload http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    lr http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    Ue http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    I http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    ze http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    Y http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    ze http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    Ue http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    Ue http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    pt http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    mt http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    Y http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    mt http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    ue http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    o http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    Y http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    o http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    setTimeout handler*o http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    fr http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    onload http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    lr http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    Ue http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    I http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    ze http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    Y http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    ze http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    Ue http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    Ue http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    pt http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    mt http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    Y http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    mt http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    ue http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    o http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    Y http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    o http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    setTimeout handler*o http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    fr http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    onload http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    lr http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    Ue http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    I http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    ze http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    Y http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    ze http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    Ue http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    Ue http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    pt http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    mt http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    Y http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    mt http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    <anonymous> http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    pr http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    <anonymous> http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    <anonymous> http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    <anonymous> http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
    <anonymous> http://127.0.0.1:8000/static/js/htmx/htmx.min.js:1
line 1 > injectedScript:1:1

This was something I never ran into before. As HTMX was quite new I didn't find much resource on this weird error. I asked on their discord channel about this particular error ( this wasn't that uncommon as another user reported this error on their discord thread ). But I didn't find any response. Do I started digging deeper into this mess.

At a glance nothing seemed wrong ( I wrote couple project in react, svelte | So I had a good understanding of JS ).. Maybe because I was not familiar with django and html I thought. I have never stumbled upon this error in my life before.

Digging deeper

So after a while I thought of removing htmx and just doing things the plain django way. Things started to work nicely again. So was it related to htmx afterall? htmx for those of you dont know does things in a particular way with hx-boost.

  1. It fetches the anchor tags html.
  2. Replaces body with said html.
  3. Re executes scripts inside those body tags.

So naturally I thought of disabling execution of script tags by moving the scripts to the head. It worked :D.

But according to htmx author. After 2.0 release htmx would re execute the scripts on the head. At the time of writing there is head-support extension that enables said functionality and it was to be merged into htmx by 2.0 release.

The Solution

So I was out of hope at this point. Then I stumbled upon this stackoverflow thread ( god bless stackoverflow ). Here I learned about scoping functions. I was familiar with function scoping back in my react days as react didn't support top level await codes and I had to do something like this :

(async () => {
    await fetch('');
})();

So I modified my code like this :

<script>
    (() => {
        let el = document.querySelector('#id_username');

        el.addEventListener('input', async () => {
            // Do a bunch of works here
        });
    })();
</script>

and the error was fixed. ( MAGICALLY!! )

Magic GIF

But wait there's no magic actually

This type of scoping function doesn't work in all cases. For example, if you go to search page on this blog you can see that it's not enhanced. What gives ?

It seems that whenever i was nagivating to that link while using flamethrower-router the webpack was broken for some reason ( maybe i will move away from flamethrower-router at some point ).

So your milage may vary on this front ( should all of your hyperlinks work depends greatly on the libraries you use on your website )

For me the hx-boost worked with my stack :

  • Django
  • Tailwind
  • HTMX
  • Alpine
  • jQuery

Moving forward

If you see the pages source. You can see that the site is not written in some fancy JavaScript framework but it feels like an SPA ( Single Page Application ). Thats because the site is using a small JavaScript library called flamethower-router ( for those of you who follow fireship.io you guys should know what this is ).

But I ran into the exact same error that I faced with htmx ( maybe its the issue with these frontend routers ). When i ran the site without scoping the script tag i ran into :

Uncaught SyntaxError: redeclaration of let themeToggleDarkIcon
    <anonymous> how-i-fixed-htmx-redeclaration-of-let-element-with-hx-boosttrue.html line 7 > injectedScript:1
    s main.js:92
    n main.js:86
    reconstructDOM main.js:197
    go main.js:104
    onclick (index):1
how-i-fixed-htmx-redeclaration-of-let-element-with-hx-boosttrue.html line 7 > injectedScript:1:1

So I modified the theme's original script to be scoped.

(() => {
    let themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
    let themeToggleLightIcon = document.getElementById(
        'theme-toggle-light-icon'
    );
    if (
        localStorage.getItem('color-theme') === 'dark' ||
        (!('color-theme' in localStorage) &&
            window.matchMedia('(prefers-color-scheme: dark)').matches)
    ) {
        themeToggleLightIcon.classList.remove('hidden');
    } else {
        themeToggleDarkIcon.classList.remove('hidden');
    }
    let themeToggleBtn = document.getElementById('theme-toggle');
    themeToggleBtn.addEventListener('click', function () {
        themeToggleDarkIcon.classList.toggle('hidden');
        themeToggleLightIcon.classList.toggle('hidden');
        if (localStorage.getItem('color-theme')) {
            if (localStorage.getItem('color-theme') === 'light') {
                document.documentElement.classList.add('dark');
                localStorage.setItem('color-theme', 'dark');
            } else {
                document.documentElement.classList.remove('dark');
                localStorage.setItem('color-theme', 'light');
            }
        } else {
            if (document.documentElement.classList.contains('dark')) {
                document.documentElement.classList.remove('dark');
                localStorage.setItem('color-theme', 'light');
            } else {
                document.documentElement.classList.add('dark');
                localStorage.setItem('color-theme', 'dark');
            }
        }
    });
})();

Moving forward I hope to see more sites powered using a frontend SPA router and finally put an end to the pesky page refresh.

— Baseplate-admin

If you have see any problems in this article please raise an issue the github repository