Function throws an error if not run from inside a $timeout - angularjs

I have this code in my controller
$scope.initMap = function() {
var mapOptions = {
center: new google.maps.LatLng(-34.397, 150.644),
zoom: 8
};
var map = new google.maps.Map(document.getElementById("map-canvas"), mapOptions);
}
$timeout(function(){$scope.initMap();}, 1); //This works
//$scope.initMap(); //Doing it like this causes the crash
Using the $timeout line the map is initialized fine, but using the bottom line Angular throws an error a is null, from somewhere inside the minified library.
I'm including the Google Maps js file just above my controller in the HTML
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=MyKey& sensor=false"></script>
<div class="row" ng-controller="VenuesCtrl">
I assume it's something to do with the $timeout function rather than needed to wait for 1 millisecond, but I can't figure it out. Do I need the $timeout or can I make my code work without it?

It happens because Google tries to use id DOM element before map-canvas element is rendered. I would create directive and call Google Map constructor from there
I think even with $timeout 0 it works too.
The function wrapped by $timeout executes after every other piece of code following the call to $timeout is executed.
Why it works?
With timeout you just level down execution priority. Its a trick to tell the browser to run it ASAP when other stuff done and browser is not busy.
You can google enough examples and demos how to write Google Map constructor with directive. I use my own.
Here is a relevant code:
HTML
<map-directive id="map_canvas" >
</map-directive>
directive
app.directive('mapDirective', ['$log', function($log) {
return {
restrict: 'E',
replace: true,
scope: {
/* ... */
},
template: '<div></div>',
link: function(scope, element, attrs) {
var map = false;
//...
initialize(document.getElementById(attrs.id));
function initialize(map_id) {
log.i("initialize Google maps");
var mapZoomLevel = locStorage.get('mapZoomLevel', 12);
var mapLast_lat = locStorage.get('mapLast_lat', 34.060122481016855);
var mapLast_longt = locStorage.get('mapLast_longt', -118.26350324225484);
var options = {
zoom: mapZoomLevel,
center: new google.maps.LatLng(mapLast_lat, mapLast_longt),
mapTypeId: google.maps.MapTypeId.ROADMAP,
panControl: false,
scrollwheel: true,
draggable: true,
rotateControl: false,
mapTypeControl: true,
scaleControl: true,
streetViewControl: true,
zoomControl: true,
disableDoubleClickZoom: false
};
map = new google.maps.Map(document.getElementById(attrs.id), options);
//...
}

Related

AngularJS Leaflet memory leak

There is a clear memory leak in my Angular app wherever I use the following leaflet directive: https://github.com/tombatossals/angular-leaflet-directive.
Note that the directive works fine, however the memory footprint continues growing as I leave and return to any view using the directive.
The directive builds off of the leaflet javascript library found here: https://github.com/Leaflet/Leaflet
I use the directive as follows:
<div ng-controller="Explore">
<div leaflet defaults="defaults" center="center" markers="markers" layers="layers"></div>
</div>
Inside my controller I extend the leaflet directive attributes to the scope:
angular.extend($scope, {
defaults: {
dragging: true,
doubleClickZoom: false,
scrollWheelZoom: false,
maxZoom: 12,
minZoom: 12
},
center: {
lat: $scope.cities[$scope.market.city][1],
lng: $scope.cities[$scope.market.city][0],
zoom: 12
},
markers: {},
layers: {
baselayers: {
google: {
name: 'Google Streets',
layerType: 'ROADMAP',
type: 'google'
}
}
}
});
I am not sure what is causing the memory leak, but I believe it may have to do with event listeners that are not removed when $destroy is called within the leaflet directive:
scope.$on('$destroy', function () {
leafletData.unresolveMap(attrs.id);
});
On destroy the function unresolveMap is called:
this.unresolveMap = function (scopeId) {
var id = leafletHelpers.obtainEffectiveMapId(maps, scopeId);
maps[id] = undefined;
};
This is as far as I got. If anyone has come across anything similar or has any ideas as to how to attack this issue further I would greatly appreciate it :)
You should try to remove completly the map by adding a map.remove() in the on $destroy handler (From leaflet API it should: "Destroys the map and clears all related event listeners").
scope.$on('$destroy', function () {
leafletData.unresolveMap(attrs.id);
map.remove();
});
Have you tried assigning an id attribute to your map? That's what the attrs.id refers to.
<leaflet id="myMap" defaults="defaults" center="center" markers="markers" layers="layers"></leaflet>

BxSlider and Angular js

If you are working with Angular, probably you will have case where you want to call some JQUERY library to do something for you. The usual way would be to call JQUERY function on page ready:
$(function(){
$(element).someJqueryFunction();
});
If your element content is dynamically added using Angular, than this approach is not good as I understood, and you need to create an custom directive in order to fulfill this functionality.
In my case, I want to load bxSlider on <ul class="bxslider"> element, but content of the <ul> element should be loaded using angular ng-repeat directive.
Picture names are collected using REST service from the database.
I call my directive called startslider using the following code:
<div startslider></div>
My custom Angular directive is: (pictures is variable in the $scope passed from my controller, which called REST service)
application.directive('startslider', function () {
return {
restrict: 'A',
replace: false,
template: '<ul class="bxslider">' +
'<li ng-repeat="picture in pictures">' +
'<img ng-src="{{siteURL}}/slide/{{picture.gallery_source}}" alt="" />'
'</li>' +
'</ul>',
link: function (scope, elm, attrs) {//from angular documentation, the link: function should be called in order to change/update the dom elements
elm.ready(function () {
elm.bxSlider({
mode: 'fade',
autoControls: true,
slideWidth: 360,
slideHeight:600
});
});
}
};
});
As result, I get all the pictures from the database displayed on my screen, but without bxSlider loaded (all pictures displayed one bellow other).
Please note that bxSlider is working, because when I call $(element).bxSlider(); on manually written code, the slider loads.
Why is this happening?
EDIT:
I think your problem is caused by trying to call the slider on HTMLUListElement, which is an object.
To call the slider on the DOM element, you could use $("." + $(elm[0]).attr('class')) that will use the existing bxslider class, or better assign an id to your <div startslider id="slideshow"></div> and call like $("." + $(elm[0]).attr('id'))
elm.ready(function() {
$("." + $(elm[0]).attr('class')).bxSlider({
mode: 'fade',
autoControls: true,
slideWidth: 360,
slideHeight:600
});
});
Full code:
http://jsfiddle.net/z27fJ/
Not my solution, but I'd thought I would pass it along.
I like this solution the best (Utilizes directive controllers)
// slightly modified from jsfiddle
// bxSlider directive
controller: function() {},
link: function (scope, element, attrs, controller) {
controller.initialize = function() {
element.bxSlider(BX_SLIDER_OPTIONS);
};
}
// bxSliderItem directive
require: '^bxSlider',
link: function(scope, element, attrs, controller) {
if (scope.$last) {
controller.initialize();
}
}
http://jsfiddle.net/CaioToOn/Q5AcH/8/
Alternate, similar solution, using events (I do not like)
Jquery bxslider not working + Angular js ng-repeat issue
Root issue
You cannot call scope.$apply in the middle of a digest cycle.
You cannot fire bxSlider() until the template gets compiled.
Ultimately, you need to wait for the template to be compiled and available before calling bxSlider()
Calling a function when ng-repeat has finished
this is the directive
.directive('slideit', function () {
return function (scope, elm, attrs) {
var t = scope.$sliderData;
scope.$watch(attrs.slideit, function (t) {
var html = '';
for (var i = 0; i <= t.length-1; i++) {
html += '<li><ul class="BXslider"><li class="BXsliderHead"><img src="'+scope.$imageUrl+'flight/'+t[i].code+'.gif" />'+t[i].name+'</li><li class="BXsliderChild">'+t[i].noneStop+'</li><li class="BXsliderChild">'+t[i].oneStop+'</li><li class="BXsliderChild">'+t[i].twoStop+'</li></ul></li>';
}
angular.element(document).ready(function () {
$("#" + $(elm[0]).attr('id')).html(html).bxSlider({
pager:false,
minSlides: 3,
maxSlides: 7,
slideWidth: 110,
infiniteLoop: false,
hideControlOnEnd: true,
auto: false,
});
});
});
};
});
this is the html in view
<div ui-if="$sliderData">
<ul id="experimental" slideit="$sliderData"></ul>
</div>
and the important part is the dependency of js file
<script src="/yii-application/frontend/web/js/libraries/angular/angular.min.js"></script>
<script src="/yii-application/frontend/web/js/libraries/angular/angular-ui.js"></script>
<script src="/yii-application/frontend/web/js/libraries/jquery/jquery-1.11.3.min.js"></script>
<script src="/yii-application/frontend/web/js/libraries/flex/jquery.bxslider.min.js"></script>
<script src="/yii-application/frontend/web/assets/e3dc96ac/js/bootstrap.min.js"></script>
<script src="/yii-application/frontend/web/js/libraries/jquery-ui/jquery-ui.min.js"></script>
<script src="/yii-application/frontend/web/js/searchResult.js"></script>
<script src="/yii-application/frontend/web/js/Flight.js"></script>
and don't forget to put ui in module
var app = angular.module('myApp', ['ui']);
this is the bxslider that i use !!!! maybe solve your problem

Issue with included JavaScript when running Testacular unit test on Angular.js directive

I'm trying to write a unit test for a directive that I use to insert a Leaflet.js map.
This tag
<div ng-controller="WorldMapCtrl">
<sap id="map"></sap>
</div>
Is used with this controller
function WorldMapCtrl($scope) {}
And the following directive
angular.module('MyApp').
directive('sap', function() {
return {
restrict: 'E',
replace: true,
template: '<div></div>',
link: function(scope, element, attrs) {
var map = L.map(attrs.id, {
center: [40, -86],
zoom: 2
});
//create a CloudMade tile layer and add it to the map
L.tileLayer('http://{s}.tile.cloudmade.com/57cbb6ca8cac418dbb1a402586df4528/997/256/{z}/{x}/{y}.png', {
maxZoom: 4, minZoom: 2
}).addTo(map);
}
};
});
This correctly inserts the map into the page. When I run the following unit test, it crashes out with
TypeError: Cannot read property '_leaflet' of null
describe('directives', function() {
beforeEach(module('MyApp'));
// Testing Leaflet map directive
describe('Testing Leaflet map directive', function() {
it('should create the leaflet dir for proper injection into the page', function() {
inject(function($compile, $rootScope) {
var element = $compile('<sap id="map"></sap>')($rootScope);
expect(element.className).toBe('leaflet-container leaflet-fade-anim');
})
});
});
});
From what I can tell from some Googling, the error seems to be fairly common. Perhaps not putting the tag in properly (With an id="map") or something similar can cause the problem. However, I'm not seeing what I can change with the test for this directive. Any ideas what I'm doing wrong here?
I've also included the necessary JS files in testacular.conf.js in the same order that they're included in index.html. So, it's not an issue of the files not being there (I assume at least).
One result from Googling (sadly, it doesn't help): https://groups.google.com/forum/#!msg/leaflet-js/2QH7aw8uUZQ/cWD3sxu8nXsJ

Angularjs with leaflet directives - To clear markers

I have following setup working fine, however, I got some small problem..
Tried many ways but I cannot get rid of markers before adding new markers..
With following example, marker keep being added whenever you push from the controller..what is the best way to erase existing markers before adding any new one...?
var module = angular.module('Map', []);
module.directive('sap', function() {
return {
restrict: 'E',
replace: true,
template: '<div></div>',
link: function(scope, element, attrs) {
var map = L.map(attrs.id, {
center: [-35.123, 170.123],
zoom: 14
});
//create a CloudMade tile layer and add it to the map
L.tileLayer('http://{s}.tile.cloudmade.com/57cbb6ca8cac418dbb1a402586df4528/997/256/{z}/{x}/{y}.png', {
maxZoom: 18
}).addTo(map);
//add markers dynamically
var points = [];
updatePoints(points);
function updatePoints(pts) {
for (var p in pts) {
L.marker([pts[p].lat, pts[p].long]).addTo(map).bindPopup(pts[p].message);
}
}
scope.$watch(attrs.pointsource, function(value) {
updatePoints(value);
});
}
};
});
And inside the controller, the way to add new marker is
$scope.pointsFromController.push({
lat: val.geoLat,
long: val.geoLong
});
Code in the HTML is simple
<sap id="map" pointsource="pointsFromController"></sap>
Leaflet creator here. The best way to clear markers is to add them to a group (instead of directly adding to the map), and then call group.clearLayers() when you need. http://leafletjs.com/examples/layers-control.html
Assuming pointsFromController is a simple javascript array.
You can simply undo the pushes you've made.
$scope.removePoints = function() {
for (var i=$scope.pointsFromController.length; i>=0; i--)
$scope.pointsFromController.pop();
};

angularjs with Leafletjs

Following directie code is from http://jsfiddle.net/M6RPn/26/
I want to get a json feed that has many lat and long.. I can get a json with $resource or $http in Angular easily but How can I feed it to this directive to map thing on the map?
module.directive('sap', function() {
return {
restrict: 'E',
replace: true,
template: '<div></div>',
link: function(scope, element, attrs) {
var map = L.map(attrs.id, {
center: [40, -86],
zoom: 10
});
//create a CloudMade tile layer and add it to the map
L.tileLayer('http://{s}.tile.cloudmade.com/57cbb6ca8cac418dbb1a402586df4528/997/256/{z}/{x}/{y}.png', {
maxZoom: 18
}).addTo(map);
//add markers dynamically
var points = [{lat: 40, lng: -86},{lat: 40.1, lng: -86.2}];
for (var p in points) {
L.marker([points[p].lat, points[p].lng]).addTo(map);
}
}
};
});
I don't know a lot about Leaflet or what you're trying to do, but I'd assume you want to pass some coordinates in from your controller to your directive?
There are actually a lot of ways to do that... the best of which involve leveraging scope.
Here's one way to pass data from your controller to your directive:
module.directive('sap', function() {
return {
restrict: 'E',
replace: true,
template: '<div></div>',
link: function(scope, element, attrs) {
var map = L.map(attrs.id, {
center: [40, -86],
zoom: 10
});
//create a CloudMade tile layer and add it to the map
L.tileLayer('http://{s}.tile.cloudmade.com/57cbb6ca8cac418dbb1a402586df4528/997/256/{z}/{x}/{y}.png', {
maxZoom: 18
}).addTo(map);
//add markers dynamically
var points = [{lat: 40, lng: -86},{lat: 40.1, lng: -86.2}];
updatePoints(points);
function updatePoints(pts) {
for (var p in pts) {
L.marker([pts[p].lat, pts[p].lng]).addTo(map);
}
}
//add a watch on the scope to update your points.
// whatever scope property that is passed into
// the poinsource="" attribute will now update the points
scope.$watch(attr.pointsource, function(value) {
updatePoints(value);
});
}
};
});
Here's the markup. In here you're adding that pointsource attribute the link function is looking for to set up the $watch.
<div ng-app="leafletMap">
<div ng-controller="MapCtrl">
<sap id="map" pointsource="pointsFromController"></sap>
</div>
</div>
Then in your controller you have a property you can just update.
function MapCtrl($scope, $http) {
//here's the property you can just update.
$scope.pointsFromController = [{lat: 40, lng: -86},{lat: 40.1, lng: -86.2}];
//here's some contrived controller method to demo updating the property.
$scope.getPointsFromSomewhere = function() {
$http.get('/Get/Points/From/Somewhere').success(function(somepoints) {
$scope.pointsFromController = somepoints;
});
}
}
I recently built an app using Angular JS and Leaflet. Very similar to what you've described, including location data from a JSON file. My solution is similar to blesh.
Here's the basic process.
I have a <map> element on one of my pages. I then have a Directive to replace the <map> element with the Leaflet map. My setup is slightly different because I load the JSON data in a Factory, but I've adapted it for your use case (apologies if there are errors). Within the Directive, load your JSON file, then loop through each of your locations (you'll need to setup your JSON file in a compatible way). Then display a marker at each lat/lng.
HTML
<map id="map" style="width:100%; height:100%; position:absolute;"></map>
Directive
app.directive('map', function() {
return {
restrict: 'E',
replace: true,
template: '<div></div>',
link: function(scope, element, attrs) {
var popup = L.popup();
var southWest = new L.LatLng(40.60092,-74.173508);
var northEast = new L.LatLng(40.874843,-73.825035);
var bounds = new L.LatLngBounds(southWest, northEast);
L.Icon.Default.imagePath = './img';
var map = L.map('map', {
center: new L.LatLng(40.73547,-73.987856),
zoom: 12,
maxBounds: bounds,
maxZoom: 18,
minZoom: 12
});
// create the tile layer with correct attribution
var tilesURL='http://tile.stamen.com/terrain/{z}/{x}/{y}.png';
var tilesAttrib='Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under CC BY SA.';
var tiles = new L.TileLayer(tilesURL, {
attribution: tilesAttrib,
opacity: 0.7,
detectRetina: true,
unloadInvisibleTiles: true,
updateWhenIdle: true,
reuseTiles: true
});
tiles.addTo(map);
// Read in the Location/Events file
$http.get('locations.json').success(function(data) {
// Loop through the 'locations' and place markers on the map
angular.forEach(data.locations, function(location, key){
var marker = L.marker([location.latitude, location.longitude]).addTo(map);
});
});
}
};
Sample JSON File
{"locations": [
{
"latitude":40.740234,
"longitude":-73.995715
},
{
"latitude":40.74277,
"longitude":-73.986654
},
{
"latitude":40.724592,
"longitude":-73.999679
}
]}
Directives and mvc in angularJs are different technologies. Directives are usually executed when the page loads. Directives are more for working on/with html and xml. Once you have JSON, then its best to use the mvc framework to do work.
After the page has rendered, to apply directives you often need to do $scope.$apply() or $compile to register the change on the page.
Either way, the best way to get a service into a directive is by using the dependency injection framework.
I noticed the scope:true, or scope:{} was missing from your directive. This has a big impact on how well the directive plays with parent controllers.
app.directive('mapThingy',['mapSvc',function(mapSvc){
//directive code here.
}]);
app.service('mapSvc',['$http',function($http){
//svc work here.
}])
Directives are applied by camelCase matching. I would avoid using or because of an issue with IE. Alternative would be
<div map-thingy=""></div>
Assuming that in your controller you got
$scope.points = // here goes your retrieved data from json
and your directive template is:
<sap id="nice-map" points="points"/>
then inside your directive definition you can use the "=" simbol to setup a bi-directional binding between your directive scope and your parent scope
module.directive('sap', function() {
return {
restrict: 'E',
replace: true,
scope:{
points:"=points"
},
link: function(scope, element, attrs) {
var map = L.map(attrs.id, {
center: [40, -86],
zoom: 10
});
L.tileLayer('http://{s}.tile.cloudmade.com/57cbb6ca8cac418dbb1a402586df4528/997/256/{z}/{x}/{y}.png', {
maxZoom: 18
}).addTo(map);
for (var p in points) {
L.marker([p.lat, p.lng]).addTo(map);
}
}
};
});
Also instead of adding the markers right into the map, it's recomended to add your markers to a L.featureGroup first, and then add that L.featureGroup to the map because it has a clearLayers() metho, which will save you some headaches when updating your markers.
grupo = L.featureGroup();
grupo.addTo(map);
for (var p in points) {
L.marker([p.lat, p.lng]).addTo(grupo);
}
// remove all markers
grupo.clearLayers();
I hope this helps, cheers

Resources