Angularjs does not update view on async model changes - javascript

I've been having a problem with trying to keep my model separate from my controller because of lack of sync between model and view. I have looked around and found that most of the time an apply would solve the issue. However, apply does not work at all for me (either when called from the root scope or the relevant scope using chrome). In this link I have a demo of pretty much the problem I have on my program but instead of intervals my program has asynchronous requests or just complicated functions that seem to also be missed by angular. In the demo I have 4 variables that should be getting updated on the view. One that is being watched by the scope, another that is being updated through a callback, another that is just plain dependent on the model and one that is being updated by passing the scope itself to the service. Out of the 4 only the callback and passing the scope to the service are the ones that update the view, even when I run apply after each update (on top of the one that already runs after each execution of $interval). What I'm trying to avoid is using tons of callbacks or promises whenever my data changes due to transformations since I have many different transformations that are possible. Is there anyway to do this or are callbacks and promises the only option?
var test = angular.module("tpg",[]);
test.controller("myctrl", function($scope, $interval, service)
{
$scope.$watch(service.list.name, function()
{
$scope.name=service.list.name;
});
$scope.op=service.list.op;
$scope.call=service.list.call;
$scope.scope=service.list.test;
$scope.update=function()
{
service.getValues(function(op){$scope.op=op}, $scope);
};
}).factory("service", function($interval, $rootScope)
{
return {
list:{name:"OPA", op:"TAN", call:"1", test:"scope"},
getValues:function(callback, $scope)
{
var self=this;
var interval = $interval(function()
{
if(self.count>2)
{
$interval.cancel(interval);
self.count=0;
self.list={name:"OPA", op:"TAN", call:"1"};
}
else
{
self.list=self.values[self.count];
callback(self.list.op);
$scope.scope=self.list.test;
console.log(self.list);
self.count++;
}
$rootScope.$$phase || $rootScope.$apply();
},2000);
},
values: [{name:"guy", op:"ungly", call:"2", test:"scope1"}, {name:"TAL", op:"stink", call:"3", test:"scope2"}, {name:"tes", op:"test", call:"4", test:"scope3"}],
count:0
};
});

You need only a callback function to be returned from a service. $scope.$apply is not required when dealing with angular services as the service itself triggers the digest run. So I modified the code to remove the $apply and the promise and had a simple callback returned from the service which is then updating the view with the returned data.
Code:
$scope.update=function()
{
service.getValues(function(data){
$scope.name = data.name;
$scope.op=data.op;
$scope.call=data.call;
$scope.scope=data.test;
});
};
}).factory("service", function($interval, $rootScope)
{
return {
list:{name:"OPA", op:"TAN", call:"1", test:"scope"},
getValues:function(callback){
var self=this;
var interval = $interval(function()
{
if(self.count>2)
{
$interval.cancel(interval);
self.count=0;
self.list={name:"OPA", op:"TAN", call:"1"};
}
else
{
self.list=self.values[self.count];
console.log(self.list);
callback(self.list);
self.count++;
}
},2000);
},
values: [{name:"guy", op:"ungly", call:"2", test:"scope1"}, {name:"TAL", op:"stink", call:"3", test:"scope2"}, {name:"tes", op:"test", call:"4", test:"scope3"}],
count:0
};
});
Working plunkr

Related

Using $scope in AngularJS callbacks

