Defer digest until Firebase .set().then() complete - angularjs

This might be more a promise misunderstanding, but I'm stuck with a situation in which I'm calling firebaseRef.set(this.myData).then(this.resolve) in a directive child controller function. In .then(this.resolve), the this.resolve() function calls a parent controller function (bound with '&'), but the model changes are not $digest()ed. I've tried returning a promise from save() using $q, but that did not help. Here is save():
this.save = () => {
const chorePath = `chores/${member.$id}/${this.chore.$id}`;
$root.user.$ref.child(chorePath)
.set(this.chore)
.then(this.resolve);
}
};
Here is the directive where resolve() is bound
<chore-editor chore="$ctrl.selectedChore"
resolve="$ctrl.choreSaved()"></chore-editor>
And then here's the choreSaved() function
this.choreSaved = () => {
this.editing = false;
};
this.editing = false; should hide the modal popup, but it doesn't because $digest() is not triggered. The next $digest() cycle does hide the modal (triggered by clicking on anything model bound). Is there a way I can structure this to allow angular to properly $digest() after the async .set() call? Here's a picture that I think describes my issue.

$scope.$apply() or $rootScope.$apply()
Just for knowledge, you can look into angular source and see that $timeout, ngClick, ngBlur and other directives after doing some stuff always call $apply. Thats why usually you do not need to call it manually.

Related

Angular-mocks digest doesn't wait for promise

I want to have to function that returns a promise that resolves when the DOM is loaded:
function waitForDom() {
return $q((resolve) => {
$window.addEventListener('DOMContentLoaded', () => {
resolve("yay, i've loaded");
});
})
})
This works just fine running in the browser. However, when I try to test it, the test never finishes as this promise never resolves:
it('some test', (done) => inject(($window, $q, $rootScope) => {
waitForDom()
.then(it => expect(it).toBe("yay, i've loaded"))
.then(done);
$rootScope.$digest();
}));
From what I understand of angular, promises don't resolve (and chain) until you call $digest. This makes it so that you can test angular in a synchronous fashion. I get it. However, the example that I have here should finish, but for some reason it always times out.
I've tried putting the resolve() inside $scope.$apply(), but I get "$digest already in progress".
I've tried putting the $digest inside setTimeout and that gives me a $digest already in progress too, which I'm really confused about. But that's not really the problem.
TLDR How do I get my test to actually finish?
Edit
While I can work around my above example, I'm looking for a general solution for when you resolve a promise inside the body of a DOM event listener, like this:
function appendIframe() {
return $q((resolve) => {
const iframe = $window.document.createElement('iframe');
const iframeParent = $window.document.body;
iframe.src = authUrl;
iframe.onload = function() {
resolve('iframe loaded')
};
iframeParent.appendChild(iframe);
});
})
And no, doing this with jqlite doesn't make a difference.
Use angular.element:
function waitForDom() {
return $q((resolve) => {
angular.element( () => {
resolve("yay, i've loaded");
});
})
})
From the Docs:
Most browsers provide similar functionality in the form of a DOMContentLoaded event. However, jQuery's .ready() method differs in an important and useful way: If the DOM becomes ready and the browser fires DOMContentLoaded before the code calls .ready( handler ), the function handler will still be executed. In contrast, a DOMContentLoaded event listener added after the event fires is never executed.

How to pass vm to a setTimeout in AngularJs? Changes to scope don't update DOM view

