AngularJS with ng-map to implement dynamic heatmap visualization - javascript

Hi everyone I am an absolute newbie to frontend development and it is my first time to use something like AngularJS, tbh I think it is really difficult and I don't really have a lot of clear idea about how it works. I am working on a project that aims to visualize a batch of coordinates on Google Map. I have already built the corresponding API at the server-side and it works fine.
I'm using ng-map in my project and below is the first version of my code of main.html:
<div class="screen-wrapper" id="map-wrapper">
<div map-lazy-load="https://maps.google.com/maps/api/js">
<ng-map center="37.782551, -122.445368" zoom="3" disable-default-u-i="true">
<heatmap-layer id="foo" data="dummyData"></heatmap>
</ng-map>
</div>
</div>
The dummyData above is a hard-coded array in an external JS file which I originally thought unnecessary, but later when I tried to get the heatmapLayer from the map I found this was very important. Without this attribute I couldn't get the heatmapLayer in my controller. That was a bit odd, but I just don't know why it is like that.
Below is my main.controller.js:
(function() {
'use strict';
angular
.module('webapp')
.controller('MainController', MainController);
/** #ngInject */
function MainController(NgMap, $scope, $http) {
var resp = $http.get('url.to.api')
.then(function(result) {
return result.data;
});
var incidentData = []
var extractor = Promise.resolve(resp).then(function(data) {
for (var i = 0; i < data.length; i++) {
var lat = data[i]["lat"];
var lon = data[i]["lon"];
incidentData.push(new google.maps.LatLng(lat,lon));
}
}).then(function() {
var incidentArray = new google.maps.MVCArray(incidentData);
var heatMapLayer = new google.maps.visualization.HeatmapLayer({
data: incidentArray,
radius: 20
});
var heatmap;
$scope.$on('mapInitialized', function(event, map) {
heatMapLayer.setMap(map.instance);
});
});
}
})();
What I am trying to do above is to do a HTTP request to get the latitudes and longitudes back. Since it is in the format of "Promise", I have to extract them and put it into an array. Everything seems look okay to me, but it is simply not working. The interface only displays the 'hard-coded' visualization but not the new heatMapLayer.
Can anyone please suggestion the direction/solution to fix the problem I stated above? Thanks everyone and StackOverflow.
update After Daniel's advice I have made the following modification to my code.
In main.controller.js I changed the code segment:
$scope.$on('mapInitialized', function(event, map) {
heatMapLayer.setMap(map.instance);
});
to:
$scope.$apply(function() {
$scope.$on('mapInitialized', function(event, map) {
heatMapLayer.setMap(map.instance);
});
});
Again, thanks Daniel!