I am trying to bind data to $scope within a callback function and display this in an html element.
Below is my angular code:
gAutocomplete.controller('geocoder', ['gdata', function($scope, gdata){
var geocoder = L.mapbox.geocoder('mapbox.places');
geocoder.query('New York', assign_geocode2());
function assign_geocode2() {
function assign_geocode(err, data) {
console.log(data);
$scope.lat = data.latlng[0];
$scope.lng = data.latlng[1];
console.log($scope.lat)
}
return assign_geocode;
};
}])
Below is HTML:
</div>
<div class="spacer50"></div>
<div class="center-block" style="width:600px" ng-cloak data-ng- controller='geocoder'>
{{"Chosen lat/long are"}} {{$scope.lat}} {{$scope.lng}}
</div>
I can see the controller gets executed, callback function is called and values are written to console.log. However, they are not propogated to HTML element. What could be happening?
Update
I am not using $timeout as below and getting errors that $timeout is not a function. i know I am using an intermediate tmp variable, but when I use $timeout in the closure, I still have the same issue.
gAutocomplete.controller('geocoder', ['$scope', 'gdata', '$timeout', function($scope, $timeout, gdata) {
var tmp = {}
var geocoder = L.mapbox.geocoder('mapbox.places');
geocoder.query('New York', assign_geocode2(tmp));
function assign_geocode2(tmp) {
function assign_geocode(err, data) {
tmp.lat = data.latlng[0],
tmp.lng = data.latlng[1]
}
return assign_geocode;
}
$timeout(function() {
$scope.lat = tmp.lat,
$scope.lng = tmp.lng,
console.log($scope)},0);
}
])
You're changing scope values from a non-angular event handler. This means you need to notify angular that, "hey, I've updated things, take note pls". AFAIK the ideal way of taking care of this is running the callback inside a $timeout call.
function assign_geocode(err, data) {
$timeout(() => {
console.log(data);
$scope.lat = data.latlng[0];
$scope.lng = data.latlng[1];
console.log($scope.lat)
});
}
Running this inside $timeout will cause angular to run a digest cycle and update scope values. You don't need to do this from events initiated by Angular, because it already knows its in a digest cycle. For example, services like $http take care of this for you.
Scope is the glue between application controller and the view. During the template linking phase the directives set up $watch expressions on the scope. The $watch allows the directives to be notified of property changes, which allows the directive to render the updated value to the DOM.
...
{{"Chosen lat/long are"}} {{lat}} {{lng}}
...
Example :
http://plnkr.co/edit/5TJJkYf21LlwPyyKjgTv?p=preview
https://docs.angularjs.org/guide/scope

AngularJS how to update view/scope after event listener

