0

I'm stuck in a really bizarre situation here. It's complicated to explain but I'll try my best.

I have a UI with 4 navigation <a> buttons on top - in the center there's always a form - and at the bottom I have Previous & Next buttons.

enter image description here

Forms are constructed in MVC using Ajax.BeginForm

For each Nav link <a> element on top, I have a JavaScript function

var LoadTabs = function (e, arg) {  
    // This is to validate a form if one of the top links is clicked and form has incomplete fields...
    if (arg !== "prev" && arg !== "next") {
        if (!window.ValidateForm(false)) return false;
    }

    var url = $(this).attr('data'); // this contains link to a GET action method
    if (typeof url != "undefined") {
        $.ajax(url, { context: { param: arg } }).done(function (data) {                
            $('#partialViewContainer').html(data);
        });
    }
}

This function above binds to each top link on page load.

$('.navLinks').on('click', LoadTabs);

My Next & Previous buttons basically trigger the click event i.e. LoadTabs function.

$('button').on('click', function () { 
        if (this.id === "btnMoveToNextTab") {
            if (!window.ValidateForm(true)) return false;                

            $.ajax({
                url: url,
                context: { param: 'next' },
                method: "GET",
                data: data,
                success: function(response) {
                    if (typeof response == 'object') {
                        if (response.moveAhead) {
                            MoveNext();
                        }
                    } else {
                        $('#mainView').html(response);
                    }
                    ScrollUp(0);
                }
            });
        } 

        if (this.id === "btnMoveToPreviousTab") {
            MoveBack();
        }
        return false;
    });


MoveNext() Implementation is as below:

function MoveNext() {        
    var listItem = $('#progressbarInd > .active').next('li');        

    listItem.find('.navLink').trigger('click', ['next']);
    ScrollUp(0);
}

The problem is, for some reasons, when Nav Link 3 is active and I hit the NEXT button - Instead of posting the form first via form.submit() - the nav 4 gets triggered - hence GET for nav 4 runs before form POST of nav 3.

My ValidateForm method is basically just checking if the form exists and is valid then Submit, else returns false. It's as below:

function ValidateForm(submit) {

    var form = $('form');

    // if form doesn't exist on the page - return true and continue
    if (typeof form[0] === "undefined") return true;

    // now check for any validation errors
    if (submit) {
        if (!$(form).valid()) {                
            return false;
        } else {
            $(form).submit();
        }
    } 
    else {
        return true;
    }

    return true;
}

My speculation is that form.submit does get triggered as it should be but since submit takes a little longer to finish it continues with the next code block in the button onclick event.

I first thought that this is C# issue as in the POST I'm saving a big chunk of data with a few loops, and any code block that's process heavy I have that part in

var saveTask =  Task.Factory.StartNew(() => ControllerHelper.SomeMethod(db, model));
Task.WaitAll(saveTask);

WaitAll will wait and pause the execution until SomeMethod finishes executing. I'm not sure how can I lock a process in javascript and wait for it to finish execution. Because I think If i can somehow lock the form.submit() in ValidateForm until its finished processing .. via a callback method perhaps...

Please if anyone can put me in right direction, I'd greatly appreciate the help. If you need more information please let me know I'd be happy to provide!

Mr Lister
  • 45,515
  • 15
  • 108
  • 150
Johny
  • 387
  • 1
  • 7
  • 20

2 Answers2

2

I'm posting this is a note to Alan since it's multi-line code and I can't make it readable in a comment. To avoid the promise anti-pattern when you sometimes run an async operation and sometimes don't, one can use this:

