Accessing a collection from outside of the $scope - javascript

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.

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();
});

AngularJS with ng-map to implement dynamic heatmap visualization

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.

AngularJS - Refresh after POST

What is the correct way to refresh content after a http POST request in Angular?
//controller.js
var hudControllers = angular.module('hudControllers', []);
hudControllers.controller('PropertyDetailsCtrl',
['$scope','$window','$http', function ($scope,$window,$http) {
//I want to reload this once the newCommentForm below has been submitted
$http.get('/api/comments')
.success(function(data) {$scope.comments = {"data":data};}})
.error(function(data) {...);
$scope.newCommentForm = function(){
newComment=$scope.newComment;
requestUrl='/api/comments';
var request = $http({method: "post",url: requestUrl,data: {...}});
request.success(function(){
//How do I refresh/reload the comments?
$scope.comments.push({'comment':'test'}); //Returns error - "TypeError: undefined is not a function"
});
};
}]);
//template.ejs
<div class="comment">
<ul>
<li ng-repeat="comment in comments.data">{{comment.comment}}</li>
</ul>
</div>
Thanks.
There are many ways you can do it. still I want to show you simplest way (according to your needs).
lets say you have 'first.html' page and 'PropertyDetailsCtrl' is associated with it.
Now, in html you can write like this,
with very first-div
<div ng-controller="PropertyDetailsCtrl" ng-init="initFirst()">
.... Your page contents...
</div> (This will initialize your controller and you will have execution of your first method 'initFirst()'.
at your .js side....
var hudControllers = angular.module('hudControllers', []);
hudControllers.controller('PropertyDetailsCtrl',
['$scope','$window','$http', function ($scope,$window,$http) {
//I want to reload this once the newCommentForm below has been submitted
$scope.initFirst=function()
{
$http.get('/api/comments')
.success(function(data) {...})
.error(function(data) {...);
//You need to define your required $scope.....
$scope.myVariable=data;
};
Now at appropriate time (You know when) your below method gets called.
$scope.newCommentForm = function(){
newComment=$scope.newComment;
requestUrl='/api/comments';
var request = $http({method: "post",url: requestUrl,data: {...}});
request.success(function(data){
//How do I refresh/reload the comments?
//without calling anything else, you can update your $scope.myVariable here directly like this
$scope.myVariable=data
});
//or else you can call 'initFirst()' method whenever and wherever needed like this,
$scope.initFirst();
};
}]);
I hope this will help.
Not sure if there is ONE correct way, but you can call $location.path() on success.
request.success(function(){
$location.path('your path');
});
To view the added comment (which apparently is what you want), you could use :
request.success(function(response){
$scope.comments.push(response.data);
});
Your content would refresh automatically on the page, if you're using angular expressions and a ng-repeat.

ng-repeat not growing with object

I'm doing this:
<body>
<div ng-controller="PresentationCtrl">
Find
<div>
<ul class ="unstyled">
<li ng-repeat="p in presentations">
<img ng-src="{{p}}" alt="presentation">
</li>
</ul>
<div>
</div>
</body>
I have one placeholder element inside of presentations that is set when the function PresentationCtrl is hit.
When the findAll link is hit, I add an element to the array like so: $scope.presentations.push(sUrl);
But, when viewing my page, the list doesn't grow.
Does ng-repeat only fire once, or something? Can I force it to "refresh" and display the current values in presentations?
Here's my controller
The console.log before I push the element into the array gets hit. and displays the string that I expect.
function PresentationCtrl($scope){
$scope.presentations = ["http://angularjs.org/img/AngularJS-small.png"];
$scope.findAll = function(){
var socket=null;
socket = io.connect('http://[server]:3000');
socket.on('handshake',function(){
socket.emit('viewAll',{tenant:'qa'});
socket.on('returnAll',function(back){
for(i=0;i<back.length;i++){
for(j=0;j<back[i].slides.length;j++){
socket.emit('signUrl',(back[i].slides[j].location));
break;
}
}
socket.on('signedUrls',function(sUrl){
console.log("back from signedUrls" + sUrl);
$scope.presentations.push(sUrl);
});
});
});
};
}
it works fine.
http://plnkr.co/edit/agadSBFgz8YhWuDTtCPx?p=preview
Your situation might be different.
--------- EDIT ---------
ok, I got your situation. You are running a function not under watch of AngularJS.
Thus, AngularJS does not know that your variable is changed, basically $digest is not called.
To mimic native Javascript asynchronous call, I use setTimeout.
The following code does not work. It change the value, but it won't be watched.
setTimeout( function() {
$scope.presentations = [6,7,8,9];
}, 1000);
However this code does work. It change the value as expected
$timeout( function() {
$scope.presentations = [6,7,8,9];
}, 1000);
The difference between setTimeout and $timeout is that $timeout is running under watch of AngularJS.
Thus to make it work, you need to run $scope.apply() after it, or within it as a function.
This code does work with setTimeout.
setTimeout( function() {
$scope.presentations = [6,7,8,9];
$scope.$apply();
}, 1000);
Can't explain all to you in detail because I don't really mastered the AngularJS code.
This blog has a very good explanation about why we need to run $apply.
http://jimhoskins.com/2012/12/17/angularjs-and-apply.html
Just for the tip, to get the answer quickly, it's better to have a simplified example in jsfiddle or plunkr.
I know $http call cannot be demoed in plunkr or jsfiddle, but you can mimic your situation using setTimeout. It can make your situation understandable to readers most of time.

Running some code only once in AngularJS

I have some code that I need to run only once, but I'm not sure where do that code belongs to (service? factory?)
This is the code:
socket.on('recv chat', function (data){
$("#chat").append(
"<b><" + data.nick + "></b>: " +
data.texto +
"<br>"
);
});
As you can see from the code, it's just a basic chat-app. My whole webpage has a few tabs and one of those tabs is the chat-tab. If I put this code inside my chat's controller, it gets executed on each tab-switch, so when somebody sends a message, it gets appended a few times.
Where should I place it for it to be executed only once?
You say it should execute only once, but presumably what you actually want is just that it display the values. If so the obvious thing would be for the code to update the model and then use angular's data binding for the display. (A good rule of thumb in angular would be that anywhere except a directive that tries to manipulate the DOM is probably doing it wrong).
So, some untested code to put inside your controller might be:
socket.on('recv chat', function (data){
$scope.apply(function() {
$scope.nick = data.nick;
$scope.texto = data.texto;
});
});
And your html just has:
<div ng-show="nick"><b><{{nick}}></b>: {{texto}}</div>
I think you need to wrap the model updates in $scope.apply() otherwise the event won't be happening in the correct angular context.
Answer to your comment:
Is there a new socket within each new instance of the controller? If so there's no problem as the old event handler at worst updates the old model and should go away when the old socket goes away. If you're re-using the socket between controllers then I think you want to define a service to handle the socket and you can register the callback with the service.
var stopWatch = $scope.$on('someEvent', function(){
//some code here
stopWatch();
});
This is not the best solution but this is by far the best working solution i have came across. You need to remove the already registered even. Just add this line before you attach the events.
socket.removeAllListeners();
socket.on('recv chat', function (data){
$("#chat").append(
"<b><" + data.nick + "></b>: " +
data.texto +
"<br>"
);
});

Categories