How to submit an ASP.NET Core Form via jquery

In this article let's look at how we can create a simple form and POST data to an AspNetCore API via jQuery. To style this application, we'll use Bootstrap, which is a popular CSS framework for desiging responsive web pages. We'll also make use of Tag Helpers, which make binding the back-end controller attributes - such as route urls in the views easy.

In this article let’s look at how we can create a simple form and POST data to an AspNetCore API via jQuery. To style this application, we’ll use Bootstrap, which is a popular CSS framework for desiging responsive web pages. We’ll also make use of Tag Helpers, which make binding the back-end controller attributes – such as route urls in the views easy.

For our example, let’s make a ReaderStore portal where one can add a new Reader entry into the ReaderStore. We’ll have a roaster of all submitted Readers along with their details in a grid.

To add a reader to the ReaderStore, we shall create a ReaderRequestModel which pass submitted Reader data to the Controller. The ReaderRequestModel model also extends a few Response properties which are later used by the View for handling the result.

namespace ReadersMvcApp.Models
{
    public class ReaderResponseModel
    {
        public bool IsSuccess { get; set; }
        public string ReaderId { get; set; }
    }

    public class ReaderRequestModel 
        : ReaderResponseModel
    {
        [Required]
        [StringLength(200)]
        public string Name { get; set; }

        [Required]
        [EmailAddress]
        [StringLength(250)]
        public string EmailAddress { get; set; }
    }
}

While the Name and EmailAddress fields are input attributes to the controller from the View, the result of the operation is decided by the IsSuccess and ReaderId attributes which are set once the Reader is added onto the ReaderStore.

Next, we’ll develop a form which accommodates these attributes along with their validation responses for any erroneous input.

@model ReadersMvcApp.Models.ReaderRequestModel
<div class="container-fluid">
    <form asp-controller="Readers" asp-action="New" class="form">
        <div class="form-group">
            <label asp-for="Name"></label>
            <input asp-for="Name" class="form-control" />
            <span class="text-danger" asp-validation-for="Name"></span>
        </div>
        <div class="form-group">
            <label asp-for="EmailAddress"></label>
            <input asp-for="EmailAddress" type="email" class="form-control" />
            <span class="text-danger" asp-validation-for="EmailAddress"></span>
        </div>
        <button class="btn btn-primary" type="submit">Submit</button>
    </form>
</div>

Observe that we used asp-* tags to specify the action and for scaffolding the model attributes which are to be passed onto the specified endpoints. These are called as taghelpers which come along with the aspnetcore MVC template and which help in making things easier for us while POSTing data to the controller from the form.

In general scenarios, to POST form to a controller using a model we would need to ensure that the property names in the model match with the “name” and “id” properties of the HTML fields, otherwise these values shall not be captured and assigned to the model object. Tag Helpers solve this problem by creating a scaffold between the HTML inputs we use and the intended Model property to be binded. And they take care of the assignment part themselves.

<input asp-for="Name" class="form-control" />

actually translates to,

<input 
  class="form-control" 
  type="text" 
  data-val="true" 
  data-val-length="The field Name must be a string with a maximum length of 200." 
  data-val-length-max="200" 
  data-val-required="The Name field is required." 
  id="Name" 
  maxlength="200" 
  name="Name" 
  value="">

Where the maxlength, required attributes are dynamically scaffolded based on how we have defined the property Name inside our Model.

[Required]
[StringLength(200)]
public string Name { get; set; }

Same is the case with the asp-controller and asp-action taghelpers, together they form the “action” HTML attribute of the form tag. This ensures that the correct action is attached to the form, no matter what route the controller or the action is actually mapped to.

Let’s add the necessary jQuery logic to post this form. Now this is done in two ways:

  1. Let the MVC trigger a generic submit from inside the customized jquery file included in the boilerplate and is POSTed to the API directly.
  2. Override the generic submit behavior by adding our own event handler and handle the request response by ourselves.

In our case, we shall override the generic submit behavior with our own event handler that is triggered when the form is submitted.

Why to override the default Submit?

Let’s say we don’t write any frontend handler ourselves and click on submit, still the data is submitted to the controller with the model data, but we can’t add any customization to this (for example adding spinners, loading popups for failure handling and such) because the default jQuery files which come bundled within the MVC template handle this HTTP request for us and they can’t be changed. For this, we override the behavior by adding our own jQuery submit event handler and then POSTing the form ourselves. This is done as below:

