I have the following code
if (testNavigation() && $scope.selections.somethingChanged) {
return false;
}
in the testNavigation I am calling modal dialog and if I answer Ok in that dialog, I re-set somethingChanged to false. My problem is that when the code is executed, the testNavigation and modal dialog is by-passed and executed later and therefore my test is not working as I need it to work. What should I change in order for my logic to properly work, e.g. first invoke my modal dialog, and proceed accordingly in the main function?
This is my testNavigation method:
var testNavigation = function()
{
if ($scope.selections.somethingChanged) {
var modal = $modal.open({
controller: function ($scope) {
$scope.okClass = 'btn-primary',
$scope.okLabel = resourceFactory.getResource('Labels', 'yes'),
$scope.cancelLabel = resourceFactory.getResource('Labels', 'cancel'),
$scope.title = resourceFactory.getResource('Labels', 'unsavedChanges'),
$scope.message = resourceFactory.getResource('Messages', 'unsavedChanges');
},
templateUrl: '/app/templates/modal'
});
modal.result.then(function () {
$scope.selections.somethingChanged = false;
});
}
return true;
}
I'll try to add more details. I have LoadView() and New() functions in the Index page controller. In both of these functions I need to do the following:
if $scope.selections.somethingChanged = false I need to proceed with the logic.
if $scope.selections.somethingChanged = true I need to pop up a modal dialog asking if I want to go ahead and Cancel my changes or Stay on the current page. If I answer Yes, I want to go ahead with the logic.
So, that's the purpose of the separate testNavigation function. In the languages where each function call is sequential, that would work as I intended. But it doesn't work this way in AngularJS / JavaScript and I am not sure how to make it to work the way I need. We tried few ideas with $q service but didn't get the result.
Make testNavigation() always return a promise (either with the result of the modal, or with false straight away, when somethingChanged is false and you don't want to ask the question. Proceed when this promise is resolved:
var testNavigation = function() {
if ($scope.selections.somethingChanged) {
var modal = [...]
return modal.result; // returns a promise that will be resolved when the modal is closed
} else {
return $q.when(true); // returns a promise that will be resolved straight away with true
}
}
// when used:
testNavigation().then(function() {
...do stuff...
})
Without knowing what your test looks like, this is kind of difficult, but from what it looks like, you're creating a race condition.
You call testNavigation() which always returns true,
but because $scope.selections.somethingChanged is set to false at some point in the future,
$scope.selections.somethingChanged may not finish before the end of that evaluation- so while you're setting $scope.selections.somethingChanged to false in testNavigation, it may or may not be false when the second if is performed:
if( testNavigation() && // returns true no matter what.
$scope.selections.somethingChanged // this is changed with a promise within testNavigation. that promise could either complete or not complete before this is evaluated with the if.
){
return false;
}
var testNavigation = function() {
if ($scope.selections.somethingChanged) {
var modal = $modal.open({
// details
});
modal.result.then(function() {
// this is async and could take place any time between {locationA} and never.
$scope.selections.somethingChanged = false;
});
//{locationA}
}
return true;
}
I can imagine that producing some weird results in tests.
Related
can someone please explain this bit in my jQuery book? I don't understand the purpose of the variable initalised.
Here is some explanation in the book:
the taskController remembers if it has been initalized in a local variable called initialised.This ensures that regardless of how many times the init method is called, it will only actually initialize the controller once
jQuery code:
tasksController = function() {
var taskPage;
var initialised = false;
return {
init : function(page) {
if (!initialised) {
taskPage = page;
$(taskPage).find('[required="required"]').prev('label').append( '<span>*</span>').children( 'span').addClass('required');
$(taskPage).find('tbody tr:even').addClass('even');
$(taskPage).find('#btnAddTask').click(function(evt) {
evt.preventDefault();
$(taskPage).find('#taskCreation').removeClass('not');
});
$(taskPage).find('tbody tr').click(function(evt) {
$(evt.target).closest('td').siblings().andSelf().toggleClass('rowHighlight');
});
$(taskPage).find('#tblTasks tbody').on('click', '.deleteRow', function(evt) {
evt.preventDefault();
$(evt.target).parents('tr').remove();
});
$(taskPage).find('#saveTask').click(function(evt) {
evt.preventDefault();
if ($(taskPage).find('form').valid()) {
var task = $('form').toObject();
$('#taskRow').tmpl(task).appendTo($(taskPage).find('#tblTasks tbody'));
}
});
initialised = true;
}
}
}
}();
When this controller is initialized, the "initialised" variable is set to false. Upon calling the init function within the controller, the first line in it checks that the "initialised" variable is not true (and it's not, it's false), then executes the remainder of that code block. At the very end of that block, you can see that this variable is set to true.
This means that the next time you run this function, the local variable is going to have a value of true this time, so when you call the init function, it will re-evaluate that statement first. Since this time "initialized" is true, the check for "is the initialised variable not true" will be false, so the code within that block will not execute.
I'm itching head with concept of promises and async procedures. I have ordered a list and want to call a function with every item, wait until first procedure with the first item is done, proceed to second, third and so on. And only after every item is processed I want continue the main process.
Below is the code that made it well with the main process. So returning Q.all(promises) resulted that first all promises were processed and then main process continued. But problem was, that items (navigation keys) were processed async while I need them in sync:
function processPages(that) {
var navs = [];
Object.keys(that.navigation).map(function(key) {
navs.push({key: key, order: parseInt(that.navigation[key].index)});
});
var promises = navs.sort(function(a, b) {
return a.order - b.order;
})
.map(function(item) {
return that.parsePage(item.key).then(function(page) {
return page.sections.filter(function(section) {
return section.type == 'normal';
})
.map(function(section) {
collectStore(section, page, that);
});
});
});
return Q.all(promises);
}
Below is the code when I modified that items are processed in sync and right order, but now main process will be out of sync:
function processPages(that) {
var navs = [];
Object.keys(that.navigation).map(function(key) {
navs.push({key: key, order: parseInt(that.navigation[key].index)});
});
var promises = navs.sort(function(a, b) {
return a.order - b.order;
})
.reduce(function(previous, item) {
return previous.then(function () {
return that.parsePage(item.key).then(function(page) {
return page.sections.filter(function(section) {
return section.type == 'normal';
})
.map(function(section) {
collectStore(section, page, that);
});
});
});
}, Q());
return Q.all(promises);
}
Does anyone know what is happening here and how to use promises right way in this case?
Additional information
processPages is called from init hook. If promise (Q.all) is not used, then page hook may fire before init hook is totally processed, which I cannot allow either. This is what I refer with the "main process".
module.exports =
{
hooks: {
"init": function() {
var options = this.options.pluginsConfig['regexplace'] || {};
options.substitutes = options.substitutes || {};
// collects text replacement queries from plugin configuration
options.substitutes.forEach(function (option) {
patterns.push({re: new RegExp(option.pattern, option.flags || ''),
sub: option.substitute,
decode: option.decode || false,
store: option.store || null,
unreset: option.unreset || false});
});
this.config.book.options.variables = this.config.book.options.variables || {};
processPages(this);
},
"page": function(page) {
var that = this;
// process all normal sections in page
page.sections.filter(function(section) {
return section.type == 'normal';
})
.map(function(section) {
collectStore(section, page, that, true);
});
return page;
}
}
};
Code is part of the GitBook plugin code.
Take a look at the runnable examples (from Chrome's dev console) from the Understanding JavaScript Promise with Examples, especially the "chaining" example.
Based on your description of "...I have ordered a list and want to call a function with every item, wait until first procedure with the first item is done, proceed to second, third and so on. And only after every item is processed I want continue the main process.",
From algorithm point of view, you should be "chaining" multiple promises together:
create a Promise for every item. When an item is done, call resolve() so that then() will execute (next item in chain).
put the "main process" as the last item in the chain.
Recommend you test/learn promises' execution flow with simple example before applying it in your problem - makes it easier to understand.
Hope this helps! :-)
Solution was as simple as changing:
processPages(this);
to
return processPages(this);
on init hook.
With all the examples of services, factories, using $scope, using Controller as, I'm getting a bit confused. I have a simple ng-if expression that's returning undefined because the data to evaluate isn't ready yet:
<div ng-if="ctrl.AlreadyBoughtShoe('ShoeId')"> ... </div>
...
<script>
(function() {
var MyApp = angular.module("MyApp", []);
MyApp.controller("MyAppController", function($http, $timeout, ShoeService) {
var x = this
loadRemoteData();
function loadRemoteData() {
ShoeService.GetShoes().then(function(Shoes){
applyRemoteData(Shoes);
});
}
function applyRemoteData(Shoes) {
x.Shoes = Shoes;
}
// FAILS HERE - Get undefined on the array
x.AlreadyBoughtShoe = function(shoeId) {
for (var i = 0; i < x.Shoes.length; i++) {
// Do stuff
}
}
});
MyApp.service("ShoesService", function($http, $q){
return({
GetShoes: GetShoes
});
function GetShoes() {
var request = $http({
method: "GET",
url: /MyUrl",
cache: false,
headers: $myHeaders
});
return( request.then(handleSuccess, handleError));
}
function handleError( response ) {
if (!angular.isObject(response.data) || !response.data.message) {
return( $q.reject( "An unknown error occurred." ) );
}
return( $q.reject(response.data.message) );
}
function handleSuccess( response ) {
return( response.data );
}
});
})();
</script>
Also, if it's relevant, in this particular case it has nothing to do with shoes... and the data is a person object, and there's no ng-repeat going on, so the ID for the "shoe" is manually typed in. I jumbled up my actual code to simplify this so I can understand the best way to deal with it, but that ng-if needs to evaluate after the data is ready to be evaluated.
I'm not sure how to best used promises or whatever else I need in this style of format, which I found an example of somewhere on the web a while back.
This is happening because of the asynchronous nature of your service call in ShoeService. Your error is occurring due to code being called before x.Shoes = Shoes is resolved, essentially iterating over undefined. Try moving your logic into the then callback of your service. For example...
function loadRemoteData() {
ShoeService.GetShoes().then(function(Shoes) {
applyRemoteData(Shoes);
x.AlreadyBoughtShoe = function(shoeId) {
for (var i = 0; i < x.Shoes.length; i++) {
// Do stuff
}
}
});
}
You can probably move this to the end of applyRemoteData also if you wish. Either way you will need to execute this logic after you resolve x.Shoes
You are right - when this code runs, x.Shoes is undefined. Change:
x.AlreadyBoughtShoe = function(shoeId) {
for (var i = 0; i < x.Shoes.length; i++) {
// Do stuff
}
}
to:
x.AlreadyBoughtShoe = function(shoeId) {
for (var i = 0; i < (x.Shoes || []).length; i++) {
// Do stuff
}
}
You have multiple options.
Evaluate the ng-if to false by default until you receive the data. You keep your AlreadyBoughtShoe method but you first check if you have data. If you don't have data yet just return false. You won't have an error anymore and when your promise is resolved your HTML should reflect that.
You can delay controller initialization until your promise is resolved.
Maybe setting semaphore or something simillar can help.
Promise evaluates after some period of time, and setting variable to true after succesfull call may help. Then add that variable to the ng-if condition, which would evaluate function only when variable is true, so the promise returned.
Set variable to and condition, which would evaluate when both are true.
<div ng-if="ctrl.loaded && ctrl.AlreadyBoughtShoe('ShoeId')"> ... </div>
Then set variable to true on success ( by default is set to false because of javascript ).
function loadRemoteData() {
ShoeService.GetShoes().then(function(Shoes){
x.loaded = true;
applyRemoteData(Shoes);
});
}
This may help.
I have an object literal which represents a page structure of an app. If a certain condition is met, I want to skip a page and go to the next one.
Please take a look at the following code:
var someObj = {
"page-1": {
before: function(x){
switch(x){
case true:
break;
case false:
someObj['page-2'].init(); // HERE, I want to skip to the page-2 init() method, **and not go back!**
return false;
break;
}
},
init: function(){
this.before(false);
alert("This is page 1!"); // I don't want to see this!
},
},
"page-2": {
init: function(){
alert("this is page 2!"); // After this alert, I want to STOP, and not go back to the page-1 init() method!
return false;
},
},
}
someObj['page-1'].init();
I can't work out how to stop after the "This is page 2!" alert - I always get 2 alerts. None of the return false work? How can I do this?
Change before() to always return a boolean and make the remainder of the page-1 init() method conditional:
before: function(x){
if (!x) {
someObj['page-2'].init();
}
return x;
},
init: function(){
if (this.before(false)) {
alert("This is page 1!");
}
},
You can either have all your functions pay attention to return values from other functions they call, implementing an "abort" protocol of some kind, or you can throw an exception.
"page-2": {
init: function(){
alert("this is page 2!");
throw new Error("Cease and desist");
},
},
The choice between the two approaches depends on your needs.
What I would do is actually check to see what returns from your before() function and based on that value execute the rest of the init() method, like this:
if(this.before(false)){
do stuff
}else{
do something else
}
I've got one function checkEvery15Seconds that runs every 15 seconds. It checks to see if new comments have been added to a page.
I've got a form that submits a new comment once the submit button is pressed, then displays the new comment on the page.
In the process of adding a new comment checkEvery15Seconds is querying the database at the same time, so I end up with duplicate comments on the page (not in the database though, this is purely a JavaScript issue).
How can I get my "submitComment" function to stop checkEvery15Seconds and restart it after the "submitComment" function has finished executing?
add a boolean called somewhat suspend15sCheck in a scope which is accessible by both functions. enable it while adding the comment and afterwards set it to false again.
in your 15sCheck-function you first have to check if you are allowed to check :-)
var suspend15sCheck = false;
function addComment()
{
suspend15sCheck = true;
// add comment on base of form data
suspend15sCheck = false;
}
function myTimer()
{
if(suspend15sCheck === false)
{
// add comments via ajax request
// remember to check if the comments who will be added already exist :-)
}
}
Simplest solution: use a flagging variable that you turn on and off. The first line of your "checkEvery15Seconds" function reads: if (!global_checkingEvery15Seconds) return;
Just set that variable (whatever you name it, global or object-bound) to true when you want the checking turned on, and off when you don't.
You'll need a status variable to indicate the current state of the comment ajax request
var requestComments = false;
if(requestComments === false) {
requestComments = true;
// make ajax request
// on ajax success/fail
requestComments = false;
}
Wrap it up in an object that allows other functions to set start/stop flags on it.
function My15SecondsObj() {
var objSelf = this;
//
this.run();
}
My15SecondsObj.Paused = false;
My15SecondsObj.prototype.run= function() {
if (!Paused)
{
// Do your call here
}
var _this = this;
setTimeout(function() { _this.run(); }, 15000);
}
Now when you want to use this object, just do
var myObj = new My15SecondsObj();
and when you want to pause it,
myObj.Paused = true;
and start it again by doing:
myObj.Paused = false;
Add some events if you want to get really crazy, so that other objects can subscribe to notifications about when the database updates have succeeded, etc...