You are probably bypassing the digest cycle of angular. Meaning your changes are done already, but they don not display yet.
Angular keeps track of all the changes by itself, updating the view when something changes. Whenever you change something manually, not tracked by angular (e.g. tracked through ng-bind, or variables in {{ }}), you need to call scope.$apply() in order to trigger the digest cycle manually. Normally, you do not even notice (or need to know), when angular does this automatically. $http for example will take care of that too.
However, here
var resp = $http.get('url.to.api')
.then(function(result) {
return result.data;
});
I suspect scope.$digest() gets already called after the callback (at that time you did not change anything yet).
var extractor = Promise.resolve(resp).then(function(data) {
In your second .then() callback, you do your changes, but they will not be displayed anymore.
I would suggest to put the code from below into the first .then() callback. If this is not possible, you will have to call scope.$apply() manually after your changes are done.

Related

Dynamically add to a list in angular

Goal: a dynamically generated list from external source.
I've set up a simple angular app that gets a list of events from an external JSON source. I want the list to update when events are added from the external source. It's currently working, but I have one problem and three questions:
1) I'm currently rewriting the list every 15 seconds. How do I just add to the end of the list without rewriting the list? (problem and question)
2) Is there another, better way to keep up to date with the external list? I'm trying to follow "RESTful" techniques, does that mean I should rely on the client side code to poll every so many seconds the way I'm doing? (best practice question)
3) Is setting the timeout in the controller best practice? Because it's controlling the action on the page?(best practice/comprehension question)
var eventModule = angular.module('eventModule', []);
eventModule.controller('eventControlller',
function($scope, $timeout, eventList) {
$scope.events = eventList.getAllEvents().success(
function(events) {$scope.events = events});
var poll = function() {
$timeout(function() {
$scope.events = eventList.getAllEvents().success(
function(events) {$scope.events = events});
poll();
}, 15000);
};
poll();
});
eventModule.factory('eventList', function($http) {
var url = "http://localhost/d8/events/request";
return {
getAllEvents: function() {
return $http.get(url);
}
};
});
If the list is an array, and you want to add new members to it, there are a few different ways. One way is to use the prototype.concat() function, like so:
function(events) {
$scope.events = $scope.events.concat(events)
});
If you cannot use that then you can go for loops solution:
function concatenateEvents(events) {
events.forEach(function(element) {
events.push(element);
}
}
Regarding the best ways to update the list, it depends on your requirements. If 15 seconds is not too long for you, then you can keep this logic, but if you need to speed up the response time, or even make it real time, then you need to emulate server-push architecture, which is different than the default web architecture, which is request-response architecture. Basically you may want to explore web sockets, and/or long polling, or reverse ajax, or comet... has many names. Web sockets is the recommended solution, others are only in case you have to use some non-compatible browsers.
Regarding the third question, I honestly don't know. Truly it doesn't feel good to control the UI from within your controller, but as I don't really know what your app is supposed to be doing, I don't know whether this is actually a bad way to do it.
Hope this helps!
EDIT - forgot to add another important point: You don't need to assign the eventList.getAllEvents() to $scope.events, as you are doing that in the callback handler function.
Perhaps you can modify your controller to something like this:
eventModule.controller('eventControlller', function($scope, $timeout, eventList) {
eventList.getAllEvents().success(
function(events) {
$scope.events = events
});
var poll = function() {
$timeout(function() {
eventList.getAllEvents().success(
function(events) {$scope.events = events});
poll();
}, 15000);
};
poll();
});

Need debugging/outside advice. Angular not working

So, my instructor thought it would be a great idea to throw in local storage and all that great nonsense the second week into learning Angular JS and basically told us to copy her code but change the names (as if that's learning). But anyways I have no idea how to work with angular js except for a few concepts and I need help finding the issues in my code. Any input would help. So far it looks as if the information from the form isn't being inputed to the html where it will be displayed. Here is my js fiddle. Any input is greatly appreciated
http://jsfiddle.net/g3tg5L15/1/
var app = angular.module('myApp', []);
app.controller("EmployeeController", function($scope, DataService){
$scope.empInfo = DataService.getEmpInfo();
$scope.newempInfo = {};
$scope.addNewEmpInfo = function(){
DataService.saveEmpInfo($scope.newempInfo.employee, $scope.newempInfo.street,
$scope.newempInfo.city,$scope.newempInfo.state, $scope.newempInfo.zip);
$scope.newempInfo = {};
};
$scope.removeEmpInformation = function(index){
DataService.removeEmpInfo(index);
};
$scope.clearInfo = function(){
DataService.destroyLocalStorage();
};
});
angular.module('myApp').service("DataService", function(){
var empInfoArray = [];
this.getEmpInfo = function(){
var employeeInfoArray = JSON.parse(localStorage.getItem("employeeInformationLS")) || [];
var empInfoArray = employeeInfoArray;
return empInfoArray;
};
this.saveEmpInfo = function(aName, aStreet, aState, aCity, aZip){
var savedEmpInfo = {employee : aName, street : aStreet, state : aState, city : aCity, zip : aZip};
empInfoArray.push(savedEmpInfo);
localStorage.setItem("employeeInformationLS", JSON.stringify(empInfoArray));
};
this.removeEmpInfo = function(aIndex){
empInfoArray.splice(aIndex, 1);
localStorage.setItem("employeeInformationLS", JSON.stringify(empInfoArray));
};
this.destroyLocalStorage = function(){
empInfoArray.splice(0);
localStorage.clear();
};
});
The main reason for the lack of response and debugging ability is due to AngularJS not loading correctly. For it to load you must change the dropdown in the left menu bar of jsFiddle from onLoad to No wrap - in <body> to load Angular correctly (as shown in the following screenshot).
The following surmises the issues I observed when debugging the code.
The getEmpInfo function within the DataService returns a new array each time it is called which prevents Angular from effectively monitoring it for changes. Instead of this function checking localStorage each time it is called it should just return the local array. The array can be simply loaded from localStorage when the service is first initialized.
The following update to the fiddle demonstrates this http://jsfiddle.net/g3tg5L15/6/. The changes implemented are as follows:
Change the dropdown in the menu bar of jsFiddle from onLoad to No
wrap - in <body> to load Angular correctly.
Added ng-click to 'Add Entry' button in HTML
<!-- Added ng-click to call addNewEmpInfo function on scope -->
<button ng-click='addNewEmpInfo()'>Add Entry</button>
Amended text on employeeInfo header to employee name rather than being hard coded value and added ng-click to remove in HTML.
<!-- Amended to add.employee rather than hardcoded value -->
<h3>{{add.employee}}</h3>
<!-- Added ng-click to call removeEmpInformation function on scope -->
<button ng-click='removeEmpInformation($index)'>X</button>
Amended the DataService to load from localStorage when it is initialized rather than initializing as an empty array.
var empInfoArray = JSON.parse(localStorage.getItem("employeeInformationLS")) || [];
Amend the getEmpInfo object to just return the local Array
this.getEmpInfo = function(){
return empInfoArray;
};
If necessary you can also watch for events triggered when localStorage changes, as included in the above fiddle. This will pick up changes from different tabs / windows if multiple are open. To monitor for these you must:
Include the services $window and $timeout in the DataService
angular.module('myApp').service("DataService", function($window, $timeout){
Add a trigger when a storage change occurs.
//Watch for storage events indicating changes to storage
angular.element($window).on('storage', function(event) {
//Check if the storage change was for our key
if (event.key === 'employeeInformationLS') {
//In a timeout (i.e. on next digest) update the array
//This could be done in a smarter way rather than clearing
//and rebuilding the entire array
$timeout(function(){
empInfoArray.splice(0);
var newArr = JSON.parse(localStorage.getItem("employeeInformationLS")) || [];
for (var i=0; i<newArr.length; i++){
empInfoArray.push(newArr[i]);
}
});
}
});

AngularFire - $firebaseArray can $add but not read

I feel like I must be missing something very basic. Here is a portion of my service:
angular.module('fire')
.constant('FIREBASE_URI', 'https://___.firebaseio.com/')
.factory('syncArraySvc', function(FIREBASE_URI, $firebaseArray) {
var buildingsUri = FIREBASE_URI + "buildings";
var buildings = $firebaseArray(new Firebase(buildingsUri));
console.log(buildings);
var getBuildings = function() {
return buildings;
};
var addBuilding = function(item) {
buildings.$add(item);
};
return {
getBuildings: getBuildings,
addBuilding: addBuilding
};
});
The console.log in the middle of that just returns an empty array. If I try to call the syncArraySvc.getBuildings() function from another controller, I also get an empty array. Somehow, $add(item) works, as does syncArraySvc.addBuilding(item). What am I missing?
If you look at $add $firebaseArray, It does create new item & add it into to $firebaseArray as like we have buildings. But as soon as you add item to ``$firebaseArray` it doesn't get added instantly. It get added when the $add promise get resolved.
I think you are doing correct thing, only you need call syncArraySvc.addBuilding(item) method on success of $add promise.
To make this approach you need to return a promise from the service method like
var addBuilding = function(item) {
return buildings.$add(item);
};
And then the caller function will take that promise and on resolve of it, he will call syncArraySvc.addBuilding(item) method that have assurity that items has added in buildings array.
syncArraySvc.addBuilding({foo: "bar"}).then(function(addedItem){
console.log(addedItem);
console.log(syncArraySvc.addBuilding(item)); //this will show you updated list
})
The other answers helped get me pointed in the right direction.
The API documentation has a code sample that doesn't seem to need the data to be wrapped in a promise:
var list = $firebaseArray(new Firebase(URL));
$scope.list = list;
However, it does point out that you can use the $loaded promise to be notified when the data is loaded. This is how I got it to work in my project:
syncArraySvc.getBuildings().$loaded(function(data) {
$scope.buildings = data;
});
I tried replicating this in a fresh project, and it consistently worked without the $loaded wrapper, like they show in the first example. It makes sense to me that the $loaded wrapper would be required. I don't understand how it could be working in the first example without it.
Try using a $timeout service inside the getBuildings function or rather when you call it. It probably takes a little while before data is returned.

How to refresh an angular tag

I was brought in to fix a website that was on fire a couple months back. I've got most things under control and I'm down to fixing various wish-list items. One of them involved some angular code that I just can't seem to get to do what I want. On some pages there are videos followed by a short quiz. I need to update the user's scores after each event. So far, this proved to be easy enough for the total score which looked like this:
<a id="updateafterscore" href="~/user/leaderboard/" class="fill-div">
{{ profile.currentScore }}
</a>
And that got updated with this:
document.getElementById('updateafterscore').innerHTML = data.Data.CurrentScore;
So far, so good. However other elements on the page have, thus far, proved impossible to update. Here's what's on the page:
I added the "id="refreshvideo" myself so I could try to alter the tag. Finally, here's the angular module for simple-circle (I've left out the actual drawing code since it's not really relevant):
angular.module('thrive.shared').directive('simpleCircle', function() {
return{
replace: true,
template: '<canvas width="60" height="60" style="margin: -10px 0 0 -15px;"></canvas>',
restrict: 'E',
scope: {
value: '#',
color: '#',
bgColor: '#',
forecolor: '#',
radius: '#'
},
link: function (scope, elem, attrs) {
var multiplyLength = 1;
var canvasElem = elem[0];
var inMotion = false;
if (scope.value <= 2) {
multiplyLength = 5;
}
scope.$watch('value', function() {
drawCircle(canvasElem, scope.color, scope.value * multiplyLength, scope.value, scope.name);
});
function drawCircle(canvas, color, calculatedPoints, displayPoints, name) {
So, to the question: how the heck do I update the number that's displayed? I tried various things:
document.getElementById('refreshvideo').setAttribute('value', data.Data.VideoWatchedCount);
document.getElementById('refreshvideo').setAttribute('data-value', data.Data.VideoWatchedCount);
$scope.profile.videosWatched = data.Data.VideoWatchedCount;
None of these things worked. I inspected the canvas element in the source in the browser and I could see the value and data-value tags change to whatever I set them, but the image remained unchanged. Am I setting the wrong thing? (Perhaps whatever $watch is watching) Do I have to force some kind of re-paint of a canvas element?
#charlietfl means your solution is not actually using AngularJS - you're completely bypassing it. Angular provides two-way data binding between Javascript data and the HTML DOM. All you do is tell it where to draw data, and it will do that for you automatically, keeping it up to date from then on as the data changes.
In Angular, you never call getElementById and certain never set innerHTML because then you block Angular from doing its thing - in many cases you actually break it. Every one of those instances introduces a new bug while "patching" another.
Go back to your example template line:
<a ..attributes...>{{ profile.currentScore }}</a>
When it sees this, Angular will create what it calls a "watcher" on profile.currentScore. If its value right now is '1', it will render this as <a ...>1</a>.
Every digest cycle, that watcher will tell it to look at profile.currentScore to see if it changed. This line of code is pretty typical in JS:
profile.currentScore = 42;
Angular will "see" this happen through that watcher, and will automatically update the rendered template. You do nothing else - and if you ever feel that you need to, it almost always means something else is wrong.
If you're running into this a lot, try the "standard quick-fix". We see this a lot with people who didn't architect an application properly, and they're doing data model updates outside Angular's digest cycle where it can't "see" them. Try wrapping your update code in an $apply() call:
$scope.$apply(function() {
profile.currentScore = 42;
});
If you have a LOT of updates to make and you don't want to nest the call, you can also cheat, like this:
// Lots of stuff...
profile.currentScore = 42;
// Lots more stuff...
$scope.$apply();
You will know right away if you need to do this. If it works, you need to do it. :) If you get an error message in your console saying you're already in a digest cycle, you do NOT need to do it (it's something else).
I mentioned that I thought perhaps I was modifying the wrong profile variable and so it wasn't refreshing. So I looked back a little bit in the code that is supplying the numbers:
angular.module('episodes').controller('episodeCtrl', ['$scope', '$rootScope', '$window', 'episode', 'relatedCourses', 'Video', 'episodeItems', 'profile', 'Profile',
function ($scope, $rootScope, $window, episode, relatedCourses, Video, episodeItems, profile, Profile) {
// stuff skipped....
onComplete: function () {
Video.complete({ videoId: item.item.id }).$promise.then(function () {
item.progress = "Completed";
$scope.loadNextItem();
$scope.profile = Profile.get(); // <<-- gotten from somewhere
$.ajaxSetup({ cache: false });
$.get('/user/getCurrentUserPointsModel', function (data) {
if (data == "")
return;
$scope.profile.currentScore = data.Data.CurrentScore;
$scope.profile.videosWatched = data.Data.VideoWatchedCount;
$scope.profile.testTakenAndCorrectAnswerCount = data.Data.TestTakenAndCorrectAnswerCount;
Profile.save(); // <-- added
The value in $scope.profile is pulled from Profile, but I don't fully get how that gets where it is. I suppose I will need to figure that out because there's another place where these updates have to happen that lack that Profile information. Anyways I added the last 4 lines in place of this:
document.getElementById('updateafterscore').innerHTML = data.Data.CurrentScore;
... and all worked according to plan. I guess I tackle the other part later when I figure out how the data gets to the controller.
You can't do this that way. It's not Angular way of dealing with data.
Read the documentation before https://docs.angularjs.org/tutorial/step_04
If you need to modify your DOM using document.. probably sth wrong is with your code.
BTW. Stop using globals like:
document.getElementById('updateafterscore')

Accessing a collection from outside of the $scope

Ok.. I've tried angular.js. It is awesome. I'm impressed. I can get bindings and stuff.. Cool.
Now what if I need to access to my data from outside of the $scope? Let's say I have a signalR hub that sends some data and function that intercepts that and should add a new item or modify existing. How do I do that? Can you show me on this example how can I access $scope.twitterResult from click handle?
<script>
angular.module('Twitter', ['ngResource'])
function TwitterCtrl($scope, $resource){
$scope.twitter = $resource('http://search.twitter.com/:action',
{action: 'search.json', q: 'obama', callback:'JSON_CALLBACK'},
{get:{method:'JSONP'}});
$scope.doSearch = function(){
$scope.twitterResult = $scope.twitter.get();
}
}
$(function(){
$('#addItem').click(function(){
// add a row to $scope.twitterResult
});
});
</script>
<body>
<div data-loading></div>
<div ng-controller='TwitterCtrl' ng-init="doSearch()">
<ul>
<li ng-repeat='tweet in twitterResult.results'><p> {{tweet.text}}</p></li>
</ul>
</div>
</body>
A better way would be to wrap your "signal hub" in an AngularJS service. Take a look on my blog post about using web sockets with AngularJS, specifically "Interacting with Socket.IO."
Why did you write:
$(function(){
$('#addItem').click(function(){
// add a row to $scope.twitterResult
});
});
And not just use ng-click? Is this some 3rd party code or widget? Pending on these this, I'll try to better advise you and write up some example code.
If you have to register an event handler, you should do so through a directive. Otherwise things will get complicated when you start managing the lifecycles of these outside-of-angular event bindings.
General answer is: you don't simply mess with the scopes from the outside.
But the requirement you have is a genuine one.
So in order to do what you want you need to establish a communication between outside of the scope and the scope itself.
The easiest way is to export the $scope to window and just mess with it, breaching into the scope from outside. You should NEVER do this. There be dragons.
The scope should maintain it's internal state.
I'm not exactly familiar with angular but you can do something to the effect of:
function TwitterCtrl($scope, $resource) {
// ...
$('body').bind('newTweetsArrived', data) {
// data contains the new tweets
// the decision to accept or not new tweets is made within the control
if (in_the_mood_to_accept_new_tweets) {
// add new tweets to the $scope.twitterResult
}
// optionally notify other components that new tweets are accepted
// so that they can adjust or whatever
$('body').trigger('afterNewTweetsArrived');
}
}
// you add new tweets by triggering global custom event
$(function(){
$('#addItem').click(function(){
$('body').trigger('newTweetsArrived', { ...here_are_the_tweets... });
});
});
You could probably do something like this, but I'm not sure if it's the best idea:
var myTwitterScope;
angular.module('Twitter', ['ngResource'])
function TwitterCtrl($scope, $resource){
$scope.twitter = $resource('http://search.twitter.com/:action',
{action: 'search.json', q: 'obama', callback:'JSON_CALLBACK'},
{get:{method:'JSONP'}});
$scope.doSearch = function(){
myTwitterScope = $scope;
$scope.twitterResult = $scope.twitter.get();
}
}
$(function(){
$('#addItem').click(function(){
// add a row to $scope.twitterResult
myTwitterScope.twitterResult.push(...); // or however you would do this.
});
});
As others have mentioned, this is not the cleanest solution.

Categories