How to execute helper function after DOM is ready in meteor - javascript

I have a list of <li>'s which gets populated with a find() using Meteor.startup as you see below. Then I'm getting all the data attributes of these <li>'s using data() and putting it in an object and trying to return/console.log it so I can see if it works. But I'm getting null as a result.
Meteor.startup(function () {
Template.messages.lists = function () {
var allitems = lists.find();
return allitems;
};
var map;
map = new GMaps({
div: '#map_canvas',
lat: -12.043333,
lng: -77.028333
});
var lat = map.getCenter().lat();
var lng = map.getCenter().lng();
map.addMarker({
lat: lat,
lng: lng,
draggable: true,
title: 'Test',
dragend: function (e) {
$('#lat').val(this.getPosition().lat());
$('#lng').val(this.getPosition().lng());
}
});
console.log(getMarkers());
});
function getMarkers() {
var coordinates = {};
coordinates = $('li.message').data();
return coordinates;
}
I tried the same in my console directly and it works - I get an object back - so I'm guessing that the DOM is not ready/populated before this function is executed.
I am having a hard time understanding the difference between things like Meteor.startup and Template.mytemplate.rendered. In this case it seems that none of them works as I want?
What's the right way/place to do stuff with the DOM (traversing,getting attributes,manipulating)?
edit
as the code changed a lot in order to do what I wanted I post the whole thing.
Meteor.startup(function () {
var map;
map = new GMaps({
div: '#map_canvas',
lat: 50.853642,
lng: 4.357452
});
Meteor.subscribe('AllMessages', function() {
var allitems = lists.find().fetch();
console.log(allitems);
allitems.forEach(function(item) {
var lat = item.location.lat;
var lng = item.location.lng;
console.log('latitude is: ' + lat);
console.log('longitude is: ' + lng);
map.addMarker({
lat: lat,
lng: lng,
draggable: true,
title: 'Test',
dragend: function(e) {
$('#lat').val(this.getPosition().lat());
$('#lng').val(this.getPosition().lng());
}
});
});
});
});
The above code creates a new google map (using the GMaps.js plugin) inside Meteor.Startup, and then in a nested Subscribe fetchs all documents from a collection, forEaches the results and gets the latitude and longitude values, then goes on to add markers in the google map...
edit 2
I made my 'map' variable a global one this way no need to nest .subscribe and .startup. :
Meteor.subscribe('AllMessages', function() {
var allitems = lists.find().fetch();
console.log(allitems);
allitems.forEach(function(item) {
var lat = item.location.lat;
var lng = item.location.lng;
console.log('latitude is: ' + lat);
console.log('longitude is: ' + lng);
map.addMarker({
lat: lat,
lng: lng,
draggable: true,
title: item.description,
dragend: function(e) {
$('#lat').val(this.getPosition().lat());
$('#lng').val(this.getPosition().lng());
}
});
});
});
Meteor.startup(function () {
map = new GMaps({
div: '#map_canvas',
lat: 50.853642,
lng: 4.357452
});
});
Template.messages.lists = function () {
var allitems = lists.find().fetch();
return allitems;
}

