Leaflet Popups with Clojurescript - leaflet

I'm pretty sure this is a conceptual error, but I'm not sure where I'm making the incorrect call.
Following the leaflet tutorial, I'm trying to create a popup on a map. Per the tutorial, this is a simple operation
var popup = L.popup();
function onMapClick(e) {
popup
.setLatLng(e.latlng)
.setContent("You clicked the map at " + e.latlng.toString())
.openOn(mymap);
}
mymap.on('click', onMapClick);
However, when I translate this into clojurescript, I receive the following error:
Uncaught TypeError: t.openPopup is not a function
I'm 100% positive I'm calling the javascript incorrectly. I'm doing the following:
(defn onMapClick [e]
(let [popup (js/L.Popup.)]
(-> popup
(.setLatLng (.-latlng e))
(.setContent (str "You clicked the map at " (.-latlng e)))
(.openOn map))))
And then I call this as:
(.on map "click" onMapClick)
where map is correctly defined. (I say correctly because I am able to draw polygons and create popups that are bound to those polygons with .bindPopup)

I was thinking of not relying on return values at all. Just relying on the call sequence:
(defn onMapClick [e]
(let [popup (js/L.Popup.)]
(.setLatLng popup (.-latlng e))
(.setContent popup (str "You clicked the map at " (.-latlng e)))
(.openOn popup map)))
If the docs say that these js setter functions return popup then this won't make much of an answer!
You should be able to isolate the problem down to just one function call??

Related

Meteor/Blaze/Mongo/Leaflet - Dynamically filled Leaflet popups do not pass data to button for entry into database?

