7

In an MVC3 app, using jquery unobtrusive validation, and a view/model with a [Remote] validator: I am trying to disable the submit button and display a wait icon during remote validation, and when a valid form is submitted to the server. I thought I had it nailed until I tried it in IE8.

The problem was, GC and FF were not firing the form's submit event when the form was invalid, so I just disabled the submit button during this event. However IE8 is firing this event when the form is invalid, causing the user to never be able to click it again. (IE8 does not submit the form, but the event is fired.)

I tried attaching a function to the submit button's click event. In there, I disabled the submit button, showed the wait icon, and had this:

$('[data-app-form-submit-button="true"]').live('click', function (e) {
    var form = $(this).parents('form');
    var icon = form.find('[data-app-form-submitting-icon="true"]');
    icon.show();
    $(this).attr('disabled', 'disabled');
    $.ajaxSetup({ async: false });
    var isValid = form.valid();
    $.ajaxSetup({ async: true });
    alert(isValid);
});

The problem is, the ajax setup call is not really turning off async. It does if I move it out of the click function, but then it disables async for everything. Instead, the page alerts "true" immediately, tested by setting a breakpoint on the remote validation action method.

Any ideas?

Additional Note:

I forgot to mention, in IE8, the submit event is only being fired when the text box in question fails a validation that can happen on the client. For example, if it fails required or regex, submit() is fired. For the remote validation action method, it is not fired. However, as soon as it fails a client validation, subsequent remote validations also trigger the IE8 submit event.

