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.
Related
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.
I have one service for handling data, and one for logic.
app.service('DataService', function() {
this.stuff = [false];
this.setStuff = function(s){
this.stuff = angular.copy(s);
}
});
The data service has a set function and a data property.
app.service('LogicService', function(DataService, $http) {
DataService.setStuff(["apple", "banana"]);
$http.get("./data.json").then(function(res){
DataService.setStuff(res.data.stuff);
});
});
I am assigning a property of the data service to the controller for binding to the DOM.
app.controller('MainCtrl', function($scope, DataService, LogicService ) {
$scope.message = "Hello, World!";
$scope.stuff = DataService.stuff;
//This is the only way I could get it to work, but isn't this JANKY?
//$scope.$watch(
// function(){
// return DataService.stuff
// },
// function(n,o){
// $scope.stuff = n;
// })
})
If I 'seed' the data service when the logic service is instantiated, and then later update it following an $http call, the DOM reflects the 'seeded' or initial value, but does not update.
Is there something fundamental I am missing in my understanding of the digest loop?
If I add a $watch function in my controller, all is well, but this seems yucky.
//FIXED//
#scott-schwalbe 's method of using Object.asign() works nicely, preserves my original structure, and is one line.
this.setStuff = function(s){
Object.assign(this.stuff, s);
}
Working Plunker
(sorry for titlegore)
If your data property is an object and is binded to the scope, then the scope will update whenever the object changes as long as you don't dereference it (eg data = x). Are you reassigning data object on the $http call?
An alternative to your current code to keep the reference using Object.assign
app.service('DataService', function() {
this.stuff = [false];
this.setStuff = function(s){
Object.assign(this.stuff, s);
}
});
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope, DataService) {
$scope.message = "Hello, World!";
//Get stuff data from your service, this way you stuff lives in your service
//And can be accessed everywhere in your app.
//It also makes your controller thin. Which is the top priority
$scope.stuff = DataService.getStuff();
//Or async
DataService.getStuffAsync()
.then(function(val){
$scope.asycStuff = val;
});
this.clickFromAButton = function(){
DataService.setStuff(["apple", "banana"]);
};
});
app.service('DataService', function() {
this.stuff = [false];
this.asyncStuff;
this.setStuff = function(s){
this.stuff = angular.copy(s);
};
this.getStuff = function(){
return this.stuff;
};
this.getStuffAsync = function(){
//If i already fetched the data from $http, get it from the service.
return this.asyncStuff || $http.get("./data.json").then(function(res){
//When i fetch it for the first time I set the data in my service
this.asyncStuff = res.data;
//and I return the data
return res.data;
});
};
});
This is a good 'pattern' to follow ;)
Instead of putting "stuff" on scope. Put your DataService object on scope.
app.controller('MainCtrl', function($scope, DataService, LogicService ) {
$scope.message = "Hello, World!";
$scope.DataService = DataService;
//$scope.stuff = DataService.stuff;
HTML
<body ng-controller="MainCtrl">
{{DataService.stuff}}
</body>
The $interpolate service will automatically places a $watch on DataService.stuff. Thus there is no need to do it inside your controller.
The DEMO on PLNKR.
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
This is how I have my JS set up:
Basically I have a page, and on that page there is a chart. I want to have a loading spinner show while the chart data is loading.
angular.module('myApp', [])
.service('chartService', ['$http', function($http) {
var svc = {};
svc.updateChartData = function($scope) {
$scope.loading = true;
$http({method: 'GET', url: 'http://example.com/getjson'})
.success(function(response) {
var data = google.visualization.arrayToDataTable(JSON.parse(response));
var options = {
...
};
var chart = new google.visualization.ComboChart(document.getElementById('chart_div'));
chart.draw(data, options);
$scope.loading = false;
});
}
return svc;
}])
.controller('PageController', ['$scope', '$http', 'chartService', function($scope, $http, chartService) {
$scope.loading = true;
// When select option changes
$scope.updateData = function() {
chartService.updateChartData($scope);
};
}])
.controller('ChartController', ['$scope', '$http', 'chartService', function($scope, $http, chartService) {
// On load
chartService.updateChartData($scope);
}]);
I am using ng-hide="loading" and `ng-show="loading" to make sure the spinner and the chart show at the correct times.
However, I've noticed that the call below // On load - doesn't actually turn the loading to false. Another message on SO suggested there is a better way to achieve this than by passing $scope around so any suggestions would be appreciated. Thank you.
It is not a good practice to pass your scope object to a service, a service is meant to be stateless. Instead utilize the callbacks of the $http:
chartService.updateChartData().finally(function(){
$scope.loading = false;
});
And, as Grundy mentioned below, return your $http from your service to enable callbacks:
svc.updateChartData = function($scope) {
return $http({ //.. the options });
}
I see some more bad practices though. You shouldn't add the data to your DOM from your service, instead utilize also for this the callbacks:
svc.updateChartData = function($scope) {
return $http({method: 'GET', url: 'http://example.com/getjson'});
}
controller:
// When select option changes
$scope.updateData = function() {
chartService.updateChartData().then(function(data) {
// success
// do something with the return data from the http call
}, function (error) {
// error
// handle error
}).finally (function() {
// always
$scope.loading = false;
});
};
For your google chart it would make most sense to create a directive.
first,you have two controllers,I'm assuming they are nested relations.
PageController include ChartController.
you want to change the value of the parent controller in the child controller.
You must use a reference type rather than a value type.
$scope.loading =true;
change to
$scope.loading ={status:true};
and if you want to set false,Should be
$scope.loading.status =false;
NOT
$scope.loading ={status:false};
second, you can pass a callback function to service.
like this
svc.updateChartData = function(callback) {
....
.success(){
callback();
}
}
controller code change to
.controller('ChartController', ['$scope', '$http', 'chartService',
function($scope, $http, chartService) {
// On load
chartService.updateChartData(function(){
$scope.loading =true;
});
}]);
I'm trying to understand how to unit test my directive in my situation below.
Basically I'm trying to unit test a directive which has a controller. On the loading of this directive the controller makes a http request by a service which brings some data to the controller again then provides this data to the directive view.
On the scenario below in my understanding I should do:
A $httpBackend to avoid an exception when the http request is done;
Populate the fake data to be able to unit test the directive with diff behaviors
Compile the directive
What I've been trying so far, as you can see, is override the Service with the fake data. What I could not make work so far.
Some doubts come up now.
As you can see in my Controller. I'm providing the whole Service to the view:
$scope.ItemsDataservice = ItemsDataservice;
What makes me believe that my approach to override the Service should work.
My question:
On scenario below I understand that I could override the Service to manipulate the data or even override the controller to manipulate the data by scope.
What's the right thing to do here?
Am I understand wrong?
Am I mixing the unit tests?
In my current unit test code, when I'm applying the fake data(or not), is not make any difference:
ItemsDataservice.items = DATARESULT;
ItemsDataservice.items = null;
Controller:
angular.module('app')
.controller('ItemsCtrl', function ($scope, $log, ItemsDataservice) {
$scope.ItemsDataservice = ItemsDataservice;
$scope.ItemsDataservice.items = null;
$scope.loadItems = function() {
var items = [];
ItemsDataservice.getItems().then(function(resp) {
if (resp.success != 'false') {
for (resp.something ... ) {
items.push({ ... });
};
ItemsDataservice.items = items;
};
}, function(e) {
$log.error('Error', e);
});
};
$scope.loadItems();
});
Service:
angular.module('app')
.service('ItemsDataservice', function ItemsDataservice($q, $http) {
ItemsDataservice.getItems = function() {
var d = $q.defer();
var deffered = $q.defer();
var url = 'http://some-url?someparameters=xxx'
$http.get(url)
.success(function (d) {
deffered.resolve(d);
});
return deffered.promise;
};
return ItemsDataservice;
});
Directive:
angular.module('app')
.directive('items', function () {
return {
templateUrl: '/items.html',
restrict: 'A',
replace: true,
controller: 'ItemsCtrl'
};
});
Unit testing directive:
ddescribe('Directive: Items', function () {
var element, scope, _ItemsDataservice_, requestHandler, httpBackend;
var URL = 'http://some-url?someparameters=xxx';
var DATARESULT = [{ ... }];
// load the directive's module
beforeEach(module('app'));
beforeEach(module('Templates')); // setup in karma to get template from .html
beforeEach(inject(function ($rootScope, ItemsDataservice) {
httpBackend = $httpBackend;
scope = $rootScope.$new();
_ItemsDataservice_ = ItemsDataservice;
requestHandler = httpBackend.when('GET', URL).respond(200, 'ok');
}));
afterEach(function() {
//httpBackend.verifyNoOutstandingExpectation();
//httpBackend.verifyNoOutstandingRequest();
});
it('Show "No Items available" when empty result', inject(function ($compile) {
_ItemsDataservice_.items = null;
element = angular.element('<div data-items></div>');
element = $compile(element)(scope);
scope.$digest();
element = $(element);
expect(element.find('.msg_noresult').length).toBe(1);
}));
it('Should not show "No Items available" when data available ', inject(function ($compile) {
_ItemsDataservice_.items = DATARESULT;
element = angular.element('<div data-items></div>');
element = $compile(element)(scope);
scope.$digest();
element = $(element);
expect(element.find('.msg_noresult').length).toBe(0);
}));
});
I sorted out the problem.
Changed this line:
element = $compile(element)(scope);
To this line:
element = $compile(element.contents())(scope);
The only diff is the jquery method .contents()
I did not get yet why. But it solved.
Update:
Another thing I've just discovered and that was really useful for me.
You can use regular expression on you httpBackend:
httpBackend.whenGET(/.*NameOfThePageXXX\.aspx.*/).respond(200, 'ok');
So, you don't need to worry to use exactly the same parameters etc if you just want to avoid an exception.