$watch doesn't work and is driving me mad - angularjs

So, what I basically want to have is visualisation of the socket connection for the user. Meaning an indication wheter the client is connected to the server over socket.io or not.
Therefor I have service which keeps track of the socket state:
[...]
.factory('DataConnection', function(CONN_EVENTS, socket, $rootScope) {
var dataConnection = {};
dataConnection.states={
// indicates if the user can interact with the ui
uiEnabled: false,
[...]
};
// socket IO connection states
$rootScope.$on(CONN_EVENTS.socketAuth, function() {
dataConnection.states.uiEnabled = true;
});
$rootScope.$on(CONN_EVENTS.socketUnAuth, function() {
dataConnection.states.uiEnabled = false;
});
$rootScope.$on(CONN_EVENTS.socketConnLost, function() {
dataConnection.states.uiEnabled = false;
});
}
which sets the states based on events. This events are emitted in a different service/factory which handles the socket events.
I tried many approaches to sync these values with my controller. It kinda worked with a manual $scope.$apply(), but sometimes it gave me the apply already in progress error and since its very bad practice, I decided to not use it.
I ended up with this solution in my controller:
.controller("MainCtrl", function($scope, DataConnection) {
$scope.$watch(function() {
return DataConnection.states.uiEnabled
}, function(uiEnabled) {
$scope.uiEnabled = uiEnabled;
}, true);
Which doesn't want to work somehow. When the socket get disconnected (the server gets shut down), the ui won't update. After interaction (e.g. trigger a popover), the ui will update as expected.
Is there anything I'm missing? Or something else I can try?

OK this is not a solution to get $watch working but instead just a workaround that "might" get your first approach using events to work without throwing a digest already in progress error. So instead of using $scope.$apply() enclose changes you make inside $timeout(function()) instead like this
$timeout(function(){
// do whatever changes you want here
}) // no timeout value
NOTE that there is no timeout actually added above. Why i think this might work is because a $timeout automatically applies whatever changes you made once script inside completes execution (ie, its like a $scope.$spply() that won't throw a digest cycle already in progress error). I am not sure about this but just give it a try.

I'd simply suggest you to use $scope.$applyAsync(), which means that if an apply in progress, it will do it on the next $digest only.

To me is it OK your approach, to watch a function that returns an object to be observed, I don't know what could be the problem, but I would use callbacks:
.factory('DataConnection', function(CONN_EVENTS, socket, $rootScope) {
var dataConnection = {};
dataConnection.states={
_uiEnabledBackingField: false,
// indicates if the user can interact with the ui
uiEnabled: function(val) {
if (typeof val !== 'undefined') {
this._uiEnabledBackingField = val;
this.callback(val);
return;
}
return this._uiEnabledBackingField;
},
onUiEnabled: function(cb){
if (cb) {
this.callback = cb;
}
}
};
});
And then in my controller:
.controller("MainCtrl", function($scope, DataConnection) {
dataConnection.states.onUiEnabled(function(uiEnabled){
$scope.uiEnabled = uiEnabled;
});
});

I did some research and this is my conclusion:
The $apply will trigger in three different cases (Exploring Angular 1.3: Go fast with $applyAsync) In my case (where I have to update the DOM after a socket event), the $apply has to be triggered manually.
This example is also found in the Angular Wiki:
If you're creating an AngularJS service (such as for sockets) it should have a $scope.$apply() anywhere it fires a callback.
I tried to trigger the $apply in the socket service already (instead of the controller), which worked way better and didn't give me the "$apply already in progress error".

Related

How $watch changes of a variable in a service from component's controller?

I have been through all related topics on SO, namely these two:
$watch not detecting changes in service variable
$watch not detecting changes in service variable
are tackling the same issue, but i failed to make it working. Unlike in the above cases, I am using a controller from a component, hence maybe this is related to lacking binding in components, idk. Hope for some experinced assistance.
Have a service:
(function (angular) {
'use strict';
angular
.module('Test')
.service('ShareData', ShareData);
ShareData.$inject = [];
function ShareData() {
let vm = this;
vm.indexes = [];
vm.setIndexes = function(firstIndexParam, lastIndexParam, message) {
if (leaderIndexParam !== undefined || partnerIndexParam !== undefined) {
vm.indexes.mainIndex = firstIndexParam;
vm.indexes.secondaryIndex = lastIndexParam;
vm.indexes.message = message;
}
};
vm.getIndexes = function() {
return vm.indexes;
};
}
})(angular);
It is used in 3 components. Two of them are sending data into the service, the third one uses this data. Sending of data is accomplished in the following way, works:
ShareData.setIndexes(firstIndex, secondIndex, 'update_indexes');
Now here is my problem. In main parent controller i can comfortably access the data by
ShareData.getIndexes();
But my issue is that I need changes in indexes to trigger certain actions in parent controller, so I tried so do as stipulated by relevant questions here on SO:
$scope.$watch('ShareData.getIndexes()', function(newVal) {
console.log('New indexes arrived', newVal);
});
In main controller, I am injecting the service:
TabController.$inject = ['ShareData'];
and using it like:
let indexService = ShareData.getIndexes();
As i said, I can get the data when I am explicitly calling the function. The issue is that it needs to be triggered by the service itself.
It does not work regardless of shamanistic ceremonies a made :) Now, obviously, I am missing something. Should I somehow bind this service to the component, and if yes how is it done? Or maybe the solution is totally dysfunctional and impossible to achieve in my circumstances? An advise is appreciated!
UPDATE: I already have a functional solution with the same service working with $rootScope.$broadcast, however my aim is to get rid of it and not work with the $rootScope.
The problem is that you never actually change the value of vm.indexes - it always points to the same array. setIndexes only modifies properties of this array. That's why $watch, which by default checks for reference equality only, fails to spot the changes.
There are (at least) two ways of solving this: either make $watch check the object equality instead, by adding a third param there:
$scope.$watch('ShareData.getIndexes()', function(newVal) {
console.log('New indexes arrived', newVal);
}, true);
... or (better, in my opinion) rewrite the set function so that it'll create a new instance of indexes instead when there's a change:
vm.setIndexes = function(firstIndexParam, lastIndexParam, message) {
if (leaderIndexParam === undefined && partnerIndexParam === undefined) {
return;
}
vm.indexes = vm.indexes.slice();
Object.assign(vm.indexes, {
mainIndex: firstIndexParam,
secondaryIndex: lastIndexParam,
message: message
});
};
As a sidenote, simply calling setIndexes() does not trigger the digest - and $watch listener only checks its expression when digest is triggered.

Synchronous service using async service in Angular [duplicate]

This question already has answers here:
AngularJS : Initialize service with asynchronous data
(10 answers)
Closed 7 years ago.
I have a link generator service that is able to generate links to specific content types (users' details page, content items' details pages etc).
This service is really easy to use and has synchronous functions:
links.content(contentInstance); // /items/123
links.user(userInstance); // /users/234
I now have to introduce separate routing for logged in user to change from /users/id to /users/me.
The only change I'd need to add to my link generator service is to check whether userInstance.id == loggedInUser.id and return a different route URL. This is not a problem as long as my logged-in user's info would be synchronously available. but it's not...
I have a userService.getMyInfo() that returns a promise. The first time it's called it actually makes a server request but subsequent calls return a resolved promise with already cached data.
So how should I implement my user link URL generation in my link generator service?
Edit
Ok. So to see better what I have at the moment and where I'm having the problem. I'm pretty aware that async will stay async and that it can't be converted to synchronous (and it shouldn't be).
This is some more of my code, that will make it easier to understand.
linkGenerator.user
angular
.module("App.Globals")
.factory("linkGenerator", function(userService) {
...
user: function(userInstance) {
// I should be calling userService.getMyInfo() here
return "/users/{0}/{1}".format(userInstance.id, userInstance.name);
},
...
});
userService.getMyInfo
angular
.module("App.Globals")
.service("userService", function($q, cacheService, userResource) {
...
getMyInfo: function() {
if (cacheService.exists("USER_KEY"))
// return resolved promise
return $q.when(cacheService.get("USER_KEY"));
// get data
return userResource
.getCurrentUser()
.$promise
.then(function(userData) {
// cache it
cacheService.set("USER_KEY", userData);
});
},
...
});
Controller
angular
.module("App.Content")
.controller("ItemDetailsController", function(linkGenerator, ...) {
...
this.model = { ... };
this.helpers = {
...
links: linkGenerator,
...
};
...
});
View
View uses ItemDetailsController as context notation.
...
<a ng-href="{{::context.helpers.links(item.author)}}"
ng-bind="::item.author.name">
</a>
...
Notes
As you can see my view generates links to item authors. The problem is that my linkGenerator (as you can see from the code may not have the information yet whether it should generate one of the correct links to user details view.
I know I can't (and don't want to) change my async code to synchronous, but what would be the best way to make this thing work as expected?
One possible solution
For the time being I've come up with a solution that does the trick, but I don't really like it, as I have to supply my logged in user's ID to linkGenerator.user(userInstance, loggedInUserId) function. Then I set up my routing so that I add resolve to my route where I call userService.getMyInfo() which means that my controller is not being instantiated until all promises are resolved. Something along this line:
routeProvider
.when("...", {
templateUrl: "path/to/my/details/template",
controller: "ItemDetailsController".
controllerAs: "context",
resolve: {
myInfo: function(userService) {
return userService.getMyInfo();
}
}
})
...
Then I also add an additional helper to my controller
this.helpers = {
...
links: linkGenerator,
me: myInfo.id,
...
};
And then I also change link generator's function by adding the additional parameter that I then supply in the view.
linkGenerator.user = function(userInstance, loggedInUserId) {
if (userInstance.id === loggedInUserId)
return "users/me";
return "users/{0}/{1}".format(userInstance.id, userInstance.name);
}
and in the view
<a ng-href="{{::context.helpers.links.user(item.author, context.helpers.me)}}"...
And I don't to always supply logged in user's ID. I want my service to take care of this data on its own.
There is no way to make anything in JavaScript that is asynchronous at some point synchronous again. This is a ground rule of how concurrency works - no blocking for waiting for stuff is allowed.
Instead, you can make your new method return a promise and use the regular tools for waiting for it to resolve.
links.me = function(){
var info = userService.getMyInfo();
return info.then(info => { // or function(info){ if old browser
// generate link here
return `/users/${info.id}`; // or regular string concat if old browser
});
}
Which you'd have to use asynchronously as:
links.me().then(function(link){
// use link here
});

angularJS unit testing where run contains a HTTP request?

I am fairly new to AngularJS and am trying to learn some best practices. I have things working, but would like to start adding some unit tests to my modules and controllers. The first one I am looking to tackle is my AuthModule.
I have an AuthModule. This Module registers a Factory called "AuthModule" and exposes things like "setAuthenticatedUser" and also fields like "isLoggedIn" and "currentUser". I think this is a fairly common pattern in an AngularJS application, with some variations on the specific implementation details.
authModule.factory(
'AuthModule',
function(APIService, $rootScope) {
var _currentUser = null;
var _isLoggedIn = false;
return {
'setAuthenticatedUser' : function(currentUser) {
_currentUser = currentUser;
_isLoggedIn = currentUser == null ? false : true;
$rootScope.$broadcast('event:authenticatedUserChanged',
_currentUser);
if (_isLoggedIn == false) {
$rootScope.$broadcast('event:loginRequired')
}
$rootScope.authenticatedUser = _currentUser;
$rootScope.isLoggedIn = _isLoggedIn;
},
'isLoggedIn' : _isLoggedIn,
'currentUser' : _currentUser
}
});
The module does some other things like register a handler for the event "loginRequired" to send the person back to the home screen. These events are raised by the AuthModule factory.
authModule.run(function($rootScope, $log, $location) {
$rootScope.$on("event:loginRequired", function(event, data) {
$log.info("sending him home. Login is required");
$location.path("/");
});
});
Finally, the module has a run block which will use an API service I have to determine the current logged in user form the backend.
authModule.run(
function(APIService, $log, AuthModule) {
APIService.keepAlive().then(function(currentUser) {
AuthModule.setAuthenticatedUser(currentUser.user);
}, function(response) {
AuthModule.setAuthenticatedUser(null);
});
});
Here are some of my questions:
My question is how would you setup tests for this? I would think that I would need to Mock out the APIService? I'm having a hard time because I keep getting unexpected POST request to my /keepalive function (called within APIService.keepAlive())?
Is there any way to use $httpBackend in order to return the right response to the actual KeepAlive call? This would prevent me from having to mock-out the API service?
Should I pull the .run() block out which obtains the current logged in user out of the AuthModule and put it into the main application? It seems no matter where I put the run() block, I can't seem to initialize the $httpbackend before I load the module?
Should the AuthModule even be its own module at all? or should I just use the main application module and register the factory there?
Run blocks are the closest thing in Angular to the main method. A run block is the code which needs to run to kickstart the application. It is executed after all of the service have been configured and the injector has been created. Run blocks typically contain code which is hard to unit-test, and for this reason should be declared in isolated modules, so that they can be ignored in the unit-tests.angularjs docs
I suggest you take a look at this authentication service, using a service is the way to go.
Hopefully this would help ... Good luck

angularjs save changes after digest has finished

I think this might be quite common use-case with any angular app. I am simply watching some objects on my scope that are changed as part of several digest cycles. After digesting them (changing their values via databinding) has finished, I want to save them to databse.
A. Now, with the current solutions I see following problems:
running save in $timeout() - how to assure that save is called only
once
running a custom function in $scope.$evalAsync - how to find out what has been chaged
There are of course solutions to both of these prolblems, but non of those I know seem ehough elegant to me.
The question is: What is the most elegant solution to the problem?
B. In particular, what are the best practices to
make sure that save gets called only once in a digest cycle
find out that object is dirty after last digest
Here is a solution I've found working best for me - as an AMD modul. Inspired by Underscore.
/**
* Service function that helps to avoid multiple calls
* of a function (typically save()) during angular digest process.
* $apply will be called after original function returns;
*/
define(['app'], function (app) {
app.factory('debounce', ['$timeout', function ($timeout) {
return function(fn){ // debounce fn
var nthCall = 0;
return function(){ // intercepting fn
var that = this;
var argz = arguments;
nthCall++;
var later = (function(version){
return function(){
if (version === nthCall){
return fn.apply(that, argz);
}
};
})(nthCall);
return $timeout(later,0, true);
};
};
}]);
});
/*************************/
//Use it like this:
$scope.$watch('order', function(newOrder){
$scope.orderRules.apply(newOrder); // changing properties on order
}, true);
$scope.$watch('order.valid', function(newOrder){
$scope.save(newOrder); //will be called multiple times while digested by angular
});
$scope.save = debounce(function(order){
// POST your order here ...$http....
// debounce() will make sure save() will be called only once
});

Angularjs and Meteor "Session" reactivity, is there a way?

I'm trying to work with Meteor and Angularjs. I'm using Meteor_angularjs package, which works OK with Collections.
Now I'm trying to use Session and my reactive data store:
TestCtrl = [
"$scope",
function($scope){
$scope.value = Session.get('someValue');
}
]
This does not work.
QUESTION: Any suggestions on how to tie down Meteor's Session and Angular?
As far as I understand, I can write directive that will be polling Session every so ofter, however I don't think that's a good choice.
Thanks
UPDATE:
I've tried the following:
TestCtrl = [
"$scope",
function($scope){
Meteor.autorun(function(){
$scope.config = Session.get('testsConfig');
if (!$scope.$$phase){
$scope.$digest();
}
});
}
]
and it sort of works, however I get the following error:
Error: INVALID_STATE_ERR: DOM Exception 11
Error: An attempt was made to use an object that is not, or is no longer, usable.
at derez (http://localhost:3000/test:95:41)
at derez (http://localhost:3000/test:95:30)
at derez (http://localhost:3000/test:95:30)
at derez (http://localhost:3000/test:95:30)
at derez (http://localhost:3000/test:95:30)
at derez (http://localhost:3000/test:95:30)
at derez (http://localhost:3000/test:95:30)
at derez (http://localhost:3000/test:95:30)
at derez (http://localhost:3000/test:95:30)
at derez (http://localhost:3000/test:95:30) angular.js:5526
$get angular.js:5526
$get angular.js:4660
$get.Scope.$digest angular.js:7674
(anonymous function) controllers.js:46
Meteor.autorun.rerun deps-utils.js:78
_.extend.run deps.js:19
Meteor.autorun.rerun deps-utils.js:78
_.extend.flush deps.js:63
_.each._.forEach underscore.js:79
_.extend.flush deps.js:61
_.each._.forEach underscore.js:79
_.extend.flush deps.js:60
UPDATE 2:
I've tried the service like this (might be wrong usage), still nothing. Now it doesn't update at all on Session value's changes.
Meteor.autorun(function(){
app.factory('ssn', function(){ return{
get: function(val){
return Session.get(val);
}
}});
});
TestCtrl = [
"$scope","ssn",
function($scope, ssn){
$scope.config = ssn.get('testsConfig');
}
]
UPDATE 3: Angular has $apply() for
to execute an expression in angular from outside of the angular framework. (For example from browser DOM events, setTimeout, XHR or third party libraries)
At the same time Meteor has Meteor.render() for
Most of the time, though, you won't call these functions directly — you'll just use your favorite templating package, such as Handlebars or Jade. The render and renderList functions are intended for people that are implementing new templating systems.
However, it seems like I just cannot put 2 and 2 together. :(
this as an old question with old answers but I see people referring to it so here is the updated answer.
First - there is a new library for angular-meteor that handles those cases for you.
And this library gives you two possible solutions:
If you want to bind a Session variable to a scope variable, use the $meteorSession service.
What it does is that every time the scope variable will change, it will change to Session variable (and trigger an autorun if it's placed inside one).
and every time the Session variable will change, the scope variable will change as well (and change the view that it's placed upon).
If you are using the Session variable just to get a variable reactive (meaning trigger an autorun), you should use getReactively . this just returns the already existing scope variable but trigger an autorun every time it changes. a good example of this can be found it our tutorial.
Note: In anyway, when you use Tracker.autorun inside Angular, you need to connect it to a scope. this can be easily done if you replace Tracker.autorun with the $meteorUtils autorun function
Hi here is an option (might not be the best but it works I think)
app.service('Session',function($rootScope){
var self = this;
self.objects = {};
self.get = function(name){
self.objects[name] = {"value" : Session.get(name)};
Meteor.autorun(function() {
var i = Session.get(name);
if(self.objects[name].value != i){
if (!$rootScope.$$phase){
$rootScope.$apply(function(){
self.objects[name].value = i;
});
}
}
});
return self.objects[name];
}
self.set = function(name,value){
self.objects[name].value = value;
Session.set(name,value);
}
return self;
});
Call it in the $scope like this
$scope.test = Session.get("test");
In the view as {{test.value}}. Sorry for the late answer .
Happy new year!
try
angular.module('yourmod', [])
.controller('TestCtrl', ['$scope', function($scope) {
var c = Deps.autorun(function (comp) {
//... put reactive stuf on scope.....
if(!comp.firstRun) {
// only do not do aply at first run becaulse then apply is already running.
$scope.$apply()
}
});
// and to realy make it nice...
$scope.on('$destroy', function () {c.stop()});
}])

Resources