I'm trying the following code. Everything seems to be working as I see the component appear/disappear depending on the value I set for the variable. However, when I do that in the setTimeout(...) function, it starts to misbehave. The poofy text shows but the value set to vm doesn't. My guess is that I need to pass it in somehow but I'm not sure how.
(function () {
'use strict';
angular
.module("app")
.controller("Banana", banana);
function banana() {
var vm = this;
vm.showStuff = false;
setTimeout(function () {
console.log("poof!");
vm.showStuff = true;
}, 1000);
}
})();
Do I need to make the view-model globally accessible?
Use the $apply method with setTimeout:
//WORKS
setTimeout(function () {
$scope.$apply("vm.showStuff = true");
}, 1000);
OR use the AngularJS $timeout service:
//RECOMMENDED
$timeout(function () {
vm.showStuff = true;
}, 1000);
The window.setTimeout method creates an event outside the AngularJS framework and its digest cycle. The $timeout service wraps window.setTimeout and integrates it with the AngularJS framework and its digest cycle.
Angular modifies the normal JavaScript flow by providing its own event processing loop. This splits the JavaScript into classical and Angular execution context. Only operations which are applied in Angular execution context will benefit from Angular data-binding, exception handling, property watching, etc... You use $apply() to enter Angular execution context from JavaScript.
Keep in mind that in most places (controllers, services) $apply has already been called for you by the directive which is handling the event. An explicit call to $apply is needed only when implementing custom event callbacks, or when working with third-party library callbacks.
— AngularJS Developer Guide - Integration with the browser event loop
Try use the script bellow.
(function () {
'use strict';
angular
.module("app")
.controller("Banana", banana);
function banana($timeout) {
var vm = this;
vm.showStuff = false;
$timeout(function () {
console.log("poof!");
vm.showStuff = true;
}, 1000);
}
})();
To be noted - there's additional step required.
Substitute setTimeout(...) for $timeout(...).
Pass $timeout into banana(...).
Provide banana.$inject = ["$timeout",...].
Hey What Mateus Koppe is showing it's a good answer, I would like to extend, just because this can help someone that needs to apply changes to the view.
The $Timeout service on angular refreshes the data on the view, but with setTimeout you don't have the same effect, so you should use $scope and $apply() to force the refresh on the screen.
On my EXAMPLE I use them, to show you the effect.
//Timeout angularjs
$timeout(function () {
console.log("poof! $timeout");
vm.showStuff = true;
}, 1000);
//Tiemout regular
setTimeout(function () {
console.log("poof1! setTimeout");
vm.showStuff1 = true;
}, 1000);
//Timeout + $scope.$apply
setTimeout(function () {
console.log("poof2! setTimeout");
vm.showStuff2 = true;
$scope.$apply(); //<<<<<<<<<<<<<<<<<<<<<<<<<<<
}, 3000);
I hope it helps.
As its explained on $apply()
$apply() is used to execute an expression in angular from outside of
the angular framework. (For example from browser DOM events,
setTimeout, XHR or third party libraries). Because we are calling into
the angular framework we need to perform proper scope life-cycle of
exception handling, executing watches.
Also you should avoid using on $digest()
Usually, you don't call $digest() directly in controllers or in
directives. Instead, you should call $apply() (typically from within a
directive), which will force a $digest().
Please check my example here.
https://jsfiddle.net/moplin/x8mnwf5a/
As explained in previous answers it has to do with the $digest cycle, native setTimeout() doesn't trigger the digest cycle and therefore the view doesn't get re-rendered with new value of vm.showStuff.
I'd slightly extend Pablo Palacious's answer with the following usage of $scope.$apply(),
//Timeout + $scope.$apply
setTimeout(function () {
$scope.$apply(function () {
console.log("poof2! setTimeout");
vm.showStuff2 = true;
});
}, 3000);
You could also consider using $scope.$applyAsync() which queues up expressions and functions for execution in the next digest cycle, as described here,
$applyAsync([exp]);
Schedule the invocation of $apply to occur at a later time. The actual time difference varies across browsers, but is typically around ~10 milliseconds.
This can be used to queue up multiple expressions which need to be evaluated in the same digest.

How to remove Cordova specific events outside Angular controller?

Imagine I have a controller which handles, for example, view changes:
function Controller($scope){
var viewModel = this;
viewModel.goBack= function(){
viewModel.visible = visibleLinks.pop(); //get last visible link
viewModel.swipeDirection = 'left';// for view change animation
}
}
But I want to handle it not only for example with HTML buttons inside <body>, but also with Back button on device. So I have to add Event Listener for deviceready event, and also explicit call $scope.$apply() in order to fact, that it is called outside of AngularJS context, like this:
document.addEventListener("deviceready", function(){
document.addEventListener("backbutton", function(){
viewModel.goBack();
$scope.$apply();
}, false);
}, false);
}
But I also want to follow (relatively :) ) new controllerAssyntax, cause this is recommended now e.g. by Todd Motto: Opinionated AngularJS styleguide for teams and it allows to remove $scope from controllers when things like $emit or $on are not used. But I can't do it, case I have to call $apply() cause my context is not Angular context when user clicks on device back button. I thought about creating a Service which can be wrapper facade for cordova and inject $scope to this service but as I read here: Injecting $scope into an angular service function() it is not possible. I saw this: Angular JS & Phonegap back button event and accepted solution also contains $apply() which makes $scope unremovable. Anybody knows a solution to remove Cordova specific events outside Angular controller, in order to remove $scope from controllers when not explicity needed? Thank you in advance.
I don't see a reason why to remove the $scope from the controller. It is fine to follow the best practice and to remove it if not needed, but as you said you still need it for $emit, $on, $watch.. and you can add it $apply() in the list for sure.
What I can suggest here as an alternative solution is to implement a helper function that will handle that. We can place it in a service and use $rootScope service which is injectable.
app.factory('utilService', function ($rootScope) {
return {
justApply: function () {
$rootScope.$apply();
},
createNgAware: function (fnCallback) {
return function () {
fnCallback.apply(this, arguments);
$rootScope.$apply();
};
}
};
});
// use it
app.controller('SampleCtrl', function(utilService) {
var backBtnHandler1 = function () {
viewModel.goBack();
utilService.justApply(); // instead of $scope.$apply();
}
// or
var backBtnHandler2 = utilService.createNgAware(function(){
viewModel.goBack();
});
document.addEventListener("backbutton", backBtnHandler2, false);
});
In my case I was simply forwarding Cordova events with the help of Angular $broadcast firing it on the $rootScope. Basically any application controller would then receive this custom event. Listeners are attached on the configuration phase - in the run block, before any controller gets initialized. Here is an example:
angular
.module('app', [])
.run(function ($rootScope, $document) {
$document.on('backbutton', function (e) {
// block original system back button behavior for the entire application
e.preventDefault();
e.stopPropagation();
// forward the event
$rootScope.$broadcast('SYSTEM_BACKBUTTON', e);
});
})
.controller('AppCtrl', function ($scope) {
$scope.$on('SYSTEM_BACKBUTTON', function () {
// do stuff
viewModel.goBack();
});
});
Obviously in the $scope.$on handler you do not have to call $scope.$apply().
Pros of this solution are:
you'll be able to modify an event or do something else for the entire application before the event will be broadcasted to all the controllers;
when you use $document.on() every time controller is instantiated, the event handler stays in the memory unless you manually unsibscribe from this event; using $scope.$on cares about it automatically;
if the way a system dispatches Cordova event changes, you'll have to change it in one place
Cons:
you'll have to be careful when inheriting controllers which already have an event handler attached on initialization phase, and if you want your own handler in a child.
Where to place the listeners and the forwarder is up to you and it highly depends on your application structure. If your app allows you could even keep all the logic for the backbutton event in the run block and get rid of it in controllers. Another way to organize it is to specify a single global callback attached to $rootScope for example, which can be overriden inside controllers, if they have different behavior for the back button, not to mess with events.
I am not sure about deviceready event though, it fires once in the very beginning. In my case I was first waiting for the deviceready event to fire and then was manually bootstrapping AngularJS application to provide a sequential load of the app and prevent any conflicts:
document.addEventListener('deviceready', function onDeviceReady() {
angular.element(document).ready(function () {
angular.bootstrap(document.body, ['app']);
});
}, false);
From my point of view the logic of the app and how you bootstrap it should be separated from each other. That's why I've moved listener for backbutton to a run block.