I'm working with a Leaflet map that displays marker data based on a MongoDB query. The query results are saved into a variable (I know, bad form for large volumes of info but okay if you only have ~25 pieces) as an array, and then I've iterated over that variable and it's stored information using a for loop to create my leaflet map markers and populate their popups with the information specific to each entry. This part works great.
this.autorun(function(){
fsqresults = FsqResults.find().fetch({});
container = $('<div />');
for (i=0; i < fsqresults.length; i++) {
marker = L.marker([fsqresults[i].geometry.coordinates[1], fsqresults[i].geometry.coordinates[0]], {icon: violetIcon}).addTo(mymap);
container.html("<b>" + "Name: " + "</b>" + fsqresults[i].properties.name + "<br>" +
"<b>" + "Address: " + "</b>" + fsqresults[i].properties.address + "<br>" +
"<b>" + "Checkins: " + "</b>" + fsqresults[i].properties.checkIns + "<br>" +
"<b>" + "Users: " + "</b>" + fsqresults[i].properties.usersCount + "<br>" +
"<b>" + "Tips: " + "</b>" + fsqresults[i].properties.tips + "<br>");
marker.bindPopup(container[0]);
} // end for loop
For each marker, there is a button to log a "checkin" event to another Mongo collection to house the checkin entries. The button fires an event successfully, and creates the entry into the second database, but will not bind the dynamically populated data to the entry so I can see which marker the user has clicked on.
container.append($('<button class="btn btn-sm btn-outline-primary js-checkin">').text("Check In"));
container.on('click', '.js-checkin', function() {
var currentVenue = fsqresults[i].properties.name;
console.log(currentVenue);
console.log("You clicked the button!");
if (!Meteor.user()) {
alert("You need to login first!");
}
if (Meteor.user()) {
console.log("Meteor User Verified");
Checkins.insert({user: Meteor.user(), name: currentVenue});
}
});
}); // end this.autorun
The console tells me that currentVenue is undefined. I know this has something to do with the fact that fsqresults is a dynamically populated variable. I have tried to find ways to "solidify" the information in it (i.e. - creating a second variable with an empty array, pushing the data from fsqresults into it, and then having the markers iterate over that variable) but that hasn't worked as the MongoDB query results, despite being in an array format themselves, will not push or concat into the variable with an empty array successfully.
I've been searching for an answer to this problem and I'm coming up short. I'm lost; is there any other solution which could be staring me in the face?
Some things to note: All of this code lives in the Template.map.onRendered() function. Leaflet has scoping issues if I delegate the code into helpers and events, which is why I haven't created a {markers} template and just done {{#each markers}} over it for iteration. Therefore I am relegated to jQuery style coding for creating DOM elements and firing event triggers. The code above is wrapped in a this.autorun function to ensure it does indeed run upon map rendering. I don't think this is the issue (although one can never rule it out!).
As pointed out in the question comments, you have a scope issue with your i index iterator, and there should be no technical problem in integrating Leaflet with Meteor (although with Blaze that may not be totally trivial nor interesting).
1. Iteration scope issue
The console tells me that currentVenue is undefined.
That is because you try to access fsqresults[i].properties.name in your container.on('click' event listener / callback, which will be called on user click, i.e. after your for loop is complete, hence your i index iterator variable will be equal to fsqresults.length.
You are in the case of Example 6 of the accepted answer of: How do JavaScript closures work?
2. Leaflet integration with Meteor (Blaze)
Since you mention having tried helpers, events, and {{#each markers}}, I assume you use Blaze as your Meteor rendering engine.
While React-Leaflet and Vue2Leaflet indeed offer the possibility to use a kind of "<Marker>" component (same for other types of Leaflet Layer), the latter is only for template declaration purpose, i.e. it does not directly render any DOM / HTML, but only calls some Leaflet methods, which will be in charge of manipulating the DOM. As stated on React-Leaflet limitations:
The components exposed are abstractions for Leaflet layers, not DOM elements.
Side note: interesting to see that angular-leaflet-directive and #asymmetrik/ngx-leaflet did not fall into the same temptation and sticked to JS declaration of Leaflet layers.
Therefore trying to create a Template.Marker (used as {{> Marker}}) in Blaze might be overkill, as you would basically just call some Leaflet factories (like L.marker) within your Template.Marker.onCreated (and needing to access somehow the parent map object to add your Marker into…), without rendering any DOM node yourself (i.e. you would have an HTML file with empty <template name="Marker"></template>).
While we forget about a Marker template in Blaze (as you have already done), we can still leverage Blaze events management to handle user clicks in your Leaflet Popup. For that, we would need a few Blaze features, that I admit could benefit being better documented:
We can attach arbitrary JS data to our template instance.
Template events are delegated, hence we do not need to attach them to each <button> before hand.
We can easily access the template instance in event handlers (as the 2nd argument of the event listener).
2.1. Attaching arbitrary JS data to our template instance
As stated in the Template Instances API:
[…] you can assign additional properties of your choice to the object. Use the onCreated and onDestroyed methods to add callbacks performing initialization or clean-up on the object.
Therefore you can store your fsqresults on your Template instance, so that you can access it later on (typically in your event listener):
Template.myTemplate.onRendered(function () {
this.autorun(() => { // Using an arrow function to keep the same `this`, but you could do `const self = this` beforehand.
const fsqresults = this.fsqresults = FsqResults.find().fetch();
});
});
But since we want to access specific Features later on, it might be more interesting to convert fsqresults to a dictionary. Since your ID seems to be feature.properties.name, you could do:
Template.myTemplate.onCreated(function () {
this.autorun(() => {
const fsqresults = this.fsqresults = FsqResults.find().fetch();
const markersDict = this.markersDict = {};
L.geoJSON(fsqresults, {
pointToLayer(feature, latlng) {
const props = feature.properties;
const markerName = props.name;
// Save a direct reference to the Feature data,
// using the `markerName` as key (ID).
markersDict[markerName] = feature;
// Store the `markerName` in the button `dataset`
// (i.e. as a `data-` attribute),
// as already suggested in the question comments,
// so that we can easily retrieve the ID / key
// of the Marker data associated with the button the user clicked on.
return L.marker(latlng).bindPopup(`
<p>${markerName}</p>
<button role="popupClick" data-marker-name="${markerName}">
Popup action
</button>
`);
},
});
});
});
2.2. Template event handler delegation
As stated on the Blaze Overview Details:
DOM engine […] which features […] event delegation
(sorry there does not seem to be any other mention of this feature in the official doc… let me know if you find a better one!)
Therefore, as long as we create a Template event handler with the appropriate selector, we do not need to attach the event listener on each button, which anyway we may not create as a Node but leave it as String passed to Leaflet .bindPopup (as done in the above code sample).
For example:
Template.myTemplate.events({
// Even if the `<button role="popupClick">` are not DOM nodes yet
// (because Leaflet will create them from the HTML String
// only when the user opens the popup by clicking on the Marker),
// the "click" event will bubble up to the template instance,
// which will call this event handler if it matches the selector.
'click button[role="popupClick"]'() {
console.log('clicked on a button that has been built in a Leaflet Popup');
}
});
2.3. Access the template instance, and our Feature data
The Blaze event handler are called with an extra 2nd argument, which is the current template instance:
The handler function receives two arguments: event, an object with information about the event, and template, a template instance for the template where the handler is defined.
Therefore in our case we can easily retrieve the markersDict variable that we have defined in onCreated, and use it to retrieve the exact Marker's Feature data associated with the button the user clicked on:
Template.myTemplate.events({
'click button[role="popupClick"]'(event, templateInstance) {
const button = event.currentTarget;
const markerName = button.dataset.markerName;
const markerFeature = templateInstance.markersDict[markerName];
// Do something with `markerFeature`…
console.log(markerFeature);
}
});
If you only need the property name, then you could even skip step 2.1 and directly use the markerName string retrieved from the <button> dataset.
I've come up with a solution to the first part of my issue - at first a javascript closure/scope issue to the inner and outer function scopes. I spent about 2 days wrapping my head around this SO answer: the concept of using the first for loop to produce individual instances of the function (if this were a play, the first for loop would "set the stage" for the show), and using the second for loop to execute each instance of the function ("lights, camera, action!").
I also decided that I could maintain scope if I declared my variables inside the first for loop - but I still had this issue of it only pulling the last value. Then I tried simply redeclaring my variables as constants. To my surprise, using const allowed me to write each instance to each map marker, and I could reliably access the correct iteration of the data upon each correspondent map marker! So no need for a second for loop.
this.autorun(function(){
fsqresults_fetch = FsqResults.find().fetch({});
// console.log(fsqresults_fetch);
for (i = 0; i < fsqresults_fetch.length; i++) {
container = $('<div />');
const fsq_marker = L.marker([fsqresults_fetch[i].geometry.coordinates[1], fsqresults_fetch[i].geometry.coordinates[0]], {icon: blueIcon}).addTo(mymap);
const fsq_venueAddress = fsqresults_fetch[i].properties.address;
const fsq_venueName = fsqresults_fetch[i].properties.name;
const fsq_geometry = {type: "Point",
coordinates: [fsqresults_fetch[i].geometry.coordinates[0], fsqresults_fetch[i].geometry.coordinates[1]]};
container.html("<b>" + "Name: " + "</b>" + fsqresults_fetch[i].properties.name + "<br>" +
"<b>" + "Address: " + "</b>" + fsqresults_fetch[i].properties.address + "<br>");
container.append($('<button class="btn btn-sm btn-outline-primary" id="js-checkin">').text("Check In"));
fsq_marker.bindPopup(container[0]);
container.on('click', '#js-checkin', function() {
console.log("You clicked the button!");
if (!Meteor.user()) {
alert("You need to login first!");
}
if (Meteor.user()) {
console.log("Meteor User Verified");
Checkins.insert({type: "Feature", geometry: fsq_geometry, properties: {name: fsq_venueName, address: fsq_venueAddress, user: Meteor.user()}});
}
}); //end container.on
} //end for loop
}); //end this.autorun
As I said in the comment on the last response, it's a bit hack-y, but functional enough to do the job successfully.
Now what I'm really curious to try is the solution that #ghybs posted so I have my events grouped and firing as Blaze is supposed to work!

Click on Leaflet map closes modal, click on marker opens modal

My Leaflet map has markers that open modals.
When a user clicks on the map, however, I would like for the modal to close. But the bit of code that makes that happen (below) interacts with the marker, and forces it to close as soon as it opens:
map.on('click', function(e) {
$('.modal').modal('hide'); });
I did get this to work—see the JSFiddle here: https://jsfiddle.net/askebos/Lh1y12uq/
But as you can see, the only reason it seems to be working is because it creates the following error:
Uncaught TypeError: e.preventDefault is not a function.
I imagine it's because the map.on('click'...) function is prevented from executing.
Any thoughts on how I can get to the same behaviour without the error?
The solution is to add an init() function that keeps track when a marker is clicked. Inspiration came from this question.
First, add the init() function to your code:
function init() {
init.called = true;
}
Then call the function when the marker is clicked:
function markerOnClick(e) {
init();
...
}
Make a function that fires when the map is clicked, but include an if/else statement that checks whether init.called has been set to true. If this is the case, reset init.called. If it has not been set to true, then the map was clicked elsewhere and any modals may close.
function mapClick () {
if(init.called) {
init.called = false;
}
else{
$('.modal').modal('hide');
}
}
Finally, bind the mapClick function to map clicks.
map.on('click', mapClick);
The function will no longer override marker clicks, and the error has been resolved as well. That still doesn't tell me why e.preventDefault caused an error, so any explanations would be welcome!
A working JSFiddle can be found here: https://jsfiddle.net/askebos/oesh59jr/

Is there a way in leaflet to get the coordinates of a map click when clicking on a polygon?

It is easy enough to get the lat lng of a map click using something like:
map.on('click', function (e) {
coords= e.latlng.lat + ", " + e.latlng.lng;
});
But if there are shapes on the map the function doesn't get called if you click a place covered by a shape.
Ultimately I want to produce a popup window triggered when a shape is clicked and populated with information based on lat/long.
Welcome to SO!
You could bind your event listener on your shapes as well (possibly through an L.FeatureGroup to avoid having to bind to each individual shape), and you can even use that event listener to fire the "click" event on the map as well.
var shapes = L.featureGroup().addTo(map);
shapes.addLayer(/* some vector shape */); // As many times as individual shapes
shapes.on("click", function (event) {
shapecoords.innerHTML = event.latlng.toString();
map.fire("click", event); // Trigger a map click as well.
});
Demo: http://jsfiddle.net/ve2huzxw/40/

Drag and drop events in embedded SVG?

Is there any possibility of receiving drag and drop events from SVG elements within a web page?
I tried the Google Closure library, to no avail.
Specifically, suppose my page contains
<ul id = "list">
<li class="item" id="item1">foo</li>
<li class="item">bar</li>
<li class="item">baz</li>
</ul>
And my script contains (Clojurescript/C2)
(let [items (select-all ".item")
lst (select "#list")
target (fx/DragDrop. lst nil)]
(dorun (map
(fn [item]
(let [source (fx/DragDrop. item nil)]
(. source (addTarget target))
(. source (init))))
items))
(. target (init)))
Then I do get a drag image (ghost), although I do not manage to receive drag events e.g. by doing
(on-raw "#item1" :dragstart (fn [e] (.log js/console (str "dragstart " e))))
Using similar code for SVG elements, I do not even get a ghost...
Any hints?
Thanks
Drag events are not supported on SVG Elements: http://www.w3.org/TR/SVG/svgdom.html#RelationshipWithDOM2Events.
You can fake the drag events with mouse events, see http://svg-whiz.com/svg/DragAndDrop.svg (view the source).
You can always implement it. Basically, you have to check if the element is touching another one when you are dragging:
this.isTouching = function(element){
if(this.x <= element.x && element.x <= (this.x + this.width) &&
this.y <= element.y && element.y <= (this.y + this.height)){
return true;
}else{
return false;
}
};
And it works in all browsers. Hope it helps :)

help on jquery sortable connected lists

Here is the exact copy of my code: jsFiddleCode
As you can see I have a two sortable connected lists and when some item is dropped to them they execute functions subFunction and unsubFunction respectively. Now, I also have the code for doubleclicking one of the items so that then they are put in the opposite list (the function switchLists() takes care of that.
Now, what I would like to accomplish here is the same behavior as when the items are dragged and dropped (the alert box appearing and saying exactly (for example): "Item 6 just subed".
My lack of understanding is how can it be that I have the ui available when the function subFunction is called, and not when I call the switchLists. ( I did try to add ui to the call of switchLists like this:
switchLists(e, ui){
//same code as before...
//this code doesn't execute
var itemText= ui.item.text();
alert(itemText + " just subed");
}
But I get an error in FireBug in Firefox saying that the ui is undefined.
You are free to edit the code on fiddle and post it here as a link.
As a more general questions: how does jquery pass variables to other functions? I mean, the code:
receive: subFunction
is called without any arguments, so how does the subFunction get event and ui? If you have some good tutorial on all this, it's appreciated.
Thank you for your help.
After a long day playing with this I finally came to the answer and did it like this: jsFiddle link
In short, I separated the previous function to two functions and I also read a little more about jQuery and found out that in the function I can do $(this) and so access the text of the element.
OK, just for reference the whole code is here:
$(function() {
$( "#sortable1" ).sortable({
connectWith: ".connectedSortable",
receive: subFunction
});
$( "#sortable2" ).sortable({
connectWith: ".connectedSortable",
receive: unsubFunction
});
$(".ui-state-default").dblclick(function() {
$(this).removeClass().addClass("ui-state-highlight");
var litem = $(this).clone();
litem.appendTo($('#sortable2'));
$(this).remove();
$.jGrowl($(this).text() + " successfully unsubed!", {header:"Subscription Status", life: 1000});
});
$(".ui-state-highlight").dblclick(function() {
$(this).removeClass().addClass("ui-state-default");
var litem = $(this).clone();
litem.appendTo($('#sortable1'));
$(this).remove();
$.jGrowl($(this).text() + " successfully subed!", {header:"Subscription Status", life: 1000});
});
function subFunction(event, ui) {
ui.item.toggleClass("ui-state-default");
ui.item.toggleClass("ui-state-highlight");
$.jGrowl(ui.item.text() + " successfully subed!", {header:"Subscription Status", life: 1000});
}
function unsubFunction(event, ui) {
ui.item.toggleClass("ui-state-default");
ui.item.toggleClass("ui-state-highlight");
$.jGrowl(ui.item.text() + " successfully unsubed!", {header:"Subscription Status", life: 1000});
}
});