I am hoping someone can help me understand an annoying problem I am having with $scope in AngularJS. Please see the comments in my code below:
app.controller('MyController', function ($scope, $routeParams, $http, $timeout) {
$scope.id = $routeParams.id;
$http.get("http://server/api/Blah/GetData/" + $scope.id).success(function (data) {
$scope.data = data;
alert($scope.data.MyObject.Property); //displays the expected value. - Not Undefined or null
}).error(function (data) {
alert(data);
});
$scope.$on('$viewContentLoaded', function () {
$timeout(function () {
var d = document.getElementById("iframe");
d.contentDocument.documentElement.innerHTML = $scope.data.MyObject.Property; //Now MyObject is magically undefined.
}, 0);
});
});
The call to the WEB API returns a valid object which is assigned to $scope.data. I display an alert to make sure that $scope.data.MyObject.Property exists, which it does. The expected value is displayed.
Now when I try accessing $scope.data.MyObject.Property in the $viewContentLoaded code, the $scope.data.MyObject is no longer in the $scope. The console reports the following:
HTML1300: Navigation occurred.
File: route.html
TypeError: Unable to get property 'MyObject' of undefined or null reference
at Anonymous function (http://server/script/route.js:43:13)
at Anonymous function (https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js:158:234)
at e (https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js:45:348)
at Anonymous function (https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js:48:275)
Why is $scope dropping the value of $scope.data.MyObject? What makes this problem even more frustrating is if I put an alert(""); in the $viewContentLoaded code, the $scope.data.MyObject value is no longer undefined. What is going on here?
You need to know the timing of how your code get executed.
This is fixed code with some logging:
app.controller('MyController', function ($scope, $routeParams, $http, $timeout) {
$scope.id = $routeParams.id;
console.log(1);
var promise = $http.get("http://server/api/Blah/GetData/" + $scope.id).success(function (data) {
$scope.data = data;
console.log(2);
alert($scope.data.MyObject.Property); //displays the expected value. - Not Undefined or null
}).error(function (data) {
alert(data);
});
$scope.$on('$viewContentLoaded', function () {
$timeout(function () {
var d = document.getElementById("iframe");
console.log(3);
// d.contentDocument.documentElement.innerHTML = $scope.data.MyObject.Property;
promise.then(function () {
console.log(4);
d.contentDocument.documentElement.innerHTML = $scope.data.MyObject.Property;
});
}, 0);
});
});
You may expect the result logs is 1234, but actually it can be 1324. In later case, the code in $viewContentLoaded is executed before the $http.get success. So it $scope.data is still null.
The solution is using Promise (or $q in angular world). So that you can wait for the result of $http.get. You have guarantee that 4 is always executed after 2 (assuming it succeeded).
Well, this behavior is because JavaScript code is get executed async. so better to include that code once promise is resolved.
$http.get("http://server/api/Blah/GetData/" + $scope.id).success(function (data) {
$scope.data = data;
alert($scope.data.MyObject.Property); //displays the expected value. - Not Undefined or null
$scope.$on('$viewContentLoaded', function () {
$timeout(function () {
var d = document.getElementById("iframe");
d.contentDocument.documentElement.innerHTML = $scope.data.MyObject.Property; //Now MyObject is magically undefined.
}, 0);
}).error(function (data) {
alert(data);
});
});
This will work :)
Cheers!
The $http request is ansynchronous. It may not complete before your $viewContentLoaded event is fired. ( I guess this event fires after DOM is loaded and does not wait for http requests to complete, I may be wrong).
Why not do something like this:
app.controller('MyController', function ($scope, $routeParams, $http, $timeout) {
$scope.id = $routeParams.id;
$http.get("http://server/api/Blah/GetData/" + $scope.id).success(function (data) {
$scope.data = data;
alert($scope.data.MyObject.Property); //displays the expected value. - Not Undefined or null
$timeout(function () {
var d = document.getElementById("iframe");
d.contentDocument.documentElement.innerHTML = $scope.data.MyObject.Property; //Now MyObject is magically undefined.
}, 0);
}).error(function (data) {
alert(data);
});
$scope.$on('$viewContentLoaded', function () {
});
Since http get is async function. you have to use promises to wait until http get fetches the result.
you can do this by following code.
make a service.
app.factory('myService', function($http) {
var getData = function(id) {
// Angular $http() and then() both return promises themselves
return $http({method:"GET", url:"http://server/api/Blah/GetData/" + id}).then(function(result){
// What we return here is the data that will be accessible
// to us after the promise resolves
return result.data; //or may be return result only.
});
};
return { getData: getData };
});
in your controller
app.controller('MyController', function ($scope, $routeParams, $http, $timeout,myService) {
$scope.id = $routeParams.id;
var Data=myService.getData($scope.id);
Data.then(function(result){
$scope.data.MyObject.Property=result;//or result.data may be
$scope.$on('$viewContentLoaded', function () {
$timeout(function () {
var d = document.getElementById("iframe");
d.contentDocument.documentElement.innerHTML = $scope.data.MyObject.Property; //Now MyObject is magically undefined.
}, 0);
});
});
To begin with, the declaration of the controller is missing elements. It should be:
app.controller('MyController', ["$scope" , "$routeParams" , "$http" , function ($scope, $routeParams, $http, $timeout) {...
Check Dependency Injection in Angular's docs.
Try this and, if still not working, update your question with the new code and some loggings.
Related
I am doing unit testing using Karma and Jasmine. I have app.js as main source file:
app.service("someServ", function(){
this.sendMsg = function(name){
return "Hello " + name;
}
})
app.factory("appFactory", function ($q, someServ) {
function getData() {
var defer = $q.defer();
defer.resolve("Success message");
return defer.promise;
}
function foo(){
var text = someServ.sendMsg("Message");
alert(text);
}
return {
getData : getData,
foo : foo
}
})
app.controller("mainController",['$scope','$http','appFactory',function($scope, $http, appFactory){
var mct = this;
mct.printData = function(){
var myPromise = appFactory.getData();
myPromise
.then(function(data){
alert("Promise returned successfully. Data : " + data);
}, function(error){
alert("Something went wrong.... Error: " + error);
})
}
mct.showMsg = function(){
appFactory.foo();
}
}]);
My testFile.js is as follows:
beforeEach(module(function($provide){
$provide.service("someServ", function(){
this.sendMsg = jasmine.createSpy('sendMsg').and.callFake(function(param){})
});
$provide.factory("appFactory", function(someServ, $q){
function getData(){
var defer = $q.defer();
defer.resolve("Success message");
return defer.promise;
}
function foo(){
var facParam = "some text";
someServ.sendMsg(facParam);
}
return {
getData : getData,
foo : foo
}
});
}));
var $scope, mainController, appFactoryMock, someServMock;
beforeEach(inject(function($rootScope, $controller, $http, $q, appFactory, someServ){
appFactoryMock = appFactory;
someServMock = someServ;
$scope = $rootScope.$new();
mainController = $controller("mainController", {
$scope : $scope,
$http : $http,
appFactory : appFactoryMock
});
}));
it('that mainController is calling appFactory methods', function(){
spyOn(appFactoryMock, "getData");
mainController.printData();
scope.$root.$digest();
expect(appFactoryMock.getData).toHaveBeenCalled();
})
it('that appFactory method foo calls someServ sendMsg', function(){
spyOn(appFactoryMock, "foo");
appFactoryMock.foo();
expect(someServMock.sendMsg).toHaveBeenCalled();
});
Both the above tests are failing. For first one, error is: Cannot read property of undefined and for second one: expected spy sendMsg to have been called. First error occurs at: app.js file as shown in call stack. I have also debugged my tests using Debug option in karma chrome window. The printData() function is calling actual code in app.js but I have already mocked it.
Please anyone explain me why is it happening so and how to solve this issue? Why original code is being called and how can I make both these tests to pass.
Jasmine's spy only checks if the function has been called, without firing the actual implementation. That's why getData().then throws an error.
As you can read on Jasmine's documentation you need to add .and.callThrough() to go through the original function.
I believe the first issue may be related to a syntax error you have in your code - in your first it block, what does the scope variable refer to?
I have the following controller:
myApp.controller('myCtrl', ['$scope', '$rootScope', '$location', 'myService',
function($scope, $rootScope, $location, myService) {
$scope.myArray = [];
$scope.myFunction = function() {
if (something) {
setTimeout(function(){
$scope.myFunction();
},500);
} else {
var itm = $rootScope.var;
for (var i in itm) {
if (itm.hasOwnProperty(i)) {
$scope.myArray.push(itm[i].value);
}
}
// first console.log
console.log($scope.myArray);
}
}
$scope.myFunction();
// second console.log
console.log($scope.myArray);
}
]);
In the example above the second console.log gets printed before the first one. Why is this the case? Is there a way to make the controller wait for the function to be executed/returned and only after that proceed to execute the rest of the code?
Without seeing how everything is being implemented. This is the best I can help you with. If you want a controller to do something only if a promise is successful you can wrap your code around the request. In the plunkr I have written a sample $http service that has a fake request to myFunction that uses $q.
I would suggest using a factory to share data between controller instead of $rootScope. $rootScope is hard to manage throughout big SPA's. The Plunkr has commented options you can mess with to change between $rootScope and using a Factory.
Service below
app.service('Service', Service);
function Service($q, $rootScope, Factory) {
var deferred = $q.defer();
this.myFunction = function(){
//Using factory to persit data instead of $rootScope
//var itm = Factory.myArray;
var itm = $rootScope.var;
var array = [];
//Item isnt set return error
if(itm === undefined || itm === null) deferred.reject("$rootScope.var is not set")
//Changed this a bit didnt know what $rootScope.var actually was
for (var i in itm) {
array.push(itm[i]);
}
deferred.resolve(array);
return deferred.promise;
}
return this;
}
The first thing the controller does is initializes a request to Service.myFunction() and waits for a success or error callback. After the success you can process and do anything you'd like with the data returned from the promise. If there is an error you can handle it as you see fit.
app.controller('controller', controller);
function controller(Service, $rootScope) {
/* jshint validthis: true */
var vm = this;
vm.myArray = [];
vm.request = "";
//Un-Comment this to return success or error
$rootScope.var = [1,2,3,4,5,6];
//This is a fake http request
Service.myFunction().then(
//if the promise was resolved or $http was a success
//initilize the controller
function(data) {
vm.myArray = (data)
},
//if the promise was resolved or $http was a success
//initilize the controller
function(err) {
vm.request = (err)
})
}
Plunkr
Being new to Angular, I am not able to figure out how to load all the data required in the controller before it starts to compile the view.
I have created a factory to load JSON from server.
app.factory('myData', function ($http) {
return {
getMetaData : function () {
return $http.get('get-metadata').then(function (result) {
return result.data;
});
}
}
});
and a controller which uses that factory
app.controller('MyController', function ($scope, $http, myData) {
$scope.meta_data = {};
myData.getMetaData().then(function (data) {
$scope.meta_data = data.metadata;
});
});
I am also using a $watch in my controller like below
$scope.$watch("my_var.x", function (x, old_x) {
if (x) {
var y = $scope.meta_data.mapping[x] || [];
$scope.meta_data.y = y;
}
});
My problem is, $watch gets called before the myData.getMetaData returns, and $scope.meta_data.mapping isn't available. Due to that an error is thrown.
Any hint in the right direction would suffice.
Also, am I doing it correctly? I mean is this the case where I should be loading all data outside the controller and bootstrap my app manually using angular.bootstrap(document.getElementById('myApp'), ['myApp']);?
If you need to wait until your data is fetched before you start your $watch, just declare it in the resolved promise callback function:
app.controller('MyController', function($scope, $http, myData) {
$scope.meta_data = {};
myData.getMetaData().then(function(data) {
$scope.meta_data = data.metadata;
$scope.$watch("my_var.x", function(x, old_x) {
if (x) {
var y = $scope.meta_data.mapping[x] || [];
$scope.meta_data.y = y;
}
});
});
});
Otherwise, it might be a good practice as #apairet said to read about using resolves with routing.
I've read on several places that $q is gracefully integrated in scope in Angular JS.
Suppose that you have this:
var superService = function() {
var deferred = $q.defer();
deferred.resolve(['foo', 'bar']);
return deferred.promise;
};
Of course, $q is useless here but if I use $timeout or run a $http call, the result is the same.
If I do this:
superService().then(function(data) {
$scope.result = data;
});
It's ok. But if I do that:
$scope.result = superService();
It's also supposed to be ok. But in my case, $scope.result contains 3 elements (they are visible in my template with a ng-repeat): "then", "catch" and "finally" functions, I guess... instead of ['foo', 'bar'] of course.
My concrete example:
angular.module('myModule', [])
.factory('HelloWorld', function($q, $timeout) {
var getMessages = function() {
var deferred = $q.defer();
deferred.resolve(['Hello', 'world']);
return deferred.promise;
};
return {
getMessages: getMessages
};
})
.controller('HelloCtrl', function($scope, HelloWorld) {
$scope.messages = HelloWorld.getMessages();
//HelloWorld.getMessages().then(function(data) {
// $scope.messages = data;
//});
});
Any idea here?
Automatic promise unwrapping has been deprecated and will soon be removed. See: https://github.com/angular/angular.js/commit/5dc35b527b3c99f6544b8cb52e93c6510d3ac577
I have the following directive:
MyApp.directive('myFilter', ['$filter','$rootScope',
function($filter, $rootScope)
{
var dir = {};
dir.restrict = 'E';
dir.templateUrl = 'views/myFilter.html';
dir.replace = true;
dir.scope =
{
name: '#',
model: '=',
};
dir.link = function(scope,el,attrs)
{
//stuff here
}
return dir;
}]);
Here's how I invoke it:
<my-filter model="someField" name="abcd" />
When the directive is first initalized, the someField is empty. Later on, it is retrieved via ajax, and its value is filled in.
Question is, how can I watch for the value of someField to be updated? When I do this from the link method:
scope.$watch(scope.model, function()
{
console.log( "changed, new val: ", scope.model );
}
This is only called once, when initalizing the directive, and the value then is empty. When the value is retrieved via ajax (from $http.get), this watch function is not called again. However, in other parts of the page where I'm displaying {{someField}}, that value DOES update when ajax request is fetched. So I don't think the problem has to do with doing $scope.apply() after ajax request.
Edit: someField is assigned in the controller. Here is the code:
MyApp.controller('myCtrl',
['$scope', '$rootScope', '$routeParams',
'ajax', '$filter',
function($scope, $rootScope, $routeParams, ajax, $filter)
{
var userId = parseInt( $routeParams.userId );
$scope.loaded = false;
$scope.someField = ""; //initalize with empty value
var load = function(data)
{
$scope.loaded = true;
$scope.someField = data.someField;
};
ajax.getUser(userId).success(load);
}
]);
The method ajax.getUser() does a $http.get() and returns its promise. In the code above, the load method is called which sets the value of someField.
$watch expects an expression that can be either a function or a string. So, it'll work if you change it to
scope.$watch('model', function() { ... });
or
scope.$watch(function() { return scope.model; }, function() { ... });
Check out this jsFiddle.