Google map contained in meteor Template is rendered twice - google-maps-api-3

I have the following template:
<template name="test">
{{#isolate}}
<div id="map_canvas" style="width:50%; height:50%"></div>
{{/isolate}}
</template>
In my test.js (from https://developers.google.com/maps/documentation/javascript/tutorial#HelloWorld) :
function initialize(){
var mapOptions = {
center: new google.maps.LatLng(-34.397, 150.644),
zoom: 8,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
var map = new google.maps.Map($("#map_canvas")[0], mapOptions);
}
Template.test.rendered = function(){
initialize();
//*** The following is the workaround I am using:***
// if (! this.initialized){
// initialize();
// this.initialized = true;
// }
};
The problem is: without the workaround shown in the commented code section, the template is always rendered twice (as initialize() is run). It's both shown in console logs (logging code not shown here) and also visible from the map flashing once (which is not acceptable).
The reason, I am guessing, is from the following events happending:
the template is rendered, with $('#map_canvas') generated as a simple div element (no map attached);
google map api returns aync-ly and modifies $('#map_canvas');
somehow Meteor detects the change, and in spite of {{#isolate}}, decides to render the whole template once again (this is shown in log);
initialize() is called again, within Template.test.rendered.
My questions:
why?
if this is what happened, why is Meteor only rendering twice, instead of infinite times?
solution?
3 questions and lots of thanks!

The template is probably being rendered twice due to the outer context that contains {{> test}} being re-rendered. There's a variety of reasons why this might happen, but usually it's just that it renders initially without subscription data, and then a second time when the data has loaded.
Anyway, in your specific case, I think what you want is a {{#constant}} wrapper around your google map, rather than an {{#isolate}}.
NOTE: {{#constant}} areas do get re-rendered if (for whatever reason) the surrounding context is re-rendered. However the new version gets thrown away rather than replaced in the DOM. So just be careful in you rendered callback to only do something the first time.

Related

Programmatically (not declaratively) render Meteor template

I'm trying to customize events added to FullCalendar, using eventRender. I'm aware that I can directly return HTML from my eventRender method, but I would prefer to programmatically merge event data with a predefined Meteor template (with associated events).
Previously I could have used Meteor.render() but that functionality is no longer available. I'm familiar with Template.dynamic, but that appear to only be available declaratively, and most of the questions I've seen here are quite old, so refer to deprecated functionality.
Here's what I would like to do:
Calendar - event population & rendering:
Template.dashboard.rendered = function(){
$('#calendar').fullCalendar({
events: function(start, end, timezone, callback) {
callback(Events.find().fetch());
},
eventRender: function(event, element) {
// PROGRAMMATICALLY RENDER TEMPLATE
// The following does not work - no data is attached
return Template.calendarEvent.renderFunction(event);
}
});
};
Event template HTML
<template name="calendarEvent">
{{title}}
<!-- full layout for rendering event here -->
</template>
Event template JS
Template.calendarEvent.events({
// define template event handlers
});
That function was not removed, it was renamed and quite a while ago it also changed behaviour (when spark was replaced by blaze).
What you are looking for is Blaze.renderWithData.
Note that it returns a Blaze.View and not a DOM object.
To make it a dom object you could provide it as a parent:
var renderedCalendarEvent = document.createElement("div");
Blaze.renderWithData(Template.calenderEvent, event, renderedCalendarEvent);
The DOM element renderedCalendarEvent will react to any reactive sources the template uses.
If you need HTML you can use Blaze.toHTMLWithData, but that html will remain static.
Blaze.toHTMLWithData(Template.calenderEvent, event);

how to add listener for markerclusterer finished loading?

I already added a map load listener after which i start adding markers to the cluster.
I wanna display a loading screen till the markerclusterer finishes the marker loading.
so how do i do that ?
this is my hide loading screen code which hides on map tiles load.
google.maps.event.addListener(map, 'tilesloaded', function() {
$("#loading").hide();
});
I know it's an old one but for anyone in the same situation I sort of found a solution.
Right after you are creating your cluster
markerCluster = new MarkerClusterer(map,
markers,
{
imagePath:
"/img/m/"
});
Add a listener for 'zoom_changed' like that:
google.maps.event.addListenerOnce(map,
'zoom_changed',
function(event) {
console.log("zoom occured");
});
(The listener once being key here, you don't want it to run every time the user zooms, you don't even need to set a different zoom value, nothing will happen visually)
and then:
map.setZoom(10);
I am using the same value (10) as in my InitMap function, so the user doesn't see any zoom in or outs.

google maps infoWindow click event re-renders map-canvas in Meteor

Hey Im trying to use google maps within my MeteorJS project to have google maps display on a map all customers, and then to display an infoWindow when you click on one of the markers.
problem is anytime you click on the marker it re-renders the map from scratch, i know this has to do with the the reactivity of the Session variable being set when the infoWindow is being clicked.
is there any way avoid the map being re-rendered when the session variable is changing?
thanks.
below is the JS and template im using in my project.
<template name="customers_map">
{{#constant}}
<div id="mapWrapper">
<div id="map-canvas"></div>
</div>
{{/constant}}
</template>
the code for making the google maps and markers.
Template.customers_map.rendered = function() {
$("#map-canvas").height("400px");
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function(p) {
Session.set("myLat", p.coords.latitude);
Session.set("myLng", p.coords.longitude);
});
}
Deps.autorun(function(){
var mapOptions = {
center: new google.maps.LatLng(Session.get("myLat"), Session.get("myLng")),
zoom: 15,
zoomControl: true,
zoomControlOptions: {style: google.maps.ZoomControlStyle.SMALL},
streetViewControl: false,
mapTypeControl: false,
scaleControl: true,
mapTypeId: google.maps.MapTypeId.SMALL
}
var map = new google.maps.Map(document.getElementById("map-canvas"), mapOptions);
var infowindow = new google.maps.InfoWindow({
content: Template.customers_infoWindow()
});
Customers.find().forEach(function(customer) {
if (customer.loc != null) {
var geo = customer.geoLocation();
var marker = new google.maps.Marker({
position: new google.maps.LatLng(geo.lat, geo.lng),
title: customer.name(),
icon:'http://maps.google.com/mapfiles/ms/icons/green-dot.png'
});
marker.setMap(map);
google.maps.event.addListener(marker, 'click', function() {
Session.set("customerId", customer._id);
infowindow.open(map,marker);
});
} else {
console.log(customer.name() + " has no geoLocation");
};
});
});
};
the infoWindow template
<template name="customers_infoWindow">
<h1>{{record.name}}</h1>
</template>
and the js for the infoWindow template
Template.customers_infoWindow.record = function() {
return Customers.findOne({_id: Session.get("customerId")});
}
If you create a global googlemaps object, you can access its properties from anywhere. This article has a nice example of doing this.
The overall gist is:
Create a googlemaps class with an initialize method. At the end of the initialize method, set a session variable for your map's existence. ( Session.set('map', true);)
Call create a googlemap object by calling the googlemap init method from within Template.customers_map.rendered.
It's a bit difficult to be sure without having a running version in front of me, but I think this is essentially because you have all your code in one big Deps.autorun block. Clicking one of the markers is changing the Session variable customerId, which will cause customers_infoWindow to re-render (as it's clearly a dependency), but I'm sure this is the intended behaviour.
However, since you're declaring var infoWindow in your Deps.autorun block to have an instance of that template as one of its properties, I think that changing customers_infoWindow will actually invalidate the entire Deps.autorun calculation, which means the whole block will be executed again, including the var map = new google.maps.Map(...) line, which will essentially re-render the map (even though it doesn't re-render that actual div element that contains it).
So, I would suggest splitting your code into separate Deps.autorun blocks, and making sure that anything in the same block should be re-run at the same time - clearly, this means that the Google Maps initialisation code and the infoWindow handler should be in separate blocks.
To reiterate, I think that's what's going on, but you'll have to try it and let me know...

Re-rendering of a template doesn't allow me to permanently change an element's class?

I have a sortable list.
<template name="the_playlist">
{{#each main_list}}
<li id="{{index}}" class="list_element">
<div class="next_song">...</div>
<div class="destroy">...</div>
<div class="element_style">{{song_title}}</div>
</li>
{{/each}}
</template>
And this is the main_list that it prints from.
Template.the_playlist.main_list = function(){
//if ret is valid, it will have a songs member
var ret = Links.find().fetch()[0];
if (typeof ret == 'undefined'){
ret = []
}
else {
ret = Links.find().fetch()[0].songs;
}
return ret;
}
And I am using the sortable plugin and more importantly its update callback which updates everytime the user changes a position the list or an element is added to the list.
$(function() {
$( "#playlist" ).sortable({
update: function(){
Template.list.updateList(); //MODIFIES DB CONTENTS, AND MAIN_LIST's VALUES CHANGE
}});
$( "#playlist" ).disableSelection();
});
*The problem: * If a page already has list elements when it's loaded, for one time only, I would like to add a class that hides (.addClass("hide")) each of the next_song elements that are on the page at that time. This *will work only until main_list changes* by a call to Template.list.updateList above, after which automagically, the added class will disappear - most likely due to the re-rendering that is occuring since the main_list depends on the db changes.
The following in the JQuery snippet I use to try and accomplish this.
$("#playlist li .next_song").each(function(){
$(this).addClass("hide_song");
})
Here is a demo. Try plugging in the above JQUery code into the console. and then move the list elements around to see the problem.
Can you not just determine whether that will be the case in a helper function?
Template.the_playlist.helpers({
'list_elements_exist': function() {
return (!!$('#playlist li').length);
}
}
Then you can just insert the logic straight into the template:
<div class="next_song{{#if list_elements_exist}} hide_song{{/if}}">...</div>
To be honest, I'm not 100% sure that this will float with reactivity depending on the structure of your app. If it doesn't work properly, I'd introduce a new session boolean, list_elements, the value of which is returned by the helper function above. It should be fairly easy to update its value in event handlers or created callbacks to keep it tracking whether there are any items in the list or not, and this will guarantee the list renders as required regardless of other dependencies changing.

Subscribing to changes in a Collection but not in a template

I'm very new to meteor, so apologies if I'm missing something very basic here.
I thought it would be fun to create a very simple textpad style app to check out meteor. I took the todo app and changed the data structures to be 'folders' and 'docs' rather than 'lists' and 'todos', so I have a list of folders and when you click on the folder you get a list of the documents in that folder.
I've then added some code to show the 'content' attribute of a single 'doc' when one of the docs in the list is clicked.
I'm using ace to add some pretty print to the content of the doc (https://github.com/ajaxorg/ace). I've set ace up to work with a hidden textarea containing the plaintext version of my document, and the editor object takes this text and pretty prints it.
The problem with ace is that I don't want the template containing the ace editor to be replaced every time the contents of the doc changes (as it takes half a second to reinitialise, which is a crappy experience after every character is typed!). Instead, I want to update the textarea template and then use the ace API to tell the editor to update it's input based on what is in the textarea.
Now, this is probably the wrong way to approach the problem, but I've ended up using two templates. The first contains a textarea containing doc.contents, which is reactive to the underlying model:
<template name="doc_content">
<textarea name="editor">{{content}}</textarea>
</template>
The second one contains the 'editor' div which ace uses to display the pretty printed text.
<template name="doc_init">
<div id="editor"></div>
</template>
The idea is that the first template will update every time the user types (on all clients), and the second template is only ever re-loaded for each new doc we load.
Template.doc_content.content = function() {
var doc_id = Session.get('viewing_itemname');
if (!doc_id) {
return {};
}
var doc = Docs.findOne({_id:doc_id});
if (doc && doc.content) {
// #1 Later
var editor = Session.get('editor');
if (editor) {
editor.getSession().setValue(doc.content);
}
return doc.content;
} else {
return '';
}
};
When you enter text into the editor div I make a call to Docs.update(doc_id, {$set: {content: text}});, which updates the value in the textarea on each client. All good so far.
editor.getSession().on('change', function(){
var text = editor.getSession().getValue();
Docs.update(doc_id, {$set: {content: text}});
});
What I want to do, for all clients other than the client which made the change, is to subscribe to the change for that doc and call editor.getSession().setContent() with the text which has just been changed, taking the text from the textarea and using it to fill the editor.
I've tried to do this by making that call from the template containing the textarea (as this changes whenever the doc is updated - see #1 above). However, this puts the clients into an infinite loop because changing the value in the editor causes another call to Docs.update.
Obviously this doesn't happen when you render a template, so I'm assuming there's some magic in meteor which can prevent this happening, but I'm not sure how.
Any thoughts?
TIA!
There's a lot to absorb in your question, but if I understand correctly, you might simply be after Deps.autorun:
Deps.autorun(function () {
var doc_id = Session.get('viewing_itemname');
if (!doc_id) {
return {};
}
var doc = Docs.findOne({_id:doc_id});
// do stuff with doc
});
Deps.autorun is really useful in that it will get re-run if any of its
dependencies change. These dependencies are limited to those that are "reactive"
such as Collections and Sessions, or anything that implements the reactive API.
In your case, both Session.get and findOne are reactive so if their values
change at all, Deps.autorun will run the function again.

Resources