Can't think of a better title, sorry.
Please consider the following code -
//controller
function ProductCtrl($scope) {
getCategories(function (result) {
$scope.category = result.categories[0].name;
}); // asynchronouse method; getCategories calls uses XHR and returns the result as callback.
}
//view
{{category}}
When the view loads in the browser, getCategories gets called immediately. How do I make it load on demand, like onLoad on div or something so I can re-use this method somewhere else? Something like, $scope.getCategories returns the data I wanted, not on controller load. And how do I use this method in the view? e.g. <div onload=getCategories()></div> works?
Another question, the view does not print the category. How do I make $scope.category available outside of getCategories?
When the view loads in the browser, getCategories gets called immediately. How do I make it load on demand?
Wrap it in a function, and call that function when appropriate:
$scope.getCategories = function() {
getCategories(function (result) { ... }
}
so I can re-use this method somewhere else?
If multiple views need access to the result of getCatetories, you should create a service that stores the result. Your controllers can then inject that service to get access to it.
the view does not print the category.
Your AJAX is happening "outside" of Angular, so Angular doesn't know that a callback is being called and that $scope.category is being updated. Simply add $scope.$apply() after you update $scope.category in your callback and Angular will run a digest cycle and update your view.
Related
I have to test a void method which has a dependent method call that returns a promise and I can't mock that call as it is made on a local object created inside the tested method.
Is there a way to make jasmine expect calls to wait until the promise is resolved? I tried using $rootScope.$digest() but it is not ensuring that the dependent call's promise is resolved.
EDIT: Adding sample code
module.service('serviceToBeTested', ['$rootScope', 'someOtherService',
function($rootScope, someOtherService) {
var thirdPartyLib;
function fnToBeTested() {
//some validations and filtering on rootScope variable to build input for processing
thirdPartyLib = new ThirdPartyLib(); //this is not an angular service
var anotherFunction = function() {
//some hook functions that will be triggered by the third party library
}
// anotherFunction is set into thirdPartyLib so that hook functions will be triggered
thirdPartyLib.start().then(funtion() {
thirdPartyLib.someThing.load(); //this load will trigger one hook function
}
}
}]);
What I need to verify is that, upon invoking fnToBeTested(), a particular logic inside a hook function is executed (for that the control has to go inside the then part of thirdPartyLib.start()).
Actually this gets executed but only after the expect statements in the spec are executed.
And my spec file is almost like this:
it('should do this and this', function() {
// some initialization
serviceToBeTested.fnToBeTested();
$rootScope.$digest();
//expect statements
});
EDIT 2: Adding details on trial made as Andrew suggested below and adding clarity on how instance is created
ThirtPartyLib is instantiated inside main source as:
var theLib = require('theLib');
...............................
thirdPartyLib = new theLib.ThirdPartyLib();
And in spec, I created a var just like this and spied prototype as below:
var theLib = require('theLib');
................................
spyOn(theLib.ThirtPartyLib.prototype, 'start').and.callFake(.....);
But the fake function is not reached. When I check theLib.ThirtPartyLib.prototype in spec during debug, it lists the SpyStrategy while checking theLib.ThirtPartyLib.prototype in main source, it doesn't list that.
You should be able to test this with some clever use of mocking. In your beforeEach block, you can do something like this:
let promise; // declare this outside of your beforeEach so you have access to it in the specs
promise = $q.resolve(); // assign promise to
spyOn(ThirdPartyLib.prototype, 'start').and.returnValue(promise);
And then in your test, you now have access to the promise returned by start.
I have such working code:
Service (factory?):
myApp.factory('MyService', ['$q','$resource', 'MyResource',
function ($q, $resource, MyResource) {
return {
getRoles: function () {
return MyResource.getRoles(); //MyResource makes API calls using $resource
} } });
Controller:
$scope.Roles = MyService.getRoles();
$scope.Roles.$promise.then(function () { $scope.RolesCount = $scope.Roles.length; });
What I'd like to do is to create such function in MyService that will return number of roles once getRoles is resolved so I can use it in controller like this:
$scope.RolesCount = MyService.getRolesCount();
and then in html
{{RolesCount}}
Unfortunately, I have no idea how to do this since my getRolesCount() method needs to return something so I can't use $promise.then() inside MyService. I'll try to update my question's title once I come up with something more accurate.
If server response needs to be transformed in a service, then then should be moved there. However, it's not practical or possible if then is supposed to unwrap a promise and assign a value to scope, like in the code above.
$resource returns an object that is filled on promise resolution. The way a resource is supposed to be used in view is:
{{Roles.length}}
When an object is updated, it will be updated in view, too. Assigning the value to another variable is inefficient.
It's impossible to do something like
$scope.RolesCount = MyService.getRolesCount();
because getRolesCount is asynchronous, and the value it resolves with is scalar. It is possible to flatten it with `async..await, (TypeScript or ES2017), but will require additional measures to synchronize control flow with AngularJS digests, as explained in this answer.
I have a download function, which has a promise called onProgress(which returns download percentage), I want to go to some other view and come back. Even though controller's scope is with proper data the page is not getting refreshed with new data.
Even with this vague description it sounds like the onProgress() function is not in "angular world". Therefore, any changes to $scope that you make in the promise callback will not be noticed by angular until the next digest cycle.
The solution is easy: inside the onProgress() promise callback, wrap your changes to $scope in $apply:
onProgress().then(function(percent)
{
$scope.$apply(function() {
// your changes to $scope here, so that angular notices them
}
}
I'm currently playing with AngularJS. I'd like to return, from a service, a variable that will let the scope know when it has changed.
To illustrate this, have a look at the example from www.angularjs.org, "Wire up a backend". Roughly, we can see the following:
var projects = $firebase(new Firebase("http://projects.firebase.io"));
$scope.projects = projects;
After this, all updates made to the projects object (through updates, be it locally or remotely) will be automatically reflected on the view that the scope is bound to.
How can I achieve the same in my project? In my case, I want to return a "self-updating" variable from a service.
var inbox = inboxService.inboxForUser("fred");
$scope.inbox = inbox;
What mechanisms let the $scope know that it should update?
EDIT:
In response to the suggestions, I tried a basic example. My controller:
$scope.auto = {
value: 0
};
setInterval(function () {
$scope.auto.value += 1;
console.log($scope.auto.value);
}, 1000);
And, somewhere in my view:
<span>{{auto.value}}</span>
Still, it only displays 0. What am I doing wrong ?
UPDATE:
I made a demo plunker: http://plnkr.co/edit/dmu5ucEztpfFwsletrYW?p=preview
I use $timeout to fake updates.
The trick is to use plain javascript references:
You need to pass an object to the scope.
You mustn't override that object, just update or extend it.
If you do override it, you lose the "binding".
If you use $http it will trigger a digest for you.
So, whenever a change occurs, the scope variable reference to same object that gets updated in the service, and all the watchers will be notified with a digest.
AFAIK, That's how $firebase & Restangular work.
If you do multiple updates you need to have a way of resetting properties.
Since you hold a reference to an object across the application, you need to be aware of memory leaks.
For example:
Service:
app.factory('inboxService', function($http){
return {
inboxForUser: function(user){
var inbox = {};
$http.get('/api/user/' + user).then(function(response){
angular.extend(inbox, response.data);
})
return inbox;
}
};
});
Controller:
app.controller('ctrl', function(inboxService){
$scope.inbox = inboxService.inboxForUser("fred");
});
It depends on how the object is updating. If it gets updated "within" angular, a digest cycle will be triggered (See http://docs.angularjs.org/guide/scope), and the view will update automatically. That is the beauty of Angular.
If the object gets updated "outside" of angular (e.g. a jQuery plugin), then you can manually trigger a digest cycle by wrapping the code that's doing the updating in an $apply function. Something like this:
$scope.$apply(function() {
//my non angular code
});
See http://docs.angularjs.org/api/ng.$rootScope.Scope for more info.
I've been using directives in AngularJS which build a HTML element with data fetched from the $scope of the controller. I have my controller set a $scope.ready=true variable when it has fetched it's JSON data from the server. This way the directive won't have to build the page over and over each time data is fetched.
Here is the order of events that occur:
The controller page loads a route and fires the controller function.
The page scans the directives and this particular directive is fired.
The directive builds the element and evaluates its expressions and goes forward, but when the directive link function is fired, it waits for the controller to be "ready".
When ready, an inner function is fired which then continues building the partial.
This works, but the code is messy. My question is that is there an easier way to do this? Can I abstract my code so that it gets fired after my controller fires an event? Instead of having to make this onReady inner method.
Here's what it looks like (its works, but it's messy hard to test):
angular.module('App', []).directive('someDirective',function() {
return {
link : function($scope, element, attrs) {
var onReady = function() {
//now lets do the normal stuff
};
var readyKey = 'ready';
if($scope[readyKey] != true) {
$scope.$watch(readyKey, function() {
if($scope[readyKey] == true) {
onReady();
}
});
}
else {
onReady();
}
}
};
});
You could use $scope.$emit in your controller and $rootScope.on("bradcastEventName",...); in your directive. The good point is that directive is decoupled and you can pull it out from project any time. You can reuse same pattern for all directives and other "running" components of your app to respond to this event.
There are two issues that I have discovered:
Having any XHR requests fire in the background will not prevent the template from loading.
There is a difference between having the data be applied to the $scope variable and actually having that data be applied to the bindings of the page (when the $scope is digested). So if you set your data to the scope and then fire an event to inform the partial that the scope is ready then this won't ensure that the data binding for that partial is ready.
So to get around this, then the best solution is to:
Use this plugin to manage the event handling between the controller and any directives below:
https://github.com/yearofmoo/AngularJS-Scope.onReady
Do not put any data into your directive template HTML that you expect the JavaScript function to pickup and use. So if for example you have a link that looks like this:
<a data-user-id="{{ user_id }}" href="/path/to/:user_id/page">My Page</a>
Then the problem is that the directive will have to prepare the :user_id value from the data-user-id attribute, get the href value and replace the data. This means that the directive will have to continuously check the data-user-id attribute to see if it's there (by checking the attrs hash every few moments).
Instead, place a different scope variable directly into the URL
My Page
And then place this in your directive:
$scope.whenReady(function() {
$scope.directive_user_id = $scope.user_id;
});