I have on my controller and service like this (both on separate file):
.controller('authCtrl',['$scope','MyConnect',function($scope,MyConnect){
/***************Testing Area******************/
console.log("connecting");
MyConnect.initialize();
$scope.myID = ??? //I want this to be updated
}
.factory('MyConnect', ['$q', function($q) {
var miconnect = {
initialize: function() {
this.bindEvents();
},
bindEvents: function() {
document.addEventListener('deviceready', this.onDeviceReady, false);
},
onDeviceReady: function() {
thirdPartyLib.initialize();
miconnect.applyConfig();
},
applyConfig: function() {
if (thirdPartyLib.isReady()) {
//I want in here to update $scope.myID in controller and reflect the changes in UI textbox
//$scope.myID = thirdPartyLib.id(); //something like this will be good
}
else {
}
}
}
return miconnect;
}])
So, I'm not sure how to update $scope.myID (which is a textbox). I'm not sure how to do the callback after event listener.. usually if ajax I can use .then to wait for the data to arrive.
Main thing is, I need to use 3rd party library (proprietary), and based on the guide is, to call thirdPartyLib.initialize() after device ready, then check if that thirdPartyLib.isReady() before actually calling the function to retrive the id.
You can't directly assign to $scope.myID until your service is ready. You need to somehow provide a callback that will assign the correct value to your $scope model. You could do this either by making the service return a Promise somewhere that resolves when it's ready, or by emitting an event from the service. I'll give an example of the last option. Depending on how much this thirdPartyLib is integrated with angular your may need to kick angular to get the scope to apply properly. Here I use $scope.$evalAsync. You could also return a promise that will resolve with the id rather than passing a callback directly in order to .then like you would with an ajax library.
Also, if the thirdPartyLib is particularly sucky, and it's initialize is asynchronous, and it doesn't provide you any callback/promise/event driven indicator that it's ready, you may need to
.controller('authCtrl', ['$scope', 'MyConnect',
function($scope, MyConnect) {
console.log("connecting");
// my connect should probably just `initialize()` on it's own when it's created rather than relying on the controller to kick it.
MyConnect.initialize();
MyConnect.whenID(function(id) {
// $evalAsync will apply later in the current $digest cycle, or make a new one if necessary
$scope.$evalAsync(function(){
$scope.myID = id;
});
})
}
])
.factory('MyConnect', ['$q', '$rootScope'
function($q, $rootScope) {
var miconnect = {
...,
onDeviceReady: function() {
thirdPartyLib.initialize();
miconnect.applyConfig();
/* Also, if the `thirdPartyLib` is particularly sucky, AND if it's initialize is asynchronous,
* AND it doesn't provide any callback/promise/event driven indicator that it's ready,
* you may need to hack some kind of `setTimeout` to check for when it is actually `isReady`. */
// ok, listeners can do stuff with our data
$rootScope.$emit('MyConnect.ready');
},
whenID: function(callback) {
if (thirdPartyLib.isReady()) {
callback(thirdPartyLib.id);
} else {
var unregister = $rootScope.$on('MyConnect.ready', function() {
// unregister the event listener so it doesn't keep triggering the callback
unregister();
callback(thirdPartyLib.id);
});
}
}
}
return miconnect;
}
])

Proper Way to Handle Asynchronous Updates in Angular Controller

Here's my setup -- I have a controller that is using a service that does some work and then returns data asynchronously. In this case, the data is returned by a timeout, but in real life this would do something more interesting:
View:
<div ng-controller="RootController as root">
<h1>Angular Project</h1>
<div ng-show="{{root.greeting}}">
<p>{{root.greeting}}</p>
</div>
</div>
Controller:
(function(){
'use strict';
function RootController(greetingService) {
var vm = this;
vm.greeting = '';
// startup logic
activate();
function activate() {
greetingService.greeting().then(
function( response ) {
vm.greeting = response;
},
function( error ) {
vm.greeting = error;
}
);
}
}
RootController.$inject = ['greeting'];
angular.module('app.core').controller('RootController', RootController);
})();
Service:
(function() {
'use strict';
// Greeting Service
function greeting( $timeout ) {
// public API
var service = {
greeting: greeting
};
return service;
// internal functions
function greeting() {
return $timeout( function() {
return 'Hello world!';
}, 1000 );
}
}
temp.$inject = ['$timeout'];
angular.module('app.core').factory( 'greeting', greeting );
})();
Questions:
Why is it that my view is not updating when the timeout resolves and the vm.greeting assignment occurs in my controller? I've seen people describe "inside Angular vs outside Angular", but it would seem to me that I haven't gone "outside Angular" here.
I'm aware that I can call $scope.$apply(), but I've encountered the "digest is already in progress" error, and again it doesn't seem like I should have to do this.
Is there a better way I should be organizing my components? I have also experimented with broadcasting an event over $rootScope and writing an event handler in the Controller, but this arrangement exhibits the same outcome (namely the view is not updated when the asynchronous model change occurs).
You don't need the curly braces for ng-show
https://docs.angularjs.org/api/ng/directive/ngShow
change
<div ng-show="{{root.greeting}}">
to
<div ng-show="root.greeting">
The way you have structured your code is a very different from what I normally do. Check out this link for a great style guide.http://toddmotto.com/opinionated-angular-js-styleguide-for-teams/
As for your issue, Angular uses $scope to bind a value in the controller to the view. So your controller should have $scope injected and you can then do$scope.greeting in place of vm.greeting.

How to make a loading indicator for every asynchronous action (using $q) in an angularjs-app

I know there are several approaches to loading-indicators in angular js (this one, for example: https://gist.github.com/maikeldaloo/5140733).
But they either have to be configured for every single call, or - if they act globally, as I want - just apply to http-requests, but not to $q-promises being used in services.
The global loading indicators, I've seen so far, work with
$httpProvider.responseInterceptors.push(interceptor);
Is there something similiar for $q, like a $qProvider.reponseInterceptors? And if not, what would be the most convenient way to implement such a functionality? Is it possible to use a decorator-pattern of some kind for example?
Although I find it very complicated, unnecessary and probably broken, you could decorate $q and override its defer function.
Every time someone asks for a new defer() it runs your own version which also increments a counter. Before handing out the defer object, you register a finally callback (Angular 1.2.0 only but always may fit, too) to decrement the counter.
Finally, you add a watch to $rootScope to monitor when this counter is greater than 0 (faster than having pendingPromisses in $rootScope and bind like ng-show="pendingPromisses > 0").
app.config(function($provide) {
$provide.decorator('$q', ['$delegate', '$rootScope', function($delegate, $rootScope) {
var pendingPromisses = 0;
$rootScope.$watch(
function() { return pendingPromisses > 0; },
function(loading) { $rootScope.loading = loading; }
);
var $q = $delegate;
var origDefer = $q.defer;
$q.defer = function() {
var defer = origDefer();
pendingPromisses++;
defer.promise.finally(function() {
pendingPromisses--;
});
return defer;
};
return $q;
}]);
});
Then, view bound to a scope that inherits from $rootScope can have:
<span ng-show="loading">Loading, please wait</span>
(this won't work in directives with isolate scopes)
See it live here.
There is a good example in the official documentation working for the current stable 1.2.0.
http://docs.angularjs.org/api/ng.$http (top quarter of the page, search for Interceptors)
My extraction of these documentation lead me to this solution:
angular.module('RequestInterceptor', [])
.config(function ($httpProvider) {
$httpProvider.interceptors.push('requestInterceptor');
})
.factory('requestInterceptor', function ($q, $rootScope) {
$rootScope.pendingRequests = 0;
return {
'request': function (config) {
$rootScope.pendingRequests++;
return config || $q.when(config);
},
'requestError': function(rejection) {
$rootScope.pendingRequests--;
return $q.reject(rejection);
},
'response': function(response) {
$rootScope.pendingRequests--;
return response || $q.when(response);
},
'responseError': function(rejection) {
$rootScope.pendingRequests--;
return $q.reject(rejection);
}
}
});
You might then use pendingRequests>0 in an ng-show expression.
Since requested by the OP, this is based on the method we are using for the app we are currently working on. This method does NOT change the behaviour of $q, rather adds a very simple API to handle promises that need some kind of visual indication. Although this needs modification in every place it is used, it is only a one-liner.
Usage
There is a service, say ajaxIndicator, that knows how to update a portion of the UI. Whenever a promise-like object needs to provide indication until the promise is resolved we use:
// $http example:
var promise = $http.get(...);
ajaxIndicator.indicate(promise); // <--- this line needs to be added
If you do not want to keep a reference to the promise:
// $http example without keeping the reference:
ajaxIndicator.indicate($http.get(...));
Or with a resource:
var rc = $resource(...);
...
$scope.obj = rc.get(...);
ajaxIndicator.indicate($scope.obj);
(NOTE: For Angular 1.2 this would need tweeking, as there is no $then() on the resource object.)
Now in the root template, you will have to bind the indicator to $rootScope.ajaxActive, e.g.:
<div class="ajax-indicator" ng-show="ajaxActive"></div>
Implementation
(Modified from our source.) WARNING: This implementation does not take into account nested calls! (Our requirements called for UI blocking, so we do not expect nested calls; if interested I could try to enhance this code.)
app.service("ajaxIndicator", ["$rootScope"], function($rootScope) {
"use strict";
$rootScope.ajaxActive = false;
function indicate(promise) {
if( !$rootScope.ajaxActive ) {
$rootScope.ajaxActive = true;
$rootScope.$broadcast("ajax.active"); // OPTIONAL
if( typeof(promise) === "object" && promise !== null ) {
if( typeof(promise.always) === "function" ) promise.always(finished);
else if( typeof(promise.then) === "function" ) promise.then(finished,finished);
else if( typeof(promise.$then) === "function" ) promise.$then(finished,finished);
}
}
}
function finished() {
$rootScope.ajaxActive = false;
}
return {
indicate: indicate,
finished: finished
};
});
I had the same big question few weeks ago and I happen to make some directives to represent the loading state on the action buttons and ng-repeat content loading.
I just spent some time and pushed it on github:
https://github.com/ocolot/angularjs_loading_buttons
I hope it helps.

Scope changes from inside a Promise

I'm using BreezeJS with AngularJS but I'm having a difficult time understanding how to get Promises to work with $scope. Whenever I try to submit my form its not showing the validation errors until I click it for a 2nd time. I realize I could call $scope.$apply() but I read its not best practice? Here is my code:
app.controller("MainController", ["$scope", "$q", "datacontext", function ($scope, $q, datacontext) {
datacontext.manager.fetchMetadata();
$scope.errors = [];
$scope.addDamp = function () {
var item = datacontext.manager.createEntity("Damp", {
name: $scope.newDamp
});
var tes = datacontext.manager.saveChanges()
.then(function () {
alert("yay");
})
.fail(function (error, a, b, c) {
var arr = [];
error.entitiesWithErrors.map(function (entity) {
entity.entityAspect.getValidationErrors().map(function (validationError) {
arr.push(validationError.errorMessage);
});
});
$scope.errors = arr;
datacontext.manager.rejectChanges();
});
};
}]);
What is the best way to go about handling scope changes that come from inside of a Promise?
Yes, you're going to need $scope.apply here, because the promise isn't coming out of a core Angular call (such as $http, which would have handled the .apply() itself behind the scenes). In fact, the Breeze/Angular example on the BreezeJS page (http://www.breezejs.com/samples/todo-angular) includes a $scope.apply() after its data retrieval:
datacontext.getAllTodos()
.then(success)
.fail(failed)
.fin(refreshView);
function refreshView() {
$scope.$apply();
}
It's a bad practice to toss $scope.apply() about where you don't need it. But when you're handling promises created outside of Angular itself, it's going to come up.

Categories