Recursive function in promise with $timeout resolves to quick - angularjs

I am creating a templateparser that can parse multiple templates and return the generated html content. For binding the html and view information I am using the Angular $compile service. The problem I am encountering is that the promise .then() is called before the promise is resolved (and thus results in undefined).
AngularJS version: 1.6.3
The parser function
/**
* Using the $compile function, this function generates a full HTML page based on the given process and template
* It does this by binding the given process to the template $scope and uses $compile to generate a HTML page
* #param {Afhandeling} process - The process that can bind to the template
* #param {string} templatePath - The location of the template that should be used
* #param {boolean} [useCtrlCall=true] - Whether or not the process should be a sub part of a $ctrl object. If the template is used
* for more then only an email template this could be the case (EXAMPLE: $ctrl.<process name>.timestamp)
* #return {IPromise<string>} A full HTML page
*/
public createEmail(process: Afhandeling, templatePath: string, useCtrlCall = true): ng.IPromise<string> {
let processScope = {};
if (useCtrlCall) { //Create scope object | Most templates are called with $ctrl.<process name>
const controller = "$ctrl";
processScope[controller] = {};
processScope[controller][process.__className.toLowerCase()] = process;
} else {
processScope = process;
}
return this.$http.get(templatePath)
.then((response) => {
let template = response.data;
let scope = this.$rootScope.$new();
angular.extend(scope, processScope);
let generatedTemplate = this.$compile(jQuery(template))(scope);
let waitForRenderCompletion = () => {
if (scope.$$phase || this.$http.pendingRequests.length) {
console.warn("Time for a timeout.");
this.$timeout(waitForRenderCompletion);
} else {
console.warn("Lets return the template.");
return generatedTemplate[0].innerHTML;
}
};
waitForRenderCompletion();
})
.catch((exception) => {
console.error(exception);
this.logger.error(
TemplateParser.getOnderdeel(process),
"Email template creation",
(<Error>exception).message
);
return null;
});
}
The function call
this.templateParser.createEmail(
this.model,
"<template url>"
).then((template: string) => {
console.warn(template); //Results in 'undefined'
});
The reason I am watchting $$phase for changes is because of $compile not giving any feedback on when it is done compiling. The template can consist of an undefinite number of templates bound together by ng-include's. ng-includes are also async so I cannot think of any other way to check when the $compile is done (My question about a better solution then this).
What I am thinking
When I look at the console output I get the following:
Time for a timeout.
undefined
(2) Time for a timeout.
Lets return the template.
So it seems like the promise is automatically resolved when the first $timeout resolves. Yet this doesn't make any sense, since I am not returning anything.
Any help is appreciated.
Answer
Thanks #charlietfl for the hint. The working code is below. I'm now returning the function, so that I have a return value in my promise. I am also returning the $timeout, so that the function can be called recursively.
The code:
let waitForRenderCompletion = () => {
if (scope.$$phase || this.$http.pendingRequests.length) {
console.warn("Time for a timeout.");
return this.$timeout(waitForRenderCompletion);
});
} else {
console.warn("Lets return the template.");
return generatedTemplate[0].innerHTML;
}
};
return waitForRenderCompletion();

Related

Jasmine: Testing a function returning a promise