function ValidateForm(){
    // do your logic
    if (pass) {
        return $.post("urlToPost");
    else {
        return $.Deferred().reject(xxx).promise();
    }
}

This way, you're always returning a promise. In one case, the promise comes from $.ajax(). In the other case, you're just returning a rejected promise since the operation has already failed.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • Thanks for the info, I will also edit my answer to include this info. Thanks agian – Alan Tsai Mar 25 '17 at 00:46
  • This is interesting. Instead of `return $.post(url);` can I `return form.submit(callback...);`? – Johny Mar 25 '17 at 01:08
  • 1
    @Johny - It does not appear that `form.submit()` returns a promise. Too bad. I'm not actually sure how to get a jQuery promise out of that. There probably is a way, but it does not appear to be documented. Perhaps `form.submit().promise()`, but you'd have to try it to see. – jfriend00 Mar 25 '17 at 01:40
  • @jfriend00 No, looks like submit doesn't return promise. I ended up using return $.post("urlToPost", form.serialize()); I haven't been able to reproduce this behavior anymore. Thank you both for taking your time and helping out. I have much better understanding of Jquery's deferred object. – Johny Mar 27 '17 at 15:07
1

Edit regard to return promise anti pattern

as jfriend00 commented the way original answer is returning the promise is anti pattern, better way would be:

function ValidateForm(){
    // do your logic
    if (pass) {
        return $.post("urlToPost");
    else {
        return $.Deferred().reject(xxx).promise();
    }
}

more info on this, checkout https://github.com/petkaantonov/bluebird/wiki/Promise-anti-patterns

original answer

I will try picture your problem:

what you want is run ValidateForm first, when it passess (such that when form post is done), then do MoveNext

your problem is, MoveNext is called before ValidateForm has finished.

If I am correct, then the reason is because Javascript by nature is async programing, hence use callback to fire when something has done.

So in your situation, you need to use promise to acheive it.

essentialy what you need to do is:

  1. let ValiateForm returns promise
  2. only execute MoveNext once the ValidateForm has finish

So code would be like:

function ValiateForm(){
    var dfd = jQuery.Deferred();

    // do your logic
    if(pass){
        $.post("urlToPost"
    } else {
        // also failed
        dfd.reject();
    }

    // return the promise
    return dfd.promise();
}

then your move next button would be like

$('button').on('click', function () { 
        if (this.id === "btnMoveToNextTab") {

            $.when(window.ValidateForm(true)).then(function(){
            // after success post form
            $.ajax({
                        url: url,
                        context: { param: 'next' },
                        method: "GET",
                        data: data,
                        success: function(response) {
                            if (typeof response == 'object') {
                                if (response.moveAhead) {
                                    MoveNext();
                                }
                            } else {
                                $('#mainView').html(response);
                            }
                            ScrollUp(0);
                        }
                    });
            if (this.id === "btnMoveToPreviousTab") {
                MoveBack();
            }
        } 
        return false;
        });                


});

more info on promise checkout

https://api.jquery.com/deferred.promise/

and for run when finish

https://api.jquery.com/deferred.then/

Alan Tsai
  • 2,465
  • 1
  • 13
  • 16
  • Thanks a lot @Alan - I'll give this a go and let you know! – Johny Mar 24 '17 at 12:39
  • thanks again @Alan. I finally figured out everything, ran into a little problem with deferred implementation but its taken care of as well. http://stackoverflow.com/questions/43008781/stop-event-from-continuing-on-deferred-reject Thanks. Happy coding :) – Johny Mar 24 '17 at 23:20
  • The first code block here is a [promise anti-pattern](https://github.com/petkaantonov/bluebird/wiki/Promise-anti-patterns). Since `$.post()` already returns a promise, there is no need to wrap another promise/deferred around it. You can just return the promise that `$.post()` already returns without creating your own promise. – jfriend00 Mar 25 '17 at 00:18
  • @jfriend00 thanks for the info. Just want some further clarification about best practise. Because the OP mention it will validate form and post - so my understanding is it may fail the client side validation before posting - hence the promise rejecting `if(pass)` fail. So it is not possible just return the promise from the `$.post()`, because it may not have happended. In this situation is it still anti-pattern? Or what would be better way to achieve this? Thanks again for pointing it out, just want to make sure get it right before edit my answer for future reference – Alan Tsai Mar 25 '17 at 00:37
  • I posted a way to avoid the anti-pattern in this case as another answer. – jfriend00 Mar 25 '17 at 00:43