$digest already in progress error - with custom directive - not fixed by $timeout

I have a custom directive datepicker that uses scope.apply and works well. I cut out most of it to avoid cluttering the question, here is a simple version
appAdmin.directive("datepickerPss", ["$compile", "$parse", function ($compile, $parse) {
return {
$element.datepicker($scope.options).on("changeDate", function (ev) {
$scope.$apply(function () {
ngModel.$setViewValue(ev.date);
});
});
}
}]);
I have the custom datepicker in a modal, I simply want to initialize the value so in my controller I did the following at the top and had the "$digest already in progress" error
$scope.sDate = Date.now();
So reading up on this issue and the scope apply I changed it to the following in my controller
$timeout(function() {
$scope.sDate = Date.now();
});
However I still get the $digest in progress error. I'm not sure where to go from here. All the posts I have read have had their issues resolved by using $timeout.
Remove $scope.$apply and just use $timeout instead.
$element.datepicker($scope.options).on("changeDate", function (ev) {
$timeout(function () {
ngModel.$setViewValue(ev.date);
}, 0);
});
$scope.$apply starts a new $digest cycle on $rootScope, so calling it inside of your directive starts another $digest cycle while one is already occurring. By wrapping your call in $timeout, it'll wait until the previous $digest cycle finishes before applying your changes.
In addition, if you are trying to initialize a value AFTER you've already bound bound your change event in your directive, you could run into issues since the directive's digest cycle might still be in progress while your controller is being parsed and executed.

AngularJS deferred specific behavior inside the scope

Assume that we have a bit of html like that:
<button
id="my-login-button"
ng-hide="loggedIn"
ng-click="login()">Log me in!</button>
Also we have a JavaScript:
// controller.js
$scope.login = function () {
API.login()
.then(function () {
console.log('login promise resolved');
});
};
// service.js
app.factory('API', ['$q', '$timeout', function ($q, $timeout) {
return {
login: function () {
var login = $q.defer();
// async login
VK.Auth.login(
function () {
// login.resolve(); // doesn't work without $timeout()
$timeout(login.resolve); // works as it should
},
VK.access.FRIENDS | VK.access.AUDIO
);
return login.promise;
}
};
}]);
This piece of code works properly, but the mysterious part is near the $timeout() function. Why I should wrap my resolving with it? Why code doesn't work as expected without it?
I don't do something with scope variables, I'm just consoling. And without $timeout it will be called with next digest...
As for me it have no sense, if I need to change scope props, I will wrap everything in $apply.
Could anyone explain why usual deferred behavior became mysterious one?
P.S. I've solved my issue with $timeout after reading this question answers.
In AngularJS promise results are propagated asynchronously, inside a $digest cycle. So, the results of then() are not propagated until the next digest cycle, which never comes without a $timeout or $http or $apply to trigger one.
See also Promise callback not called in Angular JS

Resources