Response to Russ Cam (comment #1)

Here is the relevant code in the viewmodel:

public class SignUpForm : IValidatableObject
{
    [DataType(DataType.EmailAddress)]
    [Display(Name = "Email Address")]
    [Required(ErrorMessage = "Email Address is required.")]
    [RegularExpression(@"^(email regex here)$",
        ErrorMessage = "This is not a valid email address.")]
    [Remote("Validate", "ControllerName", "AreaName", HttpMethod = "POST")]
    public string EmailAddress { get; set; }

    public IEnumerable<ValidationResult> Validate(
        ValidationContext validationContext)
    {

I'm glad you had me look at the rendered <form>. The form tag and input elements look like this:

<form action="/post-action-method" method="post" novalidate="novalidate">
...
<input class="text-box single-line" data-app-focus="true" data-val="true" 
    data-val-regex="This is not a valid email address." 
    data-val-regex-pattern="^(email regex here)$" 
    data-val-remote="&amp;#39;Email Address&amp;#39; is invalid." 
    data-val-remote-additionalfields="*.EmailAddress" 
    data-val-remote-type="POST" 
    data-val-remote-url="/validate-action-method" 
    data-val-required="Email Address is required." 
    id="EmailAddress" name="EmailAddress" type="text" value=""> 
 ... 
<input type="submit" value="Submit this form" 
    data-app-form-submit-button="true" />

I never saw the novalidate="novalidate" attribute until now. Here is what it looks like in the cshtml file:

@using (Html.BeginForm())
{
    @Html.EditorForModel()
    @Html.AntiForgeryToken("assault")
}

I am also using anti-forgery token, if that makes a difference. Thanks.

danludwig
  • 45,241
  • 21
  • 150
  • 230
  • would you be able to also show the viewmodel code and the `
    ` from the view?
    – Russ Cam Oct 20 '11 at 18:56
  • Posted the viewmodel and rendered html, along with cshtml source. Is it jquery validate or the unobtrusive adapters that could be adding the novalidate attribute? – danludwig Oct 21 '11 at 20:41
  • I never saw novalidate before. It does not exist in the MVC 3 source code. What other packages have you installed? – counsellorben Oct 21 '11 at 21:13
  • Yeah, I'm seeing it in all of my forms. Maybe I should start a new question. Packages in MVC app: AutoMapper, CommonServiceLocator, ELMAH Core Library, Elmah.Contrib.Mvc, EF4.1, jQuery, jq UI, jq Validation, jq Visual Studio 2010 Intellisense, MicrosoftWebMvc, Modernizr, Mvc Html5 Templates, Mvc2Futures, MvcScaffolding, T4MVC, T4Scaffolding, Unity, Windows Azure AppFabric Caching, Windows Azure Web Role. – danludwig Oct 23 '11 at 13:07
  • Found it, jquery.validate 1.9.0. Just upgraded. From jquery.validate.js, lines 32 & 33: // Add novalidate tag if HTML5. this.attr('novalidate', 'novalidate'); – danludwig Oct 24 '11 at 05:12

5 Answers5

3

It's kind of hacky, but I would suggest trying the following.

First, hide the submit button with display="none", and show your own "submit" button, which runs your script above.

Second, in your page, add a flag var [var remotePending = false;] and a variable for a setInterval call [var intervalPending;], and in your script, set the flag to true, then call the following function using intervalPending = setInterval('remoteCheck()', 200);

function remoteCheck() {
    if ($.validator.pendingRequest == 0) {
        // requests are done
        // clear interval
        clearInterval(intervalPending);
        // re-enable our "submit" button

        // "click" the hidden button
        $("#hiddenSubmit").click();
    }
    // we will try again after the interval passes
}

This will allow you to wait for the completion of the pending remote validation, then proceed normally. I have not tested, so you may have to play around to get this working as you want.

counsellorben
  • 10,704
  • 3
  • 37
  • 38
  • Oooh, I don't like interval. Don't mind settimeout as much, which i ended up using. Thanks for this though, I may try it later. – danludwig Oct 21 '11 at 20:17
  • when i try to do this i get an undefined on the pendingRequets? –  Nov 07 '11 at 10:37
2

I'm a bit late to the table with this, but I stumbled across this question while trying to solve a similar problem. I too didn't want to use Interval, so I cooked up a different solution and thought it might be worth detailing here for others.

In the end I decided to override the jQuery validator startRequest and stopRequest methods in order to add my own logic. After executing my own logic I simply call the old validator methods.

// Extend jQuery validator to show loading gif when making remote requests
var oldStartRequest = $.validator.prototype.startRequest;
$.validator.prototype.startRequest = function (element) {
    var container = $('form').find("[data-valmsg-for='" + element.name + "']");
    container.addClass('loading');       

    oldStartRequest.apply(this, arguments);
};

var oldStopRequest = $.validator.prototype.stopRequest;
$.validator.prototype.stopRequest = function (element) {
    var container = $('form').find("[data-valmsg-for='" + element.name + "']");
    container.removeClass('loading');

    oldStopRequest.apply(this, arguments);
};

All I wanted to do for my purposes was simply add a 'loading' css class to an adjacent validation message field in order to show a loading gif, but you could easily extend this to enable/disable a button, etc.

Matt B
  • 7,624
  • 2
  • 39
  • 57
1

I found myself puzzling over this last night and after trying various approaches I ended up with an evolution of counsellorbens. Since counsellorbens didn't work straight out of the box I thought I'd share it to prevent others having the same headache. It's not as "clean" a solution as I would like (I really hate using intervals). But it seems to suffice.

var iIntervalId = null;

//
// DECLARE FUNCTION EXPRESSIONS
//

//==============================================================================
// function that triggers update when remote validation completes successfully
//==============================================================================
var fnPendingValidationComplete = function () {

  var validator = $("#frmProductType").data("validator");
  if (validator.pendingRequest === 0) {

    clearInterval(iIntervalId);

    //Force validation to present to user (this will not retrigger remote validation)
    if ($("#frmProductType").valid()) {

      alert("valid - you can submit now if you like");

    }
    //else { alert("invalid"); }
  }
  //else { alert("NOT YET"); }
},

//==============================================================================
// Trigger validation
//==============================================================================
fnTriggerValidation = function (evt) {

  //Remove any cached values to ensure that remote validation is retriggered
  $("#txtProductType").removeData("previousValue");

  //Trigger validation
  $("#frmProductType").valid();

  //Setup interval which will evaluate validation (this approach because of remote validation)
  iIntervalId = setInterval(fnPendingValidationComplete, 50);
};

fnTriggerValidation();
John Reilly
  • 4,594
  • 4
  • 32
  • 52
0

I went through all the approaches, all were fine but nothing worked me. Then I check the version of System.Web.Mvc of model(class or DTO) project and and View's Project there was version difference. I update the both to same version and it worked for me.

I check the html generate was missing data-val-remote-additionalfields=".Email" e.g I was checking them.

sami ullah
  • 446
  • 6
  • 11
0

I ended up solving this, but the button disable / wait image show only happen when there is a valid submitted form. It's okay though, since multiple invalid form submissions are idempotent, the user can click as much as they want. (Also, the remote validation action method is cached against its parameter values.)

$(function () {
    $('form').bind('invalid-form.validate', function () {
        $('form').each(function () {
            $(this).find(
                '[data-app-form-submitting-icon="true"]').hide();
            var button = $(this).find(
                'input[type="submit"][data-app-form-submit-button="true"]');
            setTimeout(function () {
                button.removeAttr('disabled');
            }, 1);
        });
    });
    $('form').live('submit', function (e) {
        $(this).find('[data-app-form-submitting-icon="true"]').show();
        var button = $(this).find(
            'input[type="submit"][data-app-form-submit-button="true"]');
        setTimeout(function () {
            button.attr('disabled', 'disabled');
        }, 0);
    });
});

This is a little hacky too, but when I didn't use setTimeout, the submit action was never firing the invalid handler.

danludwig
  • 45,241
  • 21
  • 150
  • 230
  • Now that I re-read this, I realize it will break for forms loaded via ajax. The submit will disable, but the invalid handler will never re-enable. Will have to try counsellorben's hack... – danludwig Oct 21 '11 at 20:46