Meteor.startup
Meteor.startup() runs only once, its run on the client and server. So when the browser loads and the initial DOM is ready or the server starts. As Sohel Khalifa said you place initialization functions here. Don't define templates in here because the the templates need to be ready before this function can be fired.
Template.myTemplate.onRendered(function() {... })
This is run when meteor has finished and rendered the DOM. Additionally is run each time the HTML changes within the template. So for each item in your list in a subtemplate/a change in an item/update,etc as well as the list you will see the console.log return something if you use it to check. It will return null/undefined when calling for data sometimes (which i'll explain):
Does this mean all the DOM is ready? NO!:
I think this is what might be causing you a bit of trouble. If you use external APIs such as Google maps, they might still render the map. the Template.myTemplate.rendered() means Meteor has finished rendering the template with the reactive variables necessary. So to find out when your Google maps might be ready you need to hook into the Google maps API. Have a look at this question
Using Meteor.subscribe
The reason you might get null/undefined while using rendered is because this is the process meteor usually renders data into templates
You are basically calling console.log(getMarkers()); before the subscription is complete, which is why you get null/undefined
Meteor uses this summarized process with templates & reactive data:
Build Templates with no data & render - NO data yet at this stage
Ask server for data in collections
Rebuild templates with new data & render
So if at process 1) for a very short time you will have no data yet, which is why you might get null (such as in your code) & at the first render. To get past this you should use Meteor.subscribe's callback which is run when all the data is downloaded from the server: e.g
Meteor.subscribe("lists", function() {
//Callback fired when data received
});
Note: Before you use this you should read the docs on using subscriptions as you need to remove the autopublish package, as well as make a corresponding Meteor.publish function on the server. While this may seem tedious you may end up doing it anyway to give your users their own lists &/or implement some kind of security.
Suggested edits to your code:
You are doing DOM traversing in the right place, Template.mytemplate.onRendered(function().. but you also need to hook into Google Maps' API to capture when their map is finished drawing. You should also use Meteor.subscribe to make sure you get the timing right and not get null/undefined.
Make sure you put your Template helpers in a Meteor.isClient but not in a Meteor.startup because Meteor.startup is fired after your initial DOM is ready (the intitial is the first but before its changed by reactive variables or a router) so your template definitions need to run before this stage.

The reason why it returns null as a result is, you have placed it in Meteor.startup(). It actually runs before the data is loaded from the server. So the lists.find() returns null.
The Meteor.startup() is a place for initializing your global variables, reative sessions and subscribing to the primary bunch of data from the server. Everything you write there will be executed once, right after the client starts up.
The Template.myTemplate.rendered() is a special helper provided by meteor that runs everytime when the corresponding data has changed, it is mainly used for getting attributes or manipulating DOM elements contained within that template.
So, place your helper code outside in common isClient() area. And use .rendered()helper to traverse the DOM, and getting or manipulating attributes of DOM elements.

Many thanks Akshat for detailed answer)
I have more complicated case of using Meteor.subscribe, i have template which includes images from DB. So i need to wait for data from two collection iamges and news(all other data here).
I get my DOM ready in this way:
imageIsLoaded = new Promise(function(resolve){
Meteor.subscribe('images',function(){
resolve()
});
});
newsIsLoaded = new Promise(function(resolve){
Meteor.subscribe('news',function(){
resolve()
});
});
Template.newsList.onRendered(function(){
Promise.all([imageIsLoaded, newsIsLoaded]).then(function() {
// DOM IS READY!!!
newsServices.masonryInit();
})
});
Template structure:
<template name="newsList">
{{#each news}}
{{> news_item}}
{{/each}}
</template>

The best way to do this is to put code into Template.x.rendered() and use a Session reactive variable to track if the code has been run or not. For example, you could go about this like so:
Template.x.rendered = function () {
if (Session.get('doneMarkers') == null) {
// Do your stuff
if (getMarkers() != null) {
Session.set('doneMarkers', 'yes');
}
}
});
function getMarkers() {
var coordinates = {};
coordinates = $('li.message').data();
return coordinates;
}
If you ever want to rerun that part of the code, you only have to call:
Session.set('doneMarkers', null);

Related

Leaflet: how to swap coordinates received from an ajax call

I am using Leaflet 1.0.3 and a few plugins including Leaflet.ajax. My L.geo.ajax call is working and returning geojson objects, however, the coordinates are reversed. I created a function to fix this:
var convertLatLng = function (latlng) {
var temp = latlng[y];
latlng[y] = latlng[x];
latlng[x] = temp;
convertedLatLng = latlng;
return convertedLatLng;
console.log('this function is running')
}
But my problem is I don't know where to put it. Do I run it inside my geoJson call? If so, where? Here is a snippet of the ajax call:
var geojson = L.geoJson.ajax('http://www.iotwf.com/deployment_map/json', {
pointToLayer: function (feature, latlng) {
convertLatLng(latlng);
...
},
onEachFeature: function(feature, layer) {
...
}
});
I am also open to other suggestions for what may fix it.
Welcome to SO!
First make sure that your coordinates are indeed reversed.
Note that the GeoJSON format expects [longitude, latitude], whereas Leaflet usually expects [latitude, longitude], EXCEPT in the case of L.geoJSON() factory (and the plugin L.geoJson.ajax()), where it automatically reads the GeoJSON order and builds the layers at the correct coordinates.
If your coordinates are still reversed, the appropriate correction would be obviously to correct the order in your data source directly (or whatever service outputs your data), so that you get actually compliant GeoJSON data. That would solve many future headaches.
If that is not possible, then indeed you could try a workaround within your script.
The most appropriate way to do so would probably be to use the coordsToLatLng option of the L.geoJSON factory.
Changing its default implementation, you would get something like:
L.geoJson.ajax(url, {
coordsToLatLng: function (coords) {
// latitude , longitude, altitude
//return new L.LatLng(coords[1], coords[0], coords[2]); //Normal behavior
return new L.LatLng(coords[0], coords[1], coords[2]);
}
});

How do I use play-framework tags in javascript

I'm using the Play Framework to build a Website, that shows some Markers on a Google Map. For that I create and save some Models in the Application.class controller. But when I render these Models i couldn't find out how to get these use these Model-Objects in javascript. Does anyone know, how to use the Play Framework Tags on Javascript?
My problem was, I wanted to use the "Play-Tags" in my JavaScript-File. For example like this:
#{list items: all, as:'object'}
addMarkerFromRendered("${object.name}", "${object.lat}", "${object.lng}", "${object.type}", "${object.description}", "/public/img/${object.type}.png");
#{/list}
But the Play Tags are not defined for Javascript so the compiler stops at "${" and throws an error.
To solve the problem I wrote a function in my Javascript file:
var addMarkerFromRendered = function(name, lat, lng, category, description, icon){
var latLng = new google.maps.LatLng(lat,lng);
addMarker(latLng, name, category,description, icon);
};
And then I used the function in my HTML-File to render my data:
<script>
$('#map').ready(function(){
#{list items: all, as:'object'}
addMarkerFromRendered("${object.name}", "${object.lat}", "${object.lng}", "${object.type}", "${object.description}", "/public/img/${object.type}.png");
#{/list}
});
</script>

Leaflet - Event on tiles loading

I am currently developing a map-based application and need a way to get notified when Leaflet is pulling tiles from the TileProvider (which, in my case, is MapBox). I read the Leaflet documentation, especially the part with the TileLayer. Currently, I am using the following code to attach a tileload handler:
map.eachLayer(function (layer) {
layer.on('tileload', function(e) {
console.log(e);
});
});
Is there a better way to get the TileLayer of the current map? One problem with this approach is that I hook the handler to all layers (although only TileLayers will raise events, it is unclean to hook it too all layers). Or can I attach the handler directly to the map instance somehow?
Update
I initialize the map with the following MapBox code snippet:
map = L.mapbox.map( element, '...', mapOptions );
This automatically creates a TileLayer (and several other layers), attaches them to the map object and returns this object for later use.
Why not use tileload event directly on the tile layer, like this:
//create a variable to store the tilelayer
var osm = L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
//add the tileload event directly to that variable
osm.on('tileload', function (e) {
console.log(e);
});
If you've got a lot of L.mapbox.TileLayer instances and you don't want to add the eventhandler manually to each instance like Alexandru Pufan suggests in his answer you could still use a loop and Object's instanceof method:
map.eachLayer(function (layer) {
if (layer instanceof L.mapbox.TileLayer) {
layer.on('tileload', function(e) {
console.log(e);
});
}
});
After reading your comment on Alexandru's answer i'm guessing you only have one layer, then it would be best to add it manually to the instance, which is possible with L.mapbox.TileLayer like this:
var layer = L.mapbox.tileLayer(YOUR MAP ID);
layer.on('tileload', function(e) {
console.log(e);
});
var map = L.mapbox.map('mapbox', null, {
'center': [0, 0],
'zoom': 0,
'layers': [layer]
});

Google Maps inside Ember.js

I've successfully managed to get Google Maps rendering on my main page. I'm using the same technique for one of my inner pages.
var MapView = Ember.View.extend({
layoutName: 'pagelayout',
didInsertElement : function(){
var mapOptions = {
center: new google.maps.LatLng(-34.397, 150.644),
zoom: 8,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
var map = new google.maps.Map(this.$("#map").get(0),mapOptions);
this.set('map',map);
},
redrawMap : function(){
var newLoc = new google.maps.LatLng(this.get('latitude'), this.get('longitude'));
this.get('map').setCenter(newLoc)
}.observes('latitude','longitude')
});
However, it doesn't render. If I change it like so, it works but takes over the base html element.
this.$().get(0) /**from this.$('#map').get(0)**/
I'm quite stumped. Thinking that the problem might be that didInsertElement was being trigger way too early (I've understood it that the entire DOM for this View will be available when this event fires), I've tried setting up a afterRender listener and triggered the map load but that fails too. I've triple checked that the div exists.
Can someone please help me with this?
I ran the Google Maps init code from my console and found that the map did load so it seems to be an issue with how didInsertElement works with my setup. My understanding what that didInsertElement was called when the View was in the DOM but perhaps because I'm using layouts , it works differently.
I solved it by calling a separate view from within the div itself
{{#view "map"}}
and by creating a new View for the map.
var MapView = Ember.View.extend({
/**
Same as before
**/
});
This is just my solution that may be useful to others facing the same problem. If someone has a better solution, please add it so that I can verify and upvote it.
The this.$() might not be available as the view might not have rendered yet. Try wrapping it in a Ember.run.next call to delay the execution in the next run loop cycle:
didInsertElement: function() {
Ember.run.next(this, function() {
// your code
});
});

Need to redraw a map with new data (in conjunction with CartoDB.js)

I seem to be having some trouble with reinitializing a Leaflet map object. I am running an init() function that starts by initializing a map, adding a tilelayer and layer with polygons, and then adding sublayers which color the polygons according to a ST_Intersects query. The issue is that this function is tied to an AJAX call that is caused by a click event but does not update the data according to the new parameters sent to the map. I don't think I am explaining this very well, so here is a bit of my code:
success: function(data) {
init(data);
}
function init(data){
// initiate leaflet map
alert("start");
var map = L.map('cartodb-map').setView([40.750028, -73.926768], 11);
alert("map made");
//create sublayers, etc
}
What happens is that the first time init is run, both 'start' and 'map made' alerts work, and the map is made according to the data. Any further calls to init simply alerts with "start" and "map made" is never alerted so I believe the problem is with reinitializing the leaflet map. What should I do to fix this?
Not sure the problem without seeing more of your code, but you could try calling map.remove(); the second time around. So:
if (map) {
map.remove();
// add in new map initialization here
} else {
var map = ...
}

Categories