0

It's probably my poor knowledge in JS but I am stuck. My issue is that anything I place where !!!PROBLEM IS HERE!!! (see code below) is located, is set to the last iteration, here water as key and "{% static 'map/img/modes/water.png'%}" as value.

template.html

<script>
    for (mode in modes) {
        btn_mode = new L.easyButton('<img src="' + modes[mode] + '" style="width: 24px; height: 24px;"/>', function(btn, map){

            window.location='/' + mode; <--------- !!!PROBLEM IS HERE!!!

        },
        'Mode "' + mode + '" information here'
        );
        btn_mode.addTo(map).setPosition('topleft');
    }
</script>

with modes looking like this (in reality I collect this data in Python, dump it as JSON and pass it onto my map template using the Django template language):

template.html

// Normally retrieved by calling "var modes = JSON.parse('{{ modes | safe }}');"
var modes = {
            'default': "{% static 'map/img/modes/default.png'%}",
            'agriculture': "{% static 'map/img/modes/argriculture.png'%}",
            'commerce': "{% static 'map/img/modes/commerce.png'%}",
            'energy': "{% static 'map/img/modes/energy.png'%}",
            'environment': "{% static 'map/img/modes/environment.png'%}",
            'geology': "{% static 'map/img/modes/geology.png'%}",
            'health': "{% static 'map/img/modes/health.png'%}",
            'insurance': "{% static 'map/img/modes/insurance.png'%}",
            'vegetation': "{% static 'map/img/modes/leaf.png'%}",
            'military': "{% static 'map/img/modes/military.png'%}",
            'social': "{% static 'map/img/modes/social.png'%}",
            'weather': "{% static 'map/img/modes/weather.png'%}",
            'thermal': "{% static 'map/img/modes/thermal.png'%}",
            'water': "{% static 'map/img/modes/water.png'%}",
}

I don't get this problem if I e.g. place a simple console.log(mode + ' : ' + modes[mode]) at the beginning or the end of the loop's body. I also do not get this issue from the first (icon source) and third (tooltip message) argument of the EasyButton constructor call. Is there some general JS thing I am missing here or perhaps is it specific to this Leaflet plugin?

Given a JSON dictionary I would like to populate my Leaflet map with buttons that

  • automatically get a specific icon assigned based on the current item's value (in my case Django static image file)
  • automatically redirect the user based on the current item's key, which represents a Django view.

My intention is to automatically populate the buttons based on the functionality I have implemented (apps and the respective views) and trigger a redirection to a different view (e.g. 127.0.0.1:8000/environment for the environment button using the environment.png static file as an icon). All of this is of course mostly done on the Python side, where the Django apps are installed and managed. Here is how it's currently being visualized:

enter image description here

Minimal reproducible example

The problem is not related to Django. Also I have tried moving the declaration of btn_mode outside of the loop as well as put everything inside an outside-of-the-loop defined array. Result is always the same.

I work with Eclipse (Django Plugin) but the following example is much easier to check out using e.g. VS Code and the Five Server (or similar simple server for live preview of web content) extension. The Leaflet zoom controls and the world mini map plugins can be removed. I left those so that visually it comes closer to what I am looking for.

<!DOCTYPE html>
<html lang="en">

<head>
    <base target="_top">
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Map</title>

    <link rel="shortcut icon" type="image/x-icon" href="#" />

    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css"
        integrity="sha256-kLaT2GOSpHechhsozzB+flnD+zUyjE2LlfWPgU04xyI=" crossorigin="" />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet-easybutton@2/src/easy-button.css">
    <script src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js"
        integrity="sha256-WBkoXOwTeyKclOHuWtc+i2uENFpDZ9YPdf5Hf+D7ewM=" crossorigin=""></script>
    <script src="https://cdn.jsdelivr.net/gh/maneoverland/leaflet.WorldMiniMap@1.0.0/dist/Control.WorldMiniMap.js"
        integrity="sha512-PFw8St3qenU1/dmwCfiYYN/bRcqY1p3+sBATR+rZ6622eyXOk/8izVtlmm/k8qW7KbRIJsku838WCV5LMs6FCg=="
        crossorigin=""></script>
    <script src="https://cdn.jsdelivr.net/npm/leaflet-easybutton@2/src/easy-button.js"></script>

    <style>
        html, body {
        height: 100%;
        margin: 0;
    }
    .leaflet-container {
        height: 100%;
        width: 100%;
        max-width: 100%;
        max-height: 100%;
    }
    </style>
</head>

<body>
    <div id="map" style="width: 100%; height: 100%;"></div>
    <script>
        const map = L.map('map').setView([48.947012, 8.411119], 13);

        const tiles = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
            maxZoom: 19,
            attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
        }).addTo(map);
        var worldMiniMap = L.control.worldMiniMap(
            {
                position: 'topright',
                style: {
                    opacity: 0.8,
                    borderRadius: '0px',
                    backgroundColor: 'lightblue'
                }
            }).addTo(map);
    </script>
    <script>
        var modes = {
            'a' : 'https://www.freeiconspng.com/uploads/close-icon-46.jpg',
            'b' : 'https://www.freeiconspng.com/uploads/close-icon-46.jpg',
            'c' : 'https://www.freeiconspng.com/uploads/close-icon-46.jpg',
        }
    </script>
    <script>
        for (var mode in modes) {
            console.debug(mode + ' | path "' + modes[mode] + '"');
            var btn_mode = L.easyButton('<img src="' + modes[mode] + '" style="width: 24px; height: 24px;"/>',  function(btn, map){
                window.location='/' + mode;
            },
            'TODO Mode "' + mode + '" information'
            );
            btn_mode.addTo(map).setPosition('topleft');
        }

    </script>
</body>

</html>

What you will see here is that, upon clicking on any of the three buttons, the page will redirect you to <some host>/c.

rbaleksandar
  • 8,713
  • 7
  • 76
  • 161
  • Prob. something wrong with assigning `btn_mode`. Where is this variable defined? Have you tried putting `let` before defining variables? :) I also recommend you enabling strict mode in javascript. – zerdox Mar 01 '23 at 08:12
  • I tried defining it inside the loop (as you see it now), outside the loop and even skip it all together and just create `new Array()` outside of the loop an push `L.easyButton(...)` in there with a second loop iterating through it and calling `addTo(map)`. In C++ I would just use `new` and create a pointer to the object on the heap. My assumption was/still is that the Leaflet map takes ownership of the object. It's rather confusing especially since the `tooltip` and path to the file inside the `` tag are actually working. My `` tag doesn't currently contain anything else. – rbaleksandar Mar 01 '23 at 08:51
  • Same issue occurs if I were to put the code in a separate JS file and simply reference it inside my HTML (which I will hopefully do later on once the problem is resolved). – rbaleksandar Mar 01 '23 at 08:52
  • Would be great if you set up playgroud with enabled leaflet and easybuttons you use. Minimal reproductions are very useful when you are trying to get some help. Isolating issue may also help you to resolve it on your own – zerdox Mar 01 '23 at 09:01
  • Will try to make a simple Leaflet example. The problem is not in Django imho. – rbaleksandar Mar 01 '23 at 09:13
  • Does this answer your question? [addEventListener using for loop and passing values](https://stackoverflow.com/questions/19586137/addeventlistener-using-for-loop-and-passing-values) – zerdox Mar 01 '23 at 10:15

1 Answers1

1

The problem with your code occured due to misundertanding that mode variable exists only in "iterating" scope.

When you write function inside loop that refer to "that" variable and this function will called when loop already ended iterating (on click), and you will get only last iteration variable value (c in your case).

What am I talking about is called closure. You can see people stumbled with it here for example (practically your question is just another duplicate): addEventListener using for loop and passing values

To fix your code instead passing function directly you can call another function which will return function:

for (const mode in modes) {
    const btn_mode = L.easyButton(
        `<img src="${modes[mode]}">`,
        /* function(btn, map) {
             window.location='/' + mode;
        },*/
        createClickHandler(mode)
        mode,
    );
}
function createClickHandler(mode) {
    return function(btn, map) {
        window.location='/' + mode;
    }
}

I came up with a bit different solution which a bit more elegant IMO:

for (const mode in modes) {
    const btn_mode = L.easyButton(
        `<img src="${modes[mode]}">`,
        function(btn, map) {
            window.location = "/" + btn.button.title;
        },
        mode,
    );
}

But it requires a bit understanding what is going on here...

zerdox
  • 830
  • 8
  • 22
  • I am new to JS. Don't even know if `new` (like in C++) is a thing here. LOL I guess I thought that I am actually allocating stuff on the heap here and then the `map` object is taking the dynamically allocated buttons. I guess that's not the case. :D Thanks a lot for the solution. I tried anything but `const`. XD Fun fact: I used to have a function handler in a previous version of the code but, for God knows what reason, decided to dump it. – rbaleksandar Mar 01 '23 at 12:33
  • As for the more elegant solution: if I were to be interested in the button's title, it would be. But in my case a button is just a clickable picture with a tooltip and a redirection functionality behind it. No titles, no (at least for now) extra styling. – rbaleksandar Mar 01 '23 at 12:35
  • @rbaleksandar `const` is not the case tho.. I hope you now understand what closure is... – zerdox Mar 01 '23 at 13:32
  • Yes, I just finished reading about closures (Mozilla dev article). Thanks again. – rbaleksandar Mar 01 '23 at 14:36