Browser freezes when use function api in ng-if AngularJS - angularjs

I use angularjs and I have a problem with ng-if when I use a function that returns true or false in two API, the browser is freezes
self.isShow= function() {
service.getKioskLogByKioskId([stateParams.kioskId], function (data) {
self.kioskLog = data;
service.getKiosk([stateParams.kioskId], function (kiosk) {
self.kiosk = kiosk;
debugger
if (self.kioskLog.isConnected && self.kiosk.isActive)
return true;
else
return false;
});
});
}
and in html
ng-if="self.isShow()"

Angular's ng-if can't be an async, it expects to get true/false in synchronous manner.
Don't forget that EVERY angular directive creates a "watch" that will be invoked as part of angular's dirty check mechanism, If you make a "heavy" operation on it, it will stuck the browser.
Basically there are 2 wrong things in your code, the first one, your isShow is not returning boolean value at all, (it always returns undefined).
The second one, you are probably making an API call inside the service.getKioskLogByKioskId method.
In order to solve both of the issues, you can make the service.getKioskLogByKioskId call inside the constructor of you controller, (if it is a component there is $onInit lifecycle hook for that), then save the async result on the controller, and use it the view.
It should look something like that:
class MyController {
constructor(stateParams) {
this.stateParams = stateParams;
this.isShow = false; // initial value
}
$onInit() {
const self =this;
service.getKiosk([stateParams.kioskId], function (kiosk) {
self.kiosk = kiosk;
debugger
if (self.kioskLog.isConnected && self.kiosk.isActive)
self.isShow = true;
else
self.isShow = false;
});
}
}
// view.html
<div ng-if="$ctrl.isShow"></div>

Related

Typescript async/await doesnt update AngularJS view

