$scope variable is undefined when it is set inside a function - angularjs

I have the following example code in my learning app. The service does his job and pulls some data out of a page with json code generated by php, so far so good.
service:
(function() {
'use strict';
angular
.module('app.data')
.service('DashboardService', DashboardService);
DashboardService.$inject = ['$http'];
function DashboardService($http) {
this.getFormules = getFormules;
////////////////
function getFormules(onReady, onError) {
var formJson = 'server/php/get-formules.php',
formURL = formJson + '?v=' + (new Date().getTime()); // Disables cash
onError = onError || function() { alert('Failure loading menu'); };
$http
.get(formURL)
.then(onReady, onError);
}
}
})();
Then i call the getFormules function in my controller and put all the data inside my $scope.formuleItems and test if everything succeeded and 'o no'... $scope.formuleItems = undefined! - Strange because my view is showing data?
part of the controller:
dataLoader.getFormules(function (items){
$scope.formuleItems = items.data;
});
console.log('+++++++++++++++++', $scope.formuleItems); // gives undefined
The first thing i did was search around on stackoverflow to look if someone else had the same issue, and there was: Undefined variable inside controller function.
I know there are some walkarounds for this, i've done my own research, but something tells me that this (see example below) isn't the best way to solve this problem.
solution one: put $watch inside of the controller
$scope.$watch('formuleItems', function(checkValue) {
if (checkValue !== undefined) {
//put the code in here!
}
}
or even:
if($scope.formuleItems != null) {}
The rest of the controller is relying on $scope.formuleItems. Do i really have to put everything into that $watch or if? Can i fix this with a promise? I never did that before so some help would be appreciated.

The code in your callback
function (items){
$scope.formuleItems = items.data;
}
is evaluated asynchronously. That means you first fire the request, then javascript keeps on executing your lines of code, hence performs
console.log('+++++++++++++++++', $scope.formuleItems); // gives undefined
At this point the callback was not invoked yet, because this takes some time and can happen at any point. The execution is not stopped for this.
Therefore the value of $scope.formuleItems is still undefined, of course.
After that - at some not defined time in the future (probably a few milliseconds later) the callback will be invoked and the value of $scope.formuleItems will be changed. You have to log the value INSIDE of your callback-function.
You urgently have to understand this concept if you want to succeed in JavaScript, because this happens over and over again :)

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.

Unit Test Multiple HTTP Requests