I am writing tests using Jasmine for my angular application. All the tests are passing. My class looks like follows:
class xyz implements ng.IComponentController {
private myList: ng.IPromise<MyList[]> ;
//declare necessary variables
/* #ngInject */
constructor(private ListService: ListService,
) {
this.myList = this.ListService.getList();
}
public onChange(): void {
this.isNameUnique(this.name).then(function(unique){
scope.isUnique = unique;
scope.errorNameInput = !reg.test(scope.name) || !scope.isUnique;
scope.myFunction({
//do something
});
});
}
public isNameUnique(name: string): ng.IPromise<boolean> {
return this.myList
.then(
(names) => {
_.mapValues(names, function(name){
return name.uuid.toLowerCase();
});
return (_.findIndex(names, { uuid : uuid.toLowerCase() }) === -1) ? true : false;
});
}
}
Here, I am using ListService to pre-populate my list in the constructor itself (so it calls the service only once). Then, in my onChange method, I am checking
if a name is unique or not. The isNameUnique is returning a boolean promise.
Now, I'm trying to get 100% coverage for my test. I'm getting confused about testing isNameUnique method here. My first test is:
(Assuming myList is a json similar to response I will get from service)
this.$scope.myFunction = jasmine.createSpy('myFunction');
it('should ...', function() {
this.view.find(NAME_INPUT).val('blue').change(); // my view element.
this.getList.resolve(myList);
this.controller.isNameUnique('blue').then(function (unique) {
expect(unique).toEqual(false); //since blue is already in my json
expect(this.controller.errorNameInput).toEqual(true); //since its not unique, errornameinput will be set to true
expect(this.$scope.myFunction).toHaveBeenCalled();
});
});
I would expect this test to cover the line: scope.errorNameInput = !reg.test(scope.name) || !scope.isUnique and invocation of myFunction() but it still shows uncovered. Not sure why.
Please let me know if you see anything else wrong since I'm quite new to Angular and Jasmine. Thanks.
You need to call $scope.$digest() to cause your promise to resolve in your test. There is a handy tutorial that discusses this in depth here
Hope that helps!

I am losing my definition of a form controller when the controller is set in an ng-repeat