I'm using Typescript 2.1(developer version) to transpile async/await to ES5.
I've noticed that after I change any property which is bound to view in my async function the view isn't updated with current value, so each time I have to call $scope.$apply() at the end of function.
Example async code:
async testAsync() {
await this.$timeout(2000);
this.text = "Changed";
//$scope.$apply(); <-- would like to omit this
}
And new text value isn't shown in view after this.
Is there any workaround so I don't have to manually call $scope.$apply() every time?
The answers here are correct in that AngularJS does not know about the method so you need to 'tell' Angular about any values that have been updated.
Personally I'd use $q for asynchronous behaviour instead of using await as its "The Angular way".
You can wrap non Angular methods with $q quite easily i.e. [Note this is how I wrap all Google Maps functions as they all follow this pattern of passing in a callback to be notified of completion]
function doAThing()
{
var defer = $q.defer();
// Note that this method takes a `parameter` and a callback function
someMethod(parameter, (someValue) => {
$q.resolve(someValue)
});
return defer.promise;
}
You can then use it like so
this.doAThing().then(someValue => {
this.memberValue = someValue;
});
However if you do wish to continue with await there is a better way than using $apply, in this case, and that it to use $digest. Like so
async testAsync() {
await this.$timeout(2000);
this.text = "Changed";
$scope.$digest(); <-- This is now much faster :)
}
$scope.$digest is better in this case because $scope.$apply will perform dirty checking (Angulars method for change detection) for all bound values on all scopes, this can be expensive performance wise - especially if you have many bindings. $scope.$digest will, however, only perform checking on bound values within the current $scope making it much more performant.
This can be conveniently done with angular-async-await extension:
class SomeController {
constructor($async) {
this.testAsync = $async(this.testAsync.bind(this));
}
async testAsync() { ... }
}
As it can be seen, all it does is wrapping promise-returning function with a wrapper that calls $rootScope.$apply() afterwards.
There is no reliable way to trigger digest automatically on async function, doing this would result in hacking both the framework and Promise implementation. There is no way to do this for native async function (TypeScript es2017 target), because it relies on internal promise implementation and not Promise global. More importantly, this way would be unacceptable because this is not a behaviour that is expected by default. A developer should have full control over it and assign this behaviour explicitly.
Given that testAsync is being called multiple times, and the only place where it is called is testsAsync, automatic digest in testAsync end would result in digest spam. While a proper way would be to trigger a digest once, after testsAsync.
In this case $async would be applied only to testsAsync and not to testAsync itself:
class SomeController {
constructor($async) {
this.testsAsync = $async(this.testsAsync.bind(this));
}
private async testAsync() { ... }
async testsAsync() {
await Promise.all([this.testAsync(1), this.testAsync(2), ...]);
...
}
}
I have examined the code of angular-async-await and It seems like they are using $rootScope.$apply() to digest the expression after the async promise is resolved.
This is not a good method. You can use AngularJS original $q and with a little trick, you can achieve the best performance.
First, create a function ( e.g., factory, method)
// inject $q ...
const resolver=(asyncFunc)=>{
const deferred = $q.defer();
asyncFunc()
.then(deferred.resolve)
.catch(deferred.reject);
return deferred.promise;
}
Now, you can use it in your for instance services.
getUserInfo=()=>{
return resolver(async()=>{
const userInfo=await fetch(...);
const userAddress= await fetch (...);
return {userInfo,userAddress};
});
};
This is as efficient as using AngularJS $q and with minimal code.
As #basarat said the native ES6 Promise doesn't know about the digest cycle.
What you could do is let Typescript use $q service promise instead of the native ES6 promise.
That way you won't need to invoke $scope.$apply()
angular.module('myApp')
.run(['$window', '$q', ($window, $q) => {
$window.Promise = $q;
}]);
I've set up a fiddle showcasing the desired behavior. It can be seen here: Promises with AngularJS.
Please note that it's using a bunch of Promises which resolve after 1000ms, an async function, and a Promise.race and it still only requires 4 digest cycles (open the console).
I'll reiterate what the desired behavior was:
to allow the usage of async functions just like in native JavaScript; this means no other 3rd party libraries, like $async
to automatically trigger the minimum number of digest cycles
How was this achieved?
In ES6 we've received an awesome featured called Proxy. This object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc).
This means that we can wrap the Promise into a Proxy which, when the promise gets resolved or rejected, triggers a digest cycle, only if needed. Since we need a way to trigger the digest cycle, this change is added at AngularJS run time.
function($rootScope) {
function triggerDigestIfNeeded() {
// $applyAsync acts as a debounced funciton which is exactly what we need in this case
// in order to get the minimum number of digest cycles fired.
$rootScope.$applyAsync();
};
// This principle can be used with other native JS "features" when we want to integrate
// then with AngularJS; for example, fetch.
Promise = new Proxy(Promise, {
// We are interested only in the constructor function
construct(target, argumentsList) {
return (() => {
const promise = new target(...argumentsList);
// The first thing a promise does when it gets resolved or rejected,
// is to trigger a digest cycle if needed
promise.then((value) => {
triggerDigestIfNeeded();
return value;
}, (reason) => {
triggerDigestIfNeeded();
return reason;
});
return promise;
})();
}
});
}
Since async functions rely on Promises to work, the desired behavior was achieved with just a few lines of code. As an additional feature, one can use native Promises into AngularJS!
Later edit: It's not necessary to use Proxy as this behavior can be replicated with plain JS. Here it is:
Promise = ((Promise) => {
const NewPromise = function(fn) {
const promise = new Promise(fn);
promise.then((value) => {
triggerDigestIfNeeded();
return value;
}, (reason) => {
triggerDigestIfNeeded();
return reason;
});
return promise;
};
// Clone the prototype
NewPromise.prototype = Promise.prototype;
// Clone all writable instance properties
for (const propertyName of Object.getOwnPropertyNames(Promise)) {
const propertyDescription = Object.getOwnPropertyDescriptor(Promise, propertyName);
if (propertyDescription.writable) {
NewPromise[propertyName] = Promise[propertyName];
}
}
return NewPromise;
})(Promise) as any;
In case you're upgrading from AngularJS to Angular using ngUpgrade (see https://angular.io/guide/upgrade#upgrading-with-ngupgrade):
As Zone.js patches native Promises you can start rewriting all $q based AngularJS promises to native Promises, because Angular triggers a $digest automatically when the microtask queue is empty (e.g. when a Promise is resolved).
Even if you don't plan to upgrade to Angular, you can still do the same, by including Zone.js in your project and setting up a similar hook like ngUpgrade does.
Is there any workaround so I don't have to manually call $scope.$apply() every time?
This is because TypeScript uses the browser native Promise implementation and that is not what Angular 1.x knows about. To do its dirty checking all async functions that it does not control must trigger a digest cycle.
As #basarat said the native ES6 Promise doesn't know about the digest cycle. You should to promise
async testAsync() {
await this.$timeout(2000).toPromise()
.then(response => this.text = "Changed");
}
As it already has been described, angular does not know when the native Promise is finished. All async functions create a new Promise.
The possible solution can be this:
window.Promise = $q;
This way TypeScript/Babel will use angular promises instead.
Is it safe? Honestly I'm not sure - still testing this solution.
I would write a converter function, in some generic factory (didnt tested this code, but should be work)
function toNgPromise(promise)
{
var defer = $q.defer();
promise.then((data) => {
$q.resolve(data);
}).catch(response)=> {
$q.reject(response);
});
return defer.promise;
}
This is just to get you started, though I assume conversion in the end will not be as simple as this...

