ryanelian / aspnet-validation

Enables ASP.NET MVC client-side validation without jQuery!
MIT License
40 stars 13 forks source link

Prevent submit if invalid rather than call submit if valid #2

Open jlarc opened 7 years ago

jlarc commented 7 years ago

This allows us to layer on Progressive Enhancement if we want to use js and fetch for example for async form posts.

ryanelian commented 7 years ago

Thank you for this pull request.

Unfortunately, I have tried that exact technique which you posted in the past. I'll explain briefly the problem in your solution:

Why a Promise Validator?

https://github.com/ryanelian/aspnet-validation/blob/master/src/index.ts#L725

If you noticed, an input validator is a boolean Promise, an object which resolves asynchronously just like a C# Task<bool>.

"Why bother making a Promise for an input validator? Why can't you just do the good-old-fashioned synchronous validation for inputs one-by one?"

Well, it's due to the existence of ASP.NET Core MVC Remote Validation, which fires an HTTP request to an API for server-side validation. It is asynchronous by nature.

Starting with Gecko 30.0 (Firefox 30.0 / Thunderbird 30.0 / SeaMonkey 2.27), synchronous requests on the main thread have been deprecated due to the negative effects to the user experience. https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Synchronous_and_Asynchronous_Requests#Example_HTTP_synchronous_request

The Problem

By now you should already have an idea why I decided to first prevent the default submit event then submit later if indeed the inputs are valid. But I'll explain anyway:

Unfortunately, addEventListener for DOM Element is blissfully unaware of the asynchronous world. If you fire asynchronous validations for all inputs on form submission event, this happens:

  1. Our form validation callback function gets called.
  2. Our form validation callback function kickstarts the validations of all inputs within, also as a boolean Promise.
  3. Remember, JavaScript is a single-threaded beast. By now all synchronous validations for all inputs should have already resolved.
  4. Form submit event propagates to the browser default event. Form gets submitted!
  5. Later, if ever, the asynchronous validations completed after the form has been submitted.

Hence we are unable to stop the form from submitting if an asynchronous validation failed. 💀

The Solution

  1. Kickstart the validations of all inputs within the form as a boolean Promise.
  2. Stop the default form submit event.
  3. Resolve the form validations Promise... If successful (after an indeterminate amount of time), actually submit the form this time.

Scenario

I'm sorry, your PR bugged when this was attempted (I tried your code to be sure):

Try inputting whatever then click the submit button.

Test.cshtml.cs

public class TestModel : PageModel
{
    [Required]
    [StringLength(255)]
    [Remote("Get", "TestApi")]
    public string Test { set; get; }

    public void OnGet()
    {
    }
}

Test.cshtml

@page
@model TestModel
@{
}

<h3>Test</h3>
<form method="post">
    <div class="form-group">
        <label asp-for="Test"></label>
        <input asp-for="Test" class="form-control" />
        <span asp-validation-for="Test"></span>
    </div>
    <div class="form-group">
        <button type="submit" class="btn btn-primary">Submit</button>
    </div>
</form>

TestApiController.cs

[Produces("application/json")]
[Route("api/v1/test")]
public class TestApiController : Controller
{
    [HttpGet]
    public IActionResult Get(string test)
    {
        if (test == "jono")
        {
            return Ok(true);
        }
        else
        {
            return Ok("Input value must be 'jono'!");
        }
    }
}

That said, I am still willing to help you with this issue. Please describe in more detail what you're trying to solve. I'm open for discussion ☕️