$(document).ready(function () {
   
    $("form")
        .submit(function (event) {
            debugger;
            event.preventDefault();
            
            // fetch the form object
            $f = $(event.currentTarget);

            // check if form is valid
            if ($f.valid()) {
                $("div.loader").show();
                // fetch the action and method
                var url = $f.attr("action");
                var method = $f.attr("method");

                if (method.toUpperCase() === "POST") {

                    // prepare the FORM data to POST
                    var data = new FormData(this);

                    // ajax POST
                    $.ajax({
                        url: url,
                        method: "POST",
                        data: data,
                        processData: false,
                        contentType: false,
                        success: handleResponse,
                        error: handleError,
                        complete: function (jqXHR, status) {
                            console.log(jqXHR);
                            console.log(status);
                            $f.trigger('reset');
                        }
                    });
                }
            }
        });

        function handleResponse(res) {
        debugger;
        $("div.loader").hide();

        // check if isSuccess from Response
        // is False or Not set
        if (!res.isSuccess) {
            debugger;
            // handle unsuccessful scenario
        } else {
            debugger;
            // handle successful scenario
            showSuccessMessage();
        }
    }

    function handleError(err) {
        $("div.loader").hide();
        if (xhr.responseText)
            showErrorMessage(xhr.responseText);
        else
            showErrorMessage("Error has occured. Please try again later.");
    }

    function showErrorMessage(message) {
        debugger;
        // show a popup logic or an alert logic
        var popup = $('#errorAlert');
        popup.removeClass('d-none');
        setTimeout(() => {
            popup.addClass('d-none');
        }, 5000);
    }

    function showSuccessMessage(message) {
        debugger;
        // show a popup logic or an alert logic
        var popup = $('#successAlert');
        popup.text(message);
        popup.removeClass('d-none');
        setTimeout(() => {
            popup.addClass('d-none');
        }, 5000);
    }
});

Here we use the “submit” event handler on the “form” element and do the needful ourselves, in addition we can add customization like adding a spinner (<div.loader> tag) for all the time the form is being processed, or handle the success or failure cases better. In any case, this provides us with better control over the form than not using this. This entire logic resides under site.js under the wwwroot folder for better caching and bundling.

And finally the controller logic is as below:

namespace ReadersMvcApp.Controllers
{
    public class ReadersController : Controller
    {
        private readonly IReaderRepo _repo;
        private readonly IHostEnvironment _env;

        // default constructor
        // where any instance level
        // assignments happen
        public ReadersController(IReaderRepo repo, 
            IHostEnvironment environment)
        {
            _repo = repo;
            _env = environment;
        }

        // The Grid View of
        // all the Readers in the Store
        public IActionResult Index()
        {
            return View(_repo.Readers);
        }

        // default GET Endpoint which
        // renders the View for us
        // from ~/Views/Readers/New.cshtml
        public IActionResult New()
        {
            return View();
        }

        // default POST Endpoint which
        // receives data from the Form submit
        // at ~/Views/Readers/New.cshtml
        // and returns the response to
        // the same View
        [HttpPost]
        public async _Task<IActionResult>_ New([FromBody]ReaderModel model)
        {
            var res = new ReaderResponseModel();

            // magic happens here
            // check if model is not empty
            if (model != null)
            {
                // create new entity
                var reader = new Reader();

                // add non-file attributes
                reader.Name = model.Name;
                reader.EmailAddress = model.EmailAddress;

                // add the created entity to the datastore
                // using a Repository class IReadersRepository
                // which is registered as a Scoped Service
                // in Startup.cs
                var created = _repo.AddReader(reader);

                // Set the Success flag and generated details
                // to show in the View 
                res.IsSuccess = true;
                res.ReaderId = created.Id.ToString();
            }

            // return the model back to view
            // with added changes and flags
            return Json(res);
	    } 
	}
}

Observe that we use [FromBody] attribute for the model to be captured, since we are using $.ajax which pushes data inthe form of content “application/json” we would require this attribute to state that the request content is coming in the body section, to be processed by the controller. And once the model is received, the magic happens within the controller. otherwise, the controller expects the data be sent in the default contentType of the form (form-url-encoded) and so the data passed from jQuery is not received resulting in a 415 – ContentType not Supported response.

Finally the Index page contains all the Readers submitted in the store along with their details, bound in a GridView which is as follows:

@model IEnumerable<ReaderStore.WebApp.Models.Entities.Reader>
<table class="table">
    <tr>
        <th>Id</th>
        <th>Name</th>
        <th>EmailAddress</th>
        <th>Work</th>
        <th>Added On</th>
    </tr>
    @foreach (var r in Model)
    {
        <tr>
            <td>
            <a href="@Url.Action("Index", "Readers", new { id = r.Id })">
                @r.Id</a>
            </td>
            <td>@r.Name</td>
            <td>@r.EmailAddress</td>
            <td>@r.AddedOn</td>
        </tr>
    }
</table>

In this way, we can implement a simple POST form and submit it to an ASP.NET Core MVC Controller using jQuery and TagHelpers complemented by the BootStrap form classes.


Buy Me A Coffee

Found this article helpful? Please consider supporting!

Ram
Ram

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity. You can connect with me on Medium, Twitter or LinkedIn.

Leave a Reply

Your email address will not be published. Required fields are marked *