So in my factory I have a loop which requests HTTP calls and adds them to a promise array.
I then do a $q.all on the result to build a model.
When I come to test this however I can't get HTTP to make all the calls, it only makes the last one, I need it to make all the calls and build the model.
Below is very cut down code, ( I use 7 dates, but wanted to keep things short)
Factory Code
function getLatestData(){
var dateArray= ['2017-09-21','2017-09-22']
for (i = 0; i < 2; i++) {
var url = 'data-server/date/[i]'
promises.push(getData(url)); // getData is a simple $http function call.
}
return $q.all(promises).then(function(response){
buildModel(reponse);
});
}
So when I come to test this, I've got something like (I did try a loop but that failed).
httpBackend.expectGET('data-server/date/2017-09-21' ).respond(mockData[0]);
httpBackend.expectGET('data-server/date/2017-09-22' ).respond(mockData[1]);
rootScope.$apply();
modelFactory.getLatestData().then(function(response){
expect(response).toEqual(mockModelData);
})
So I console.log the get URL and I see all the URL requests are the same, they don't seem to be updating which results in this error
Error: Unexpected request: GET 'data-server/date/2017-09-22'
Expected GET 'data-server/date/2017-09-21'
because it's always the last httpBackend.expectGET that's taken.
What am I missing?
My problem was mocking.
I left this out of my example because I thought it wasn't relevant and added complication, but to build the dates I used the momentJS Library.
so
var url = 'data-server/date/[i]'
is
var url = 'data-server/date/'+factory.getMoment().add(i,'d').format('YYYY-MM-DD');
The factory.getMoment is just a wrapper for moment, the idea being I could over ride this in the unit tests to provide me with a 'given' date object.
funciton getMoment(){
return moment();
}
Anyway in my tests, I had this
var mockDate = moment('2017-09-21');
spyOn(factory, 'getMoment').and.returnValue(moment('2017-09-21'));
httpBackend.expectGET('data-server/date/'+mockDate.format('YYYY-MM-DD').respond(mockData[0]);
httpBackend.expectGET('data-server/date/'+mockDate.add(1,'d').format('YYYY-MM-DD').respond(mockData[1]);
Thinking that each time this would be called, it would give me back this mock, guess I was wrong about that.
What I needed to do, as pointed out by a colleague, was to use jasmine's clock mock.
beforeEach(function () {
jasmine.clock().install();
})
afterEach(function () {
jasmine.clock().uninstall();
})
Then in my test I set up the date time with
mockDate = moment('2017-09-21'); // always use moment as JS date if badly broken and just can't be trusted!
jasmine.clock().mockDate(mockDate.toDate());
httpBackend.expectGET('data-server/date/'+moment().add(0, 'days').format("YYYY-MM-DD").respond(mockData[0]);;
httpBackend.expectGET('data-server/date/'+moment().add(1, 'days').format("YYYY-MM-DD").respond(mockData[1]);;
(the above is in a loop)
I've removed my spy and now I have the dates and requests working as I expected.
Hope this helps someone else who finds themselves scratching their head for days trying to figure out why their tests are not working!

Angular: Get aware of spelling mistakes in function call with ng-click

My question is about discovering possible spelling mistakes in angular expressions, in particular spelling mistakes in the function name.
Consider the snippet bellow:
I have two buttons there, the first one with correct spelling, the second with a spelling mistake in the angular expression. Clicking the second button does nothing and gives no hints about a potential error.
My question is now: are there ways to detect erroneous calls to function that don't exist (while executing the application)?
I am not looking for some checking possibility in the build or unit test process but rather would like to see a way I could get aware of such a potential issue when running the erroneous expression in the browser when the application is executed.
angular.module("myApp", [])
.controller("TestController", function($scope){
$scope.myFunction = function() {
console.log("Hello World");
};
});
angular.element(document).ready(function () {
angular.bootstrap(document, ['myApp']);
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.6/angular.min.js"></script>
<section ng-controller="TestController">
<button ng-click="myFunction()">myFunction</button>
<button ng-click="myFunctio()">myFunctio</button>
</section>
I'm not familiar with a built in option in angular to do that (using binding to an "undefined" object is a legit UC as things may become "undefined" during program run) - but you may write your own "ng-click" directive which, in case not finding the function to bound to, raise an error (exception or better - console error / warning).
This is an extremely common complaint about Angular. Even when writing code for the Closure compiler, with all the type annotations and everything, these still fall right through the cracks.
You can kluge something together, I've seen things like bussing all events to a common broker and looking for the target handler in the bound scope, and so on. But it always appears to be more trouble than it's worth.
Your unit tests are where you catch this sort of thing. It's why being able to test template code via triggering events is such an important thing for an Angular developer to master. If you trigger that button click and your test fails (e.g. your spyOn the handler never gets called), check the template.
Protractor (and other end to end testing frameworks) will do that for you.
I'm not sure if this would work for function calls or not, but it would solve part of the problem of misspelling something. In Scott Allen's AngularJS Playbook course on Pluralsight, he suggests creating a decorator for the $interpolate service to see if any bindings are potentially incorrect. Here is the code for that:
(function(module) {
module.config(function ($provide) {
$provide.decorator("$interpolate", function ($delegate, $log) {
var serviceWrapper = function () {
var bindingFn = $delegate.apply(this, arguments);
if (angular.isFunction(bindingFn) && arguments[0]) {
return bindingWrapper(bindingFn, arguments[0].trim());
}
return bindingFn;
};
var bindingWrapper = function (bindingFn, bindingExpression) {
return function () {
var result = bindingFn.apply(this, arguments);
var trimmedResult = result.trim();
var log = trimmedResult ? $log.info : $log.warn;
log.call($log, bindingExpression + " = " + trimmedResult);
return result;
};
};
angular.extend(serviceWrapper, $delegate);
return serviceWrapper;
});
});
}(angular.module("common")));

AngularJS retrieve data outside request

I have a problem accessing data outside my service request. See my code below. The variable works within the service request. But when i want to acces the variable outside the request, i'm getting a undefined variable.
Does anyone know how to fix this?
API.getUser($scope.email, $scope.password).then(function(data) {
$scope.user_id = (data.id);
console.log($scope.user_id) // this works
});
console.log($scope.user_id); // <--- Here i'm getting undefined.
Use $apply() on scope in the callback function.
API.getUser($scope.email, $scope.password).then(function(data) {
$scope.user_id = (data.id);
$scope.$apply(); // <----- Here
console.log($scope.user_id)
});
The call to API.getUser is an asynchronous call and the code below that executes before the callback executes. That is why $scope.user_id is undefined. You can do anything you want to do with the variable inside the success callback and pass it to functions if you need to work more with this user_id.
Well, the problem is with the Javascript behavior for asyncs operations and the use of Promises ($q), when the interpreter run the code, does something like this:
1) Make this ajax request (An async operation), and return a Promise
API.getUser($scope.email, $scope.password)
2) Register the function in the Promise, to be executed when the operation ends
.then(function(data) {
$scope.user_id = (data.id);
console.log($scope.user_id) // this works
});
3) Print the current value of $scope.user_id
console.log($scope.user_id);
Print undefined because in this moment the Async operation are not finished
4) On some time the Async operation finish and execute this code
$scope.user_id = (data.id);
console.log($scope.user_id) // this works
In the last part the $scope.user_id was set, and the console.log print the correct value.

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

Resources