angularJS: Function in function

Hello I'm new in angularJS. Suitable or not to implement function inside function?
For example like this:-
$scope.loadDistrict = function(id) {
// statement
$scope.loadBasedOnYear = function(y_id) {
console.log(y_id);
// statement
};
};
If you bind method on scope, it's available from view.
From your code
$scope.loadDistrict = function(id) {
// statement
$scope.loadBasedOnYear = function(y_id) {
console.log(y_id);
// statement
};
};
loadBasedOnYear won't available until loadDistrict is called.
It's very bad pattern to follow.
It is possible but without context I don't really know why you would do this, calling $scope.loadBasedOnYear before calling $scope.loadDistrict will result in an error so be careful with such a design pattern.
Yes this is fine.
You should watch out for when the function will be executed outside of angular's cycle. E.g. if you do:
setTimeout(function() { $scope.test = 5; }, 1000);
If you need to do this then you need to wrap the function in $scope.$apply(), or use $timeout.

How can I tell when rendering has completed on ngRepeat

I am working on a large scale Angular app that is tested using selenium webdriver. My concern is that the transcluding takes time, and I need feedback to let me know when transcluding finishes. This would allow me to wait until that trigger has fired to grab additional information. Is there a way to do this? Would something like ng-repeat-end always get processed after everything has been loaded?
There is no way to tell that the DOM has been completely rendered, but you can get an event triggered when the last element was $compiled and added to the DOM with a simple directive:
.directive('last', function() {
return {
link: function(scope) {
if(scope.$last) {
$scope.emit('ngRepeat.finished');
//or really anything you want to do
}
}
}
});
Usage:
<div ng-repeat="item in items" last>
The thing to be aware of is that if "items" is changed, the ng-repeat is rebuilt, so you'll get another event.
I'm unclear on if you want to get an event after an ng-repeat, or if you just want to wait until you can be guaranteed it has finished.
This is a driver wait that waits for loading in jQuery and $http, combined with the digest/render cycle in angular (you can chop out the part that isn't about digest/render if you don't need it). It's wrapped in a commented example of how you'd plug it into a C# selenium driver call, which should translate to whichever language you are using.
/*var pageLoadWait = new WebDriverWait(WebDriver, TimeSpan.FromSeconds(timeout));
pageLoadWait.Until<bool>(
(driver) =>
{
return (bool)JS.ExecuteScript(
#"*/
try {
if (document.readyState !== 'complete') {
return false; // Page not loaded yet
}
if (window.jQuery) {
if (window.jQuery.active) {
return false;
} else if (window.jQuery.ajax && window.jQuery.ajax.active) {
return false;
}
}
if (window.angular) {
if (!window.qa) {
// Used to track the render cycle finish after loading is complete
window.qa = {
doneRendering: false
};
}
// Get the angular injector for this app (change element if necessary)
var injector = window.angular.element('body').injector();
// Store providers to use for these checks
var $rootScope = injector.get('$rootScope');
var $http = injector.get('$http');
var $timeout = injector.get('$timeout');
// Check if digest
if ($rootScope.$$phase === '$apply' || $rootScope.$$phase === '$digest' || $http.pendingRequests.length !== 0 || $rootScope.$$applyAsyncQueue.length > 0) {
window.qa.doneRendering = false;
return false; // Angular digesting or loading data
}
if (!window.qa.doneRendering) {
// Set timeout to mark angular rendering as finished
$timeout(function() {
window.qa.doneRendering = true;
}, 0);
return false;
}
}
return true;
} catch (ex) {
return false;
}
/*");
});*/
EDIT: After comment from Jackie, I noticed some situations where the page didn't fully render, adding || $rootScope.$$applyAsyncQueue.length > 0 appears to have fixed this.
I think the answer above is great but I think the point here is that even though the ng-repeat directive isolates scope, it has a scope variable that will tell you if it is the last one. If you are firing an event or something on the angular side the previous answer is probably the best ides. However, if your motives are purely testing based (like mine) this was enough...
ng-class="{'last': $last}"

How can I pass data from the directive of a parent element to child elements?

I am attempting to write a directive to resolve promises and add data to the scope so that I can lazily load data into child elements from an API.
Through console.logging I am certain that I am resolving the promise and getting my expected data, but nothing updates on the page. I am assuming that dynamically adding to the scope from inside a directive is causing my problem.
The directive seems fairly trivial and I know that the promise is getting resolved:
app.directive('resolver', () => {
return {
restrict: "AEC",
scope: {
outName: "#",
promise: "&",
defaultVal: "="
},
link: function ($scope, element, attrs) {
var promise = $scope.promise,
outName = $scope.outName || 'myPromise',
defaultVal = $scope.defaultVal || {};
$scope[outName] = defaultVal;
promise().then(data => {
$scope[outName] = data;
});
}
}
});
I was hoping that this would let me do something like the following:
<li class="company-entry" ng-repeat="company in currentFeed.companies" resolver promise="getPreview(company.companyId)" out-name="profile" default-val="{name:'Loading...',description:'',images:[],sources:[]}">
...
<a ui-sref="company.feed({ id: company.companyId })" class="company-name">{{ profile.name || 'foo' }}</a>
</li>
Firstly, I have profile.name || 'foo' in order to test if the default-val part of my code would work; since it stays at foo, not even the default value gets placed into profile.
Secondly, after the promise is resolved, I should be setting profile to the data, but the page does not updating.
Adding a $scope.$apply() causes me to get rootscope update already in progress errors.
Is there any way to accomplish what I am trying to do?
I would solve this differently. Instead of building this into a directive you can easily produce a thenable that automatically unwraps just like ngResource does.
Let's consider an example: You have an array you need to fetch online with a getArray() function which returns a promise and makes an http request.
Normally you do:
getArray().then(function(result){
$scope.data = result;
});
Which I assume you're trying to avoid because it's a lot to write if everything in your scope is like this. The trick promises used to use and ngResource still uses in Angular is the following:
Return an empty result, which is also a promise
Whenever that promise resolves, update the result on the screen.
For example, our getArray() could look something like:
function getArray(){
var request = $http(...); // actual API request
var result = []
result.then = request.then; // make the result a promise too
result.then(function(resp){
// update _the same_ object, since objects are passed by reference
// this will also update every place we've send the object to
resp.data.forEach(function(el){ result.push(el); });
});
return result;
}
Now if you call getArray() and assign the result to a scope variable, whenever the promise resolves it will update the value.

Errors binding checkbox to $scope

I have a checkbox, like:
<input type="checkbox" ng-model="isPreCheckIn" />
I'm getting isPreCheckin (boolean) from a service which uses $q and either returns from the server or localStorage (if it exists).
The call in the controller looks like:
deviceSettings.canCheckIn().then(function (canCheckIn) {
$scope.isPreCheckin = !canCheckIn ? true : false;
});
And deviceSettings.canCheckIn looks like:
function canCheckIn() {
var dfrd = $q.defer();
LoadSettings().then(function (success) {
return dfrd.resolve(localStorage.canCheckIn);
});
return dfrd.promise;
};
So, on first page load, the checkbox doesn't bind correctly to isPreCheckIn; in fact, if I do a {{isPreCheckIn}}, it doesn't either. If I switch off of that page and go back, it works.
It appears that canCheckIn is outside of angular, based on that assumption, you need to wrap your assignment within $scope.apply:
deviceSettings.canCheckIn().then(function (canCheckIn) {
$scope.$apply(function(){
$scope.isPreCheckin = !canCheckIn ? true : false;
});
});
This tells angular to recognize the changes on your $scope and apply to your UI.
I think you should wrap the following in a $apply:
function canCheckIn() {
var dfrd = $q.defer();
LoadSettings().then(function (success) {
scope.$apply(function() {
dfrd.resolve(localStorage.canCheckIn);
}
});
return dfrd.promise;
};
It sounds like a timing issue. You may need to put a resolve clause in your route to give this call time to run and then pass in the result as a DI value. Without knowing which router you are using it is impossible to give you an accurate answer, but you might look at the video on egghead.io regarding routes and resolve.

Resources