I have code that uses AngularJS v1.5.0 and creates multiple forms with an ng-repeat like this. Note that inside the form I show the form details between xx and xx:
<div ng-click="wos.wordFormRowClicked(wf)"
ng-form="wos.wordFormNgForm_{{wf.wordFormId}}"
ng-repeat="wf in wos.word.wordForms">
xx {{ wos['wordFormNgForm_1465657579'] }} xx
When the runs I am able to see the form details appear between the xx and xx and I am able to query the state of the form like this:
wordFormCheckAndUpdate = (): ng.IPromise<any> => {
var self = this;
var wordFormNgForm = 'wordFormNgForm_' + wf.wordFormId;
self[wordFormNgForm].$setDirty();
However in my code after calling this procedure the form becomes undefined and also nothing shows between the xx and xx. As I step through this procedure with the debugger the last line I see is the line setting the value of a and then as soon as the function finishes the information between the xx and xx disappears and the form becomes undefined:
wordEditSubmit = (): ng.IPromise<any> => {
var self = this;
return this.wordFormCheckAndUpdate().then(
() => {
return self.$http({
url: self.ac.dataServer + "/api/word/Put",
method: "PUT",
data: self.word
})
.then(
(response: ng.IHttpPromiseCallbackArg<IWordRow>): any => {
self.word = angular.copy(response.data);
self['wordNgForm'].$setPristine();
self.uts.remove(self.words, 'wordId', self.word.wordId);
response.data.current = true;
self.words.push(response.data);
var a = 99;
},
(error: ng.IHttpPromiseCallbackArg<any>): any => {
self.ers.error(error);
return self.$q.reject(error);
});
}
);
}
My problem is that if I then try to repeat this:
setDirty = (): ng.IPromise<any> => {
var self = this;
var wordFormNgForm = 'wordFormNgForm_' + wf.wordFormId;
self[wordFormNgForm].$setDirty();
}
then the controller object self[wordFormNgForm] is no longer defined.
For reference. Here is how new wordForms are created:
wordFormAdd = () => {
this.wordFormId = Math.floor(Date.now() / 1000);
var emptyWordForm: IWordForm = <IWordForm>{
wordId: this.word.wordId,
wordFormId: this.wordFormId,
posId: 1,
statusId: Status.New
};
this.word.wordForms.push(emptyWordForm);
this.wordNgForm.$setDirty();
}
Here is the remove function:
remove = (arr, property, num) => {
arr.forEach((elem, index) => {
if (elem[property] === num)
arr.splice(index, 1);
})
};
Does anyone have any advice as to how I could solve this problem
Your problem could have been explained well with a demo reproducing the issue. Nevertheless, I've partially reproduced your problem in a fiddle here (not with Typescript though, it's just vanilla JS).
What I suspect happens when you first add a wordForm object with the wordFormAdd() method, is that, if you try to reference the FormController object associated with the ng-form in the same method immediately, then it might be too early to do so, because the $digest loop might not have been completed.
This is because as soon as you click and trigger the wordFormAdd() function, a new emptyWordForm object is added to the word.wordForms array and immediately ng-repeated in your view. But, the controller hasn't had enough time to associate the newly created ng-form object with itself, so you may end up with referencing an undefined object.
wordFormAdd = () => {
this.wordFormId = Math.floor(Date.now() / 1000);
var emptyWordForm: IWordForm = <IWordForm>{
wordId: this.word.wordId,
wordFormId: this.wordFormId,
posId: 1,
statusId: Status.New
};
this.word.wordForms.push(emptyWordForm);
this.wordNgForm.$setDirty(); //<== too early to do so
}
To overcome this, you should wrap that portion of the code within a $timeout wrapper. This ensures that Angular's so-called dirty check (or simply the digest loop) is finished.
Also note that keeping a single wordNgForm or wordFormId reference doesn't make sense, because you might dynamically add other forms, each of which may be associated with a new wordNgForm key and wordFormId.
I would suggest doing the above like so:
wordFormAdd = () => {
this.wordFormId = Math.floor(Date.now() / 1000);
...
this.word.wordForms.push(emptyWordForm);
this._timeout(function(){ // $timeout injected and assigned to this._timeout in controller definition
var wordFormNgForm = 'wordFormNgForm_' + this.wordFormId;
this[wordFormNgForm].$setDirty(); // <==
});
}
However in my code after calling this procedure the form becomes undefined and also nothing shows between the xx and xx. As I step through this procedure with the debugger the last line I see is the line setting the value of a and then as soon as the function finishes the information between the xx and xx disappears and the form becomes undefined:
A possible reason where the watched value in your view ({{ wos['wordFormNgForm_1465657579'] }}) becomes undefined, is that you are fetching new values and storing a copy of them in the controller's self.word property:
...
.then((response: ng.IHttpPromiseCallbackArg<IWordRow>): any => {
self.word = angular.copy(response.data); // <==
...
},
By doing so, the collection under word.wordForms that was previously ng-repeated in the view is changed and the watched value is no longer a valid reference to an item of this collection.
Meanwhile, self['wordNgForm'] in the wordEditSubmit certainly isn't associated with a FormController object as far as the ng-repeat in your view is concerned. This is because the FormController object keys associated with an ng-form must have a format (as imposed by you) similar to something like wordFormNgForm_1465657579. Therefore, here too, you are referencing an undefined property under self['wordNgForm']:
...
.then((response: ng.IHttpPromiseCallbackArg<IWordRow>): any => {
self.word = angular.copy(response.data);
self['wordNgForm'].$setPristine(); // <==
...
},
This looks like standard issue with java script that this means different things depending what called the function.
What I would advise is to generate form names and place them in wos.word.wordForms collection and bind them from there. Doing gymnastics like ng-form="wos.wordFormNgForm_{{wf.wordFormId}}" and var wordFormNgForm = 'wordFormNgForm_' + wf.wordFormId; feel quite awkward.
If there is a reason you are not using this approach please tell me, there might be a different solution :)

Protractor: functions overloading

I try to extend ElementFinder library. I wondering how I can require different methods with the same names?
I want to make something like:
// spec.js
var ef1 = require('./ef_extend1.js');
var ef2 = require('./ef_extend2.js');
expect(column_resizer.ef1.getWidth()).toEqual(18);
expect(column_resizer.ef2.getWidth()).toEqual(18);
Now I have an error:
TypeError: Cannot read property 'getWidth' of undefined
My required libraries:
// ef_extend1.js
var ElementFinder = $('').constructor;
ElementFinder.prototype.getWidth = function() {
return this.getSize().then(function(size) {
return size.width + 1;
});
};
And the second one:
// ef_extend2.js
var ElementFinder = $('').constructor;
ElementFinder.prototype.getWidth = function() {
return this.getSize().then(function(size) {
return size.width;
});
};
I guess you've used a solution from Protractor issue #1102, but now it can be accomplished a bit easier after PR#1633, because ElementFinder is now exposed in protractor global variable:
protractor.ElementFinder.prototype.getWidth = function () {
return this.getSize().then(function (size) {
return size.width;
});
};
expect($('body').getWidth()).toBe(100);
Update:
As I said in the comment, ElementFinder can only be extended again and again. If you already had a method getWidth, and you extend ElementFinder with one more getWidth implementation, then the first one will be overriden, there should not be any conflict. But you'll have to keep them in strict order depending on when do you want to use appropriate set of methods:
require('./ef_extend1.js');
expect(column_resizer.getWidth()).toEqual(18);
require('./ef_extend2.js');
expect(column_resizer.getWidth()).toEqual(18);
Actually I've came with some alternative approach, but I do not think it will be nice to use, but anyway. Here is a sample module with extension methods:
// ef_extend1.js
// shortcut
var EF = protractor.ElementFinder;
// holds methods you want to add to ElementFinder prototype
var extend = {
getWidth: function () {
return this.getSize().then(function (size) {
return size.width;
});
}
};
// will hold original ElementFinder methods, if they'll get overriden
// to be able to restore them back
var original = {};
// inject desired methods to prototype and also save original methods
function register() {
Object.keys(extend).forEach(function (name) {
original[name] = EF.prototype[name]; // save original method
EF.prototype[name] = extend[name]; // override
});
}
// remove injected methods and return back original ones
// to keep ElementFinder prototype clean after each execution
function unregister() {
Object.keys(original).forEach(function (name) {
if (typeof original[name] === 'undefined') {
// if there was not such a method in original object
// then get rid of meaningless property
delete EF.prototype[name];
} else {
// restore back original method
EF.prototype[name] = original[name];
}
});
original = {};
}
// pass a function, which will be executed with extended ElementFinder
function execute(callback) {
register();
callback();
unregister();
}
module.exports = execute;
And you will use them like that, being able to run protractor commands in "isolated" environments, where each of them has it's own set of methods for ElementFinder:
var ef1 = require('./ef_extend1.js');
var ef2 = require('./ef_extend2.js');
ef1(function () {
expect(column_resizer.getWidth()).toEqual(18);
});
ef2(function () {
expect(column_resizer.getWidth()).toEqual(18);
});
I'm not quire sure about it, maybe I am over-engineering here and there are solutions much easier than that.

How do you tell when a view is loaded in extjs?

Im working on an extjs application. We're have a page that is for looking at a particular instance of an object and viewing and editing it's fields.
We're using refs to get hold of bits of view in the controller.
This was working fine, but I've been sharding the controller into smaller pieces to make it more managable and realised that we are relying on a race condition in our code.
The logic is as follows:
Initialise the controller
parse the url to extract the id of the object
put in a call to load the model with the given view.
in the load callback call the controller load method...
The controller load method creates some stores which fire off other requests for bits of information using this id. It then uses some of the refs to get hold of the view and then reconfigures them to use the stores when they load.
If you try and call the controller load method immediately (not in the callback) then it will fail - the ref methods return undefined.
Presumably this is because the view doesnt exist... However we aren't checking for that - we're just relying on the view being loaded by the time the server responds which seems like a recipe for disaster.
So how can we avoid this and be sure that a view is loaded before trying to use it.
I haven't tried rewriting the logic here yet but it looks like the afterrender event probably does what I want.
It seems like waiting for both the return of the store load and afterrender events should produce the correct result.
A nice little abstraction here might be something like this:
yourNamespace.createWaitRunner = function (completionCallback) {
var callback = completionCallback;
var completionRecord = [];
var elements = 0;
function maybeFinish() {
var done = completionRecord.every(function (element) {
return element === true
});
if (done)
completionCallback();
}
return {
getNotifier: function (func) {
func = func || function (){};
var index = elements++;
completionRecord[index] = false;
return function () {
func(arguments);
completionRecord[index] = true;
maybeFinish();
}
}
}
};
You'd use it like this:
//during init
//pass in the function to call when others are done
this.waiter = yourNamespace.createWaitRunner(controller.load);
//in controller
this.control({
'SomeView': {
afterrender: this.waiter.getNotifier
}
});
//when loading record(s)
Ext.ModelManager.getModel('SomeModel').load(id, {
success: this.waiter.getNotifier(function (record, request) {
//do some extra stuff if needs be
me.setRecord(record);
})
});
I haven't actually tried this out yet so it might not be 100% but I think the idea is sound

Angularjs promise not binding to template in 1.2

After upgrading to 1.2, promises returned by my services behave differently...
Simple service myDates:
getDates: function () {
var deferred = $q.defer();
$http.get(aGoodURL).
success(function (data, status, headers, config) {
deferred.resolve(data); // we get to here fine.
})......
In earlier version I could just do, in my controller:
$scope.theDates = myDates.getDates();
and the promises returned from getDates could be bound directly to a Select element.
Now this doesn't work and I'm forced to supply a callback on the promise in my controller or the data wont bind:
$scope.theDates = matchDates.getDates();
$scope.theDates.then(function (data) {
$scope.theDates = data; // this wasn't necessary in the past
The docs still say:
$q promises are recognized by the templating engine in angular, which means that in templates you can treat promises attached to a scope as if they were the resulting values.
They (promises) were working in older versions of Angular but in the 1.2 RC3 automatic binding fails in all my simple services.... any ideas on what I might be doing wrong.
There are changes in 1.2.0-rc3, including one you mentioned:
AngularJS 1.2.0-rc3 ferocious-twitch fixes a number of high priority
issues in $compile and $animate and paves the way for 1.2.
This release also introduces some important breaking changes that in some cases could break your directives and templates. Please
be sure to read the changelog to understand these changes and learn
how to migrate your code if needed.
For full details in this release, see the changelog.
There is description in change log:
$parse:
due to 5dc35b52, $parse and templates in general will no longer automatically unwrap promises. This feature has been deprecated and
if absolutely needed, it can be reenabled during transitional period
via $parseProvider.unwrapPromises(true) api.
due to b6a37d11, feature added in rc.2 that unwraps return values from functions if the values are promises (if promise unwrapping is
enabled - see previous point), was reverted due to breaking a popular
usage pattern.
As #Nenad notices, promises are no longer automatically dereferenced. This is one of the most bizarre decisions I've ever seen since it silently removes a function that I relied on (and that was one of the unique selling points of angular for me, less is more). So it took me quite a bit of time to figure this out. Especially since the $resource framework still seems to work fine. On top of this all, this is also a release candidate. If they really had to deprecate this (the arguments sound very feeble) they could at least have given a grace period where there were warnings before they silently shut it off. Though usually very impressed with angular, this is a big minus. I would not be surprised if this actually will be reverted, though there seems to be relatively little outcry so far.
Anyway. What are the solutions?
Always use then(), and assign the $scope in the then method
function Ctrl($scope) {
foo().then( function(d) { $scope.d = d; });
)
call the value through an unwrap function. This function returns a field in the promise and sets this field through the then method. It will therefore be undefined as long as the promise is not resolved.
$rootScope.unwrap = function (v) {
if (v && v.then) {
var p = v;
if (!('$$v' in v)) {
p.$$v = undefined;
p.then(function(val) { p.$$v = val; });
}
v = v.$$v;
}
return v;
};
You can now call it:
Hello {{ unwrap(world) }}.
This is from http://plnkr.co/edit/Fn7z3g?p=preview which does not have a name associated with it.
Set $parseProvider.unwrapPromises(true) and live with the messages, which you could turn off with $parseProvider.logPromiseWarnings(false) but it is better to be aware that they might remove the functionality in a following release.
Sigh, 40 years Smalltalk had the become message that allowed you to switch object references. Promises as they could have been ...
UPDATE:
After changing my application I found a general pattern that worked quite well.
Assuming I need object 'x' and there is some way to get this object remotely. I will then first check a cache for 'x'. If there is an object, I return it. If no such object exists, I create an actual empty object. Unfortunately, this requires you to know if this is will be an Array or a hash/object. I put this object in the cache so future calls can use it. I then start the remote call and on the callback I copy the data obtained from the remote system in the created object. The cache ensures that repeated calls to the get method are not creating lots of remote calls for the same object.
function getX() {
var x = cache.get('x');
if ( x == undefined) {
cache.put('x', x={});
remote.getX().then( function(d) { angular.copy(d,x); } );
}
return x;
}
Yet another alternative is to provide the get method with the destination of the object:
function getX(scope,name) {
remote.getX().then( function(d) {
scope[name] = d;
} );
}
You could always create a Common angular service and put an unwrap method in there that sort of recreates how the old promises worked. Here is an example method:
var shared = angular.module("shared");
shared.service("Common", [
function () {
// [Unwrap] will return a value to the scope which is automatially updated. For example,
// you can pass the second argument an ng-resource call or promise, and when the result comes back
// it will update the first argument. You can also pass a function that returns an ng-resource or
// promise and it will extend the first argument to contain a new "load()" method which can make the
// call again. The first argument should either be an object (like {}) or an array (like []) based on
// the expected return value of the promise.
// Usage: $scope.reminders = Common.unwrap([], Reminders.query().$promise);
// Usage: $scope.reminders = Common.unwrap([], Reminders.query());
// Usage: $scope.reminders = Common.unwrap([], function() { return Reminders.query(); });
// Usage: $scope.reminders.load();
this.unwrap = function(result, func) {
if (!result || !func) return result;
var then = function(promise) {
//see if they sent a resource
if ('$promise' in promise) {
promise.$promise.then(update);
}
//see if they sent a promise directly
else if ('then' in promise) {
promise.then(update);
}
};
var update = function(data) {
if ($.isArray(result)) {
//clear result list
result.length = 0;
//populate result list with data
$.each(data, function(i, item) {
result.push(item);
});
} else {
//clear result object
for (var prop in result) {
if (prop !== 'load') delete result[prop];
}
//deep populate result object from data
$.extend(true, result, data);
}
};
//see if they sent a function that returns a promise, or a promise itself
if ($.isFunction(func)) {
// create load event for reuse
result.load = function() {
then(func());
};
result.load();
} else {
then(func);
}
return result;
};
}
]);
This basically works how the old promises did and auto-resolves. However, if the second argument is a function it has the added benefit of adding a ".load()" method which can reload the value into the scope.
angular.module('site').controller("homeController", function(Common) {
$scope.reminders = Common.unwrap([], Reminders.query().$promise);
$scope.reminders = Common.unwrap([], Reminders.query());
$scope.reminders = Common.unwrap([], function() { return Reminders.query(); });
function refresh() {
$scope.reminders.load();
}
});
These were some good answers, and helped me find my issue when I upgraded angular and my auto-unwrapping of promises stopped working.
At the risk of being redundant with Peter Kriens, I have found this pattern to work for me (this is a simple example of simply putting a number of famous people's quotes onto a page).
My Controller:
angular.module('myModuleName').controller('welcomeController',
function ($scope, myDataServiceUsingResourceOrHttp) {
myDataServiceUsingResourceOrHttp.getQuotes(3).then(function (quotes) { $scope.quotes = quotes; });
}
);
My Page:
...
<div class="main-content" ng-controller="welcomeController">
...
<div class="widget-main">
<div class="row" ng-repeat="quote in quotes">
<div class="col-xs-12">
<blockquote class="pull-right">
<p>{{quote.text}}</p>
<small>{{quote.source}}</small>
</blockquote>
</div>
</div>
</div>
...

Resources