Populating a Leaflet map with EasyButton instances in JavaScript using a loop results in onclick function reflecting only the last iteration

huangapple go评论77阅读模式
英文:

Populating a Leaflet map with EasyButton instances in JavaScript using a loop results in onclick function reflecting only the last iteration

问题

以下是您要翻译的部分:

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][1]][1]

**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.

请注意,我已经删除了代码部分并只保留了文本内容。如果您需要任何其他帮助,请随时告诉我。

英文:

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:

Populating a Leaflet map with EasyButton instances in JavaScript using a loop results in onclick function reflecting only the last iteration

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: '© <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.

答案1

得分: 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): https://stackoverflow.com/questions/19586137/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...

英文:

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): https://stackoverflow.com/questions/19586137/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(
        `&lt;img src=&quot;${modes[mode]}&quot;&gt;`,
        /* function(btn, map) {
             window.location=&#39;/&#39; + mode;
        },*/
        createClickHandler(mode)
        mode,
    );
}
function createClickHandler(mode) {
    return function(btn, map) {
        window.location=&#39;/&#39; + 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(
        `&lt;img src=&quot;${modes[mode]}&quot;&gt;`,
        function(btn, map) {
            window.location = &quot;/&quot; + btn.button.title;
        },
        mode,
    );
}

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

huangapple
  • 本文由 发表于 2023年3月1日 16:00:39
  • 转载请务必保留本文链接:https://go.coder-hub.com/75600917.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定