I'm following this angular tutorial and am confused by the $on("loaded", ...) calls.
$scope.user.$on("loaded", function() {
populatePosts();
populatecomments();
});
...
function populatecomments() {
$scope.comments = {};
angular.forEach($scope.user.comments, function(comment) {
var post = Post.find(comment.postId);
post.$on("loaded", function() {
$scope.comments[comment.id] = post.$child("comments").$child(comment.id);
$scope.commentedPosts[comment.postId] = post;
});
});
}
Why can't we just call populatePosts() and populatComments() here? Why is a second $on("loaded", ...) needed within populatecomments() if it isn't called until loaded?
The first:
$scope.user.$on("loaded",...
will be triggered when $scope.user is loaded, the second:
post.$on("loaded",...
will be triggered for each post when the given one is loaded.
Angular(and mostly javascript) does network calls asynchronously, so if you want to postprocess their result, you have to define a callback which will be called when they are loaded.
As you see populatecomments() uses $scope.user.comments which could be inaccessible if you call populatecomments() directly, outside of the loaded event handler.
In this case, it's not. You could (and should) refactor this code to use promises instead.
Related
I have a service making two consecutive calls to an API asynchronously.
I would like the app to wait for both to be resolved before proceeding and since one of calls may or may not be made, I believe $watch is the way to go versus nested or chained callbacks.
var response_complete = {call1:false, call2:false};
$http.post("myapi.com/slug", data, header).then(function(res){
/* ... */
response_complete.call1 = true;
});
if(make_this_call==true){
$http.post("myapi.com/anotherslug", data, header).then(function(res){
/*...*/
response_complete.call2 = true;
});
} else response_complete.call2 = true;
$scope.$watch("response_complete",function(){
if(response_complete.call1==true && response_complete.call2==true){
console.log("DONE!");
}
});
So the idea is to create a global variable, and watch it as the two calls complete. The second call, which is conditional, immediately sets it's response variable to true if it is not being made.
But the $watch callback is only fired once and the condition within it (call1 & call2 == true) is never met.
your watch do not work as response complete is not a $scope variable | property:
// replace this with $scope property declaration
//var response_complete = {call1:false, call2:false};
$scope.response_complete = {call1:false, call2:false};
then in your succeeding code use $scope.response_complete to modify its value and so your $watch will be triggered as $scope.response_complete changed.
A better solution:
As others have specified it is better to use $broadcast than $watch, so instead watching the variable throw events instead and catch those event inside your $scope.
$http.post("myapi.com/slug", data, header).then(function() {
// stuff
$scope.$broadcast("POST_SLUG_COMPLETE");
});
$http.post("myapi.com/anotherslug", data, header).then(function() {
// stuff
$scope.$broadcast("POST_ANOTHERSLUG_COMPLETE");
});
// then in your $scope
$scope.$on("POST_SLUG_COMPLETE", function () {
// stuff
});
$scope.$on("POST_ANOTHERSLUG_COMPLETE", function () {
// stuff
});
hope that helps
If you need your "global" variable for the current scope, you can just do:
$scope.complete = false;
$http.post("myapi.com/slug", data, header).then(function(res) {
$http.post("myapi.com/anotherslug", data, header).then(function(res) {
$scope.complete = true;
console.log("DONE!");
});
});
You may also use $rootScope for a more "global" value. Other alternatives are $broadcast or a property inside a service.
But more important is to ensure how are you using the async calls. If you want both to be resolved put the second call inside the first. The sample provided by you wouldn't work because response_complete.call1 = true is inside an async thread and it is always false by the time you try to verify it
I have an angular js app, that uses an ng-repeat with a directive like this:
<div data-ng-repeat="n in items">
<div data-my-directive
item="n"></div>
</div>
where items is an array with integers.
Depending on actions of the user, the items array can be completely destroyed and made anew with new integers.
First time, it may be [1,2,4,9]
and next it may be [1,3,6,7]
for instance. This is dependent on some user choices.
The directive my-directive will perform some business logic server-side, so it will call the server as soon as it gets loaded. And then after a result returns, it shows a nice table to the users.
The problem is that some users don't wait until everything is nice and loaded and switch their view (which means the array changes). In this case, I see that the calls to the server are still being executed and that the result-function is still being called, even though the directive itself has been destroyed because the ngRepeat has rebound and all of the directives are re-made.
For instance:
$http.get(service.url + '/filters').success(function(result) {
alert(result);
});
This will display all of the alerts, even of the directives that are no longer on the page.
This poses a few problems. Can I destroy the directives in the repeat when the array changes or something like that to make sure that no logic is executed in a directive that shouldn't exist anymore (or that isn't displayed on the page anymore) ?
Or do you have some other ideas on how best to approach this?
Ideally, the directives should just disappear after the ng-repeat has rebound itself, so no logic is executed as soon as data comes back from the server.
When the user changes the parameters you can cancel the running request and start a new one.
In this Scott Allen's blog post you can find the detailed explanation of how this work.
You start creating a service or a factory with the method you will call:
var getData = function(){
var canceller = $q.defer();
var cancel = function(reason){
canceller.resolve(reason);
};
var promise =
$http.get(service.url + '/filters', { timeout: canceller.promise})
.then(function(response){
return response.data;
});
return {
promise: promise,
cancel: cancel
};
};
Then you call it in this way:
var request = service.getData();
$scope.requests.push(request);
request.promise.then(function(movie){
$scope.movies.push(movie);
clearRequest(request);
}, function(reason){
console.log(reason);
});
You then provide a method that will cancel the request:
$scope.cancel = function(){
var request = // retrieve the correct request from the requests array
request.cancel("User cancelled");
// Remove the request from the array
};
So I have a few thoughts for your question.
First you could use ng-cloak which is used to prevent the Angular html template from being briefly displayed by the browser in its raw (uncompiled) form while your application is loading. I find this very helpful if I want the user to wait until all the data has returned to view the page. ex.
<div id="template1" ng-cloak>{{ 'hello' }}</div>
Second you could try a resolve. A resolve contains one or more promises that must resolve successfully before the route will change. This means you can wait for data to become available before actually changing routes.
$routeProvider
.when("/news", {
templateUrl: "newsView.html",
controller: "newsController",
resolve: {
message: function(messageService){
return messageService.getMessage();
}
}
})
The directive needs to use the $destroy event to cancel operations in progress.
app.directive("myDirective", function() {
return {
controller: function($scope, $http, $q) {
var canceller = $q.defer();
var cancel = function(reason){
canceller.resolve(reason);
};
$http.get(url, { timeout: canceller.promise})
.then(function(response){
$scope.data = response.data;
});
$scope.$on('$destroy', function () {
cancel("Scope destroyed");
});
}
}
});
When the ng-repeat removes an item, it destroys the scope of the item. Use the $destroy event to cancel asynchronous operations in progress.
From the Docs:
The $destroy() method is usually used by directives such as ngRepeat for managing the unrolling of the loop.
Just before a scope is destroyed, a $destroy event is broadcasted on this scope. Application code can register a $destroy event handler that will give it a chance to perform any necessary cleanup.
--AngularJS $rootScope.scope API Reference -- $destroy
I have a function, for example, boot:
function boot(){
return("Arbitrary value");
}
Like you can listen for the onload, onclick, onkeydown, etc., is there any easy way (Without modifying the boot function) to do a sort of onboot?
[EDIT]
Seeing confusion, I will explain more.
Imagine a script that I have no control over is imported. The boot function is randomly called, and when the boot function is called, the script I have no control over wants to do something. Whoever wrote the script would like to do something like this: onboot = function(){consoe.log("whatever")}
Is this something that is possible?
Thanks,
-Dylan
You can do something similar to what you want, but not without modifying boot. You can change the boot function so that you can register event handlers that are called whenever boot is called with something like this:
var callbacks = [];
function boot() {
callbacks.forEach(function(f) { f(); });
return "Arbitrary value";
}
function registerBootCallback(f) {
callbacks.push(f);
}
Note that the boot function doesn't need to explicitly call the handlers.
If you can't modify the boot function itself, but you control everywhere it is called, create a new function that calls boot and all the callbacks, and call that instead of boot directly. If you can't change boot and don't control everywhere it is called, (and some kind of callback mechanism doesn't already exist) then you are out of luck.
There are also probably libraries that make this a little easier, and allow you to do things like event propagation.
You can overwrite the boot function in your code without changing it in the script.
var real_boot = boot
boot = function () {
// insert your code
real_boot()
}
See this documentation. My solution creates a custom event, which will be triggered by the calling function.
Let's say we have functions base and called, base calling called once complete:
var onboot = new Event('onboot');
document.addEventListener('onboot' called);
function base () {
// I do stuff
console.log("base");
document.dispatchEvent(onboot);
}
function called () {
// do stuff here
console.log("called");
}
So if you call base now, you'll get this result:
If the function is globally accessible. You can overwrite the function with your own function and then call old function inside your own function.
Eg
other-lib.js
function boot(){
//Do something internally that you dont have control over
}
your-file.js
var oldBoot = boot;
function boot(){
oldBoot.call(this, arguments);
//Do your stuff here
}
This is probably a total newb question...apologies, but I can't get my head around it.
In a lot of angular documentation/examples I see asynchronous functions wrapped in 'timeout' blocks. Many are wrapped in setTimeout() and require the explicit use of
if (!$scope.$$phase) {
$scope.$apply();
}
Given that angular provides $timeout, the above code just seems outdated or wrong and within angular the use of $timeout should always be preferred. However, I digress.
Here is a snippet of some example code taken from: http://markdalgleish.com/2013/06/using-promises-in-angularjs-views/
var myModule = angular.module('myModule', []);
// From this point on, we'll attach everything to 'myModule'
myModule.factory('HelloWorld', function($timeout) {
var getMessages = function(callback) {
$timeout(function() {
callback(['Hello', 'world!']);
}, 2000);
};
return {
getMessages: getMessages
};
});
I see this wrapping of code in timeout blocks everywhere particularly related to asynchronous calls. But can someone explain why this is needed? Why not just change the code above to:
var myModule = angular.module('myModule', []);
// From this point on, we'll attach everything to 'myModule'
myModule.factory('HelloWorld', function() {
var getMessages = function(callback) {
callback(['Hello', 'world!']);
};
return {
getMessages: getMessages
};
});
Why wouldn't the code snippet above work just fine?
The use of $timeout or $interval is to implicitly trigger a digest cycle. The process is as follows:
Execute each task in the callback function
Call $apply after each task is executed
$apply triggers a digest cycle
An alternative is to inject $rootScope and call $rootScope.$digest() if you are using services that don't trigger a $digest cycle.
Angular uses a dirty-checking digest mechanism to monitor and update values of the scope during
the processing of your application. The digest works by checking all the values that are being
watched against their previous value and running any watch handlers that have been defined for those
values that have changed.
This digest mechanism is triggered by calling $digest on a scope object. Normally you do not need
to trigger a digest manually, because every external action that can trigger changes in your
application, such as mouse events, timeouts or server responses, wrap the Angular application code
in a block of code that will run $digest when the code completes.
References
AngularJS source: intervalSpec.js
AngularJS source: timeoutSpec.js
$q deferred.resolve() works only after $timeout.flush()
AngularJS Documentation for inprog | Digest Phases
The $timeout in your example is probably used just to simulate an async function, like $http.get. As to why $timeout and not setTimeout: $timeout automatically tells angular to update the model, without the need to call $scope.$apply()
Also, consider the following example:
$scope.func = function(){
$scope.showSomeLoadingThing = true;
//Do some long-running stuff
$scope.showSomeLoadingThing = false;
}
No loading thingy will be shown, you would have to write it like this:
$scope.func = function(){
$scope.showSomeLoadingThing = true;
$timeout(function(){
//Do some long-running stuff
$scope.showSomeLoadingThing = false;
});
}
I was implementing a on-demand script controller based on jquery's getscript, it looks like this:
function controller = function(){
var script = function(){
var scripts = {};
return {
load: function(jsurl){
$.getScript(jsurl, null);
},
run: function(js){
window[js].apply(this,null);
}
}
};
return {
script: script()
};
}
var ctlr = controller();
then here is a remote script with a function to be loaded - remote.js
function remotefunc(){
alert( 'remotefunc invoked' );
}
and here is how the whole thing supposed to work, in the main script:
ctlr.script.load( 'remote.js' ); // remote script successfully loaded
ctlr.script.run( 'remotefunc' ); // got an error, window['remotefunc'] undefined
but as you can see, 'remotefunc' is defined in the global 'window' scope, so the window object is supposed to be able to 'see' it.
I thought the problem was probably the closure stuff in the 'controller' definition, so I did a direct $.getScirpt without using the 'controller':
$.getScript( 'http://path/to/remote.js', function(){
window['remotefunc'].apply( this, null ); // this worked
} );
strange. So it is about the 'controller' implementation(I kind need it)! Anybody can help me out with this? How to fix the 'controller' implementation so the
window[js].apply(this,null);
can actually work?
Thanx.
The reason it's telling you window['remotefunc'] is undefined is because you are not giving it time to actually download and execute the remote script before attempting to call a function defined in it.
The remote script is loaded asynchronously, which means the script execution isn't paused while waiting for a response.
You will need to either re-implement the getScript method to be synchronous or somehow work your class around the fact that the function will not be available in any determinate amount of time.
EDIT: Just found another possible solution, try calling this before your request
$.ajaxSetup({async: false});
This will make the getScript method synchronous
When using something like getSript, it's important to remember that it is fetching asynchronously. Meaning, the browser fires off the request and while that's happening, code after that line executes without pause.
jQuery provides a callback function parameter to get script that allows you to do something after the asynchronous fetch is finished.
Try this:
var script = function(){
var scripts = {};
return {
load: function(jsurl, callback){
$.getScript(jsurl, callback);
},
run: function(js){
window[js].apply(this,null);
}
}
};
Then, when using it:
ctlr.load( 'remote.js', function(){
// remote script successfully loaded
ctlr.run( 'remotefunc' );
});
Could this be a timing issue?
In your working example you call the function in a callback which jQuery will not invoke until the script is loaded. In your non-working example, you call the function immediately after getScript which is asynchronously loading the script.