ASP.NET Core ajax modals with validation using Bootstrap

ASP.NET Core ajax modals with validation using Bootstrap

In this tutorial I’m going to show you how to load modals dynamically and how to process forms (within modals) via ajax requests.

I’m going to use Bootstrap for modals, because it’s pretty popular, but you may as well use any other UI framework or library for displaying modals.

We’ll use jQuery to send ajax and perform modal actions.

For the backend, we’ll use .NET Core MVC, but you can replace it with your technology of choice. The keypoint here for you is to find a way to return partial view (that is only HTML of a modal, without the whole layout) and to validate data.

You can find whole solution in my GitHub respository.

Setup

Firstly we need to create a project. Go ahead and launch your Visual Studio (I’m using Visual Studio Community 2017). Create new “ASP.NET Core Web Application”. I named my Solution/Project “AjaxModals”.

ASP.NET Core new Web application screenshot

Then select “Web Application (Model-View-Controller)”.

Web Application (Model-View-Controller) screenshot

This project inlcudes jQuery and Bootstrap already, but they are old versions. We’re going to replace it with newer version. We’ll go the simplest path which is to use a CDN. I’m also going to clean it up, so we’ll remove all unnecessary markup code. Firstly let cleanup main layout file and link required files via CDN. Open Views\Shared\_Layout.cshtml and replace its contents with:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - AjaxModals</title>

    <link 
          rel="stylesheet" 
          href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" 
          integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" 
          crossorigin="anonymous">
    <environment include="Development">
        <link rel="stylesheet" href="~/css/site.css" />
    </environment>
    <environment exclude="Development">
        <link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
    </environment>
</head>
<body>
    <div class="container pt-5">
        @RenderBody()
    </div>

    <script 
            src="https://code.jquery.com/jquery-3.3.1.slim.min.js" 
            integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" 
            crossorigin="anonymous"></script>
    <script 
            src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.0/umd/popper.min.js" 
            integrity="sha384-cs/chFZiN24E4KMATLdqdvsezGxaGsi4hLGOzlXwp5UZB1LY//20VyM2taTB4QvJ" 
            crossorigin="anonymous"></script>
    <script 
            src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js" 
            integrity="sha384-uefMccjFJAIv6A+rW+L4AHf99KvxDjWSu1z9VI8SKNVmz4sk7buKt/6v9KI65qnm" 
            crossorigin="anonymous"></script>
    <environment include="Development">
        <script src="~/js/site.js" asp-append-version="true" defer></script>
    </environment>
    <environment exclude="Development">
        <script src="~/js/site.min.js" asp-append-version="true" defer></script>
    </environment>

    @RenderSection("Scripts", required: false)
</body>
</html>

In this tutorial I’m not going to show you how to do client-side validation, we’ll only do server side validation. Nevertheless let us also update client-side validation scripts. By default client-side validation is based on jQuery unobtrusive validation. Go to Views\Shared\_ValidationScriptsPartial.cshtml and replace it with:

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.17.0/jquery.validate.min.js"
        integrity="sha256-F6h55Qw6sweK+t7SiOJX+2bpSAa3b/fnlrVCJvmEj1A="
        crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.9/jquery.validate.unobtrusive.min.js"
        integrity="sha256-paoxpct33kY9fpQlvgC8IokjGq8iqYaVFawgFPg8oz0="
        crossorigin="anonymous"></script>

Go to “Views\Home” and remove everything except Index.cshtml. Remove Index.cshtml file contents (but leave the file, we’ll need it).

Good, now we’re going to do some code cleanup. Open Controllers\HomeController.cs and remove About() and Contact() functions.

Overview

Here’s how it’s going to work.

Action plan for Ajax based Bootstrap modals using ASP.NET Core infographics

Creating a modal

It’s time to create our modal. We’ll keep it simple for now. It will include a basic contact form with three input fields: first name, last name and email.

Let’s start by creating a new model (Models\Contact.cs) with following contents:

public class Contact
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
}

We’ll also need an instance of this model inside our view (for now). Go to HomeController and modify your Index, so it will pass an instance of Contact into the view.

Let’s add a “Add contact” button which will open our modal window. I’m not going to dive into the html/markup here, if you’re interested go to bootstrap documentation here. Notice that there is a proper form inside and that we’re using tag helpers to create inputs.

@model Contact
<!-- Button trigger modal -->
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#add-contact">
    Add contact
</button>

<!-- Modal -->
<div class="modal fade" id="add-contact" tabindex="-1" role="dialog" aria-labelledby="addContactLabel" aria-hidden="true">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="addContactLabel">Add contact</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">×</span>
                </button>
            </div>
            <div class="modal-body">
                <form>
                    <div class="form-group">
                        <label asp-for="FirstName"></label>
                        <input asp-for="FirstName" class="form-control" />
                        <span asp-validation-for="FirstName" class="text-danger"></span>
                    </div>
                    <div class="form-group">
                        <label asp-for="LastName"></label>
                        <input asp-for="LastName" class="form-control" />
                        <span asp-validation-for="LastName" class="text-danger"></span>
                    </div>
                    <div class="form-group">
                        <label asp-for="Email"></label>
                        <input asp-for="Email" class="form-control" />
                        <span asp-validation-for="Email" class="text-danger"></span>
                    </div>
                </form>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                <button type="button" class="btn btn-primary">Save</button>
            </div>
        </div>
    </div>
</div>

Loading modal form data via ajax

The modal we have right now is static. What we want however is to load the modal dynamically. Why? We could use it to edit existing contact. So we want it to be able to preload with given contact data.

We need a couple of steps to achieve this. Firstly we need to return a modal contents from controller. We’ll use PartialView to return only contents of given view, without the layout. To do this we’re going to create new method Contact and we’re going to move model creation from Index to it. Since we’re going to name the view a little bit different we need to pass it to the PartialView method:

public IActionResult Contact()
{
    var model = new Contact { };

    return PartialView("_ContactModalPartial", model);
}

Then we’re going to move modal markup into separete view file Home\_ContactModalPartial.cshtml:

@model Contact
<!-- Modal -->
<div class="modal fade" id="add-contact" tabindex="-1" role="dialog" aria-labelledby="addContactLabel" aria-hidden="true">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="addContactLabel">Add contact</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">×</span>
                </button>
            </div>
            <div class="modal-body">
                <form>
                    <div class="form-group">
                        <label asp-for="FirstName"></label>
                        <input asp-for="FirstName" class="form-control" />
                        <span asp-validation-for="FirstName" class="text-danger"></span>
                    </div>
                    <div class="form-group">
                        <label asp-for="LastName"></label>
                        <input asp-for="LastName" class="form-control" />
                        <span asp-validation-for="LastName" class="text-danger"></span>
                    </div>
                    <div class="form-group">
                        <label asp-for="Email"></label>
                        <input asp-for="Email" class="form-control" />
                        <span asp-validation-for="Email" class="text-danger"></span>
                    </div>
                </form>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                <button type="button" class="btn btn-primary">Save</button>
            </div>
        </div>
    </div>
</div>

This is what is left in Index.cshtml file:

@model Contact
<!-- Button trigger modal -->
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#add-contact">
    Add contact
</button>

Alright, so we have separated the button (which opens the modal) which is in Index.cshtml view and the modal itself which can be found in _ContactModalPartial.cshtml view, also both are in separate actions (Index and Contact). Now we’re going to make it possible to display modal after button click.

OK, this is how we’re going to make it work. Button will have additional data attribute to tell us which action returns the modal view. We’ll also change data-toggle="modal" to data-toggle="ajax-modal", because we want to open it with our custom code (and load it via ajax).

<!-- Button trigger modal -->
<button type="button" class="btn btn-primary" data-toggle="ajax-modal" data-target="#add-contact" data-url="@Url.Action("Contact")">
    Add contact
</button>

Then we’re going to add some Javascript/jQuery code which will request (ajax) the modal view from controller and the we’re going to show it. Open up wwwroot\js\site.js file. Let’s start by adding some code to make sure that it’ll execute only after document.ready event:

$(function () {

});

Good now add event handler to each button which opens modal:

// all buttons with data-toggle equal to ajax-modal
$('button[data-toggle="ajax-modal"]').click(function (event) {

});

Inside the event handler we retrieve the url:

var url = $(this).data('url');

Then the ajax GET request receives the modal body from controller/action:

$.get(url).done(function (data) {

});

The last step is to display the modal. There’s one problem here. We have our modal HTML (received with $.get), but before we display it we need to attach it to the page DOM somewhere, to make it’s a part of page structure. Right now it’s just a variable in memory. In order to do that we’re going to create placeholder div, which will contain loaded modal markup code, so modify your Index.cshtml:

@model Contact
<!-- Modal placeholder -->
<div id="modal-placeholder"></div>

<!-- Button trigger modal -->
<button type="button" class="btn btn-primary" data-toggle="ajax-modal" data-target="#add-contact" data-url="@Url.Action("Contact")">
    Add contact
</button>

Then we’ll load it with modal and then we’re going to display it:

$('#modal-placeholder').html(data);
$('#modal-placeholder > .modal', data).modal('show');

Just to make sure, let’s see how our site.js file should look like by now:

$(function () {
    $('button[data-toggle="modal"]').click(function (event) {
        var url = $(this).data('url');
        $.get(url).done(function (data) {
            $('#modal-placeholder').html(data);
            $('#modal-placeholder > .modal').modal('show');
        });
    });
});

We have dynamically loaded modal. We’re done here.

Before we move on let’s refactor our JavaScript code a little bit. We can reuse reference to placeholder. It will be needed in next steps:

$(function () {
    var placeholderElement = $('#modal-placeholder');
    $('button[data-toggle="modal"]').click(function (event) {
        var url = $(this).data('url');
        $.get(url).done(function (data) {
            placeholderElement .html(data);
            placeholderElement.find('.modal').modal('show');
        });
    });
});

Saving modal form data via ajax

Our modal doesn’t do much yet. Our next step is to send form data to the controller. There are however two difficulties which we need to solve. First one is that we want to send it in the background (as an ajax request) and the second one is that the modal “Save” button is outside of a form tag. The good news is that it’s easy to do with jQuery.

Firstly we need an action to process data. It’s going to be an POST request which will send us contact data. Let’s add another method in HomeController.cs:

[HttpPost]
public IActionResult Contact(Contact model)
{
    return PartialView("_ContactModalPartial", model);
}

Nothing interesting here, for simplicity sake I’m not going to introduce ORM/DB here. We’ll just create a static list of contacts. Just to get it saved somewhere. Drop this line into your controller:

private readonly static List Contacts = new List();

Also we’ll need to add new contact once we receive it:

[HttpPost]
public IActionResult Contact(Contact model)
{
    Contacts.Add(model);

    return PartialView("_ContactModalPartial", model);
}

Now for the jQuery part. We need to attach click event handler to a button which will send whole form. Correct element to attach to is a “Save” button. We’ll identify it by an additional data-attribute (data-save="modal"). Once user clicks “Send”, it should either show errors or disappear in case of success.

// _ContactModalPartial.cshtml
// update form action to point to Contact
<form asp-action="Contact">
// add data attribute to save button
<button type="button" class="btn btn-primary" data-save="modal">Save</button>

We need to rememver that the whole modal is dynamic. This means that the every event handler is going to get removed after new HTML is loaded with ajax. There are two ways to fix this. First way is to always attach event handler upon ajax load and the second one is to use on jQuery function which allows us to attach event handler to element and filter the descendants of the element that will trigger the event. I’m going to take the second route.

// site.js
// attach click event handler to an element
// which is located inside #modal-placeholder
// and has data-save attribute equal to modal
placeholderElement.on('click', '[data-save="modal"]', function (event) {
    event.preventDefault();
});

What’s the next step? Well, we need to get form data and then send it. I’m not going to explain this step too much. We’re simply going to navigate DOM in order to find a form tag located inside the modal. Then we’ll extract form action url. We’re going to get form data and make it eligible for sending (serialization) then we’re going to send the data to server. Once we receive response we’re going to close the modal. This is how our code looks like now:

placeholderElement.on('click', '[data-save="modal"]', function (event) {
    event.preventDefault();

    var form = $(this).parents('.modal').find('form');
    var actionUrl = form.attr('action');
    var dataToSend = form.serialize();

    $.post(actionUrl, dataToSend).done(function (data) {
        placeholderElement.find('.modal').modal('hide');
    });
});

Now go ahead and check how this work. If you set breakpoint inside Contact (POST) you’ll see submitted values, you can as well verify that contact has been added into static list we’ve defined earlier.

Validating modal form data via ajax

It’s time to make sure that the data we’re processing in controller/action is valid. Unfortunately as it is for now, users can add new contact with empty name or empty/invalid email.

The easiest way to add validation to modal is data annotation attribute. If you never heard of those, then let me just tell you that data annotation attributes are speciall class which we can use to decorate properties. There are many built-in attributes, but we’ll only need two. Required attribute to make sure that first and last name are not empty and EmailAddress attribute to make sure that the email field has a valid email address.

public class Contact
{
    [Required]
    public string FirstName { get; set; }

    [Required]
    public string LastName { get; set; }

    [Required]
    [EmailAddress]
    public string Email { get; set; }
}

Valiation occurs automatically. In order to check validation state we use ModalState.IsValid property available in controller. Let’s make sure that new contact is saved only in case of a valid model:

[HttpPost]
public IActionResult Contact(Contact model)
{
    if (ModelState.IsValid)
    {
        Contacts.Add(model);
    }

    return PartialView("_ContactModalPartial", model);
}

You can verify that it’s working as expected by setting a breakpoint there.

We’ve got it checked. It also should show some errors in the front-end. Partially we alerady have that. Contact action already returns view with errors. Validation tag helper renders error messages. It is already included inside form:

// display validation errors (if any) for FirstName property
<span asp-validation-for="FirstName" class="text-danger"></span>

The only reason we’re not seeing those errors is that we’re not showing them. Once we receive response from server we’re simply closing the modal window. But leaving the modal panel open is not enough. We still need to copy modal body rendered by the server, because it contains error messages.

To sum it up. Once user presses Save button a request is made to HomeController/Contact action. Action validates it and if everything is OK saves it into static list. Then it returns a view (full modal HTML), including errors if validation failed. Then we receive response inside script. We need to extract modal body from the response and replace current modal body with the one from response.

$.post(actionUrl, dataToSend).done(function (data) {
    var newBody = $('.modal-body', data);
    placeholderElement.find('.modal-body').replaceWith(newBody);
});

Closing modal when it has been saved without errors

There is only one more step we need to do. At them moment modal will show errors if there were any validation error or empty modal on success. We want to make sure that the modal dissapears upon success.

Firstly we need to find a way to differentiate a modal which succeeded from the one that failed. Since we’re already processing modal HTML, we can also include a hidden input field which will tell us wheter ModelState is valid or not. The very same ModelState.IsValid property from controller can also be access inside view:

// _ContactModalPartial.cshtml
// add hidden input field to form
<form asp-action="Contact">
    <input name="IsValid" type="hidden" value="@ViewData.ModelState.IsValid.ToString()" />
    <div class="form-group">
        <label asp-for="FirstName"></label>
        <input asp-for="FirstName" class="form-control" />
        <span asp-validation-for="FirstName" class="text-danger"></span>
    </div>

The only thing that is left is to check the value of the hidden field. We need to do this once we receive modal:

// site.js
placeholderElement.on('click', '[data-save="modal"]', function (event) {
    event.preventDefault();

    var form = $(this).parents('.modal').find('form');
    var actionUrl = form.attr('action');
    var dataToSend = form.serialize();

    $.post(actionUrl, dataToSend).done(function (data) {
        var newBody = $('.modal-body', data);
        placeholderElement.find('.modal-body').replaceWith(newBody);

        // find IsValid input field and check it's value
        // if it's valid then hide modal window
        var isValid = newBody.find('[name="IsValid"]').val() == 'True';
        if (isValid) {
            placeholderElement.find('.modal').modal('hide');
        }
    });
});

It’s done! Check your solution. Now you know how to create dynamic Bootstrap modals using ASP.NET core. Check full source code here.

Bootstrap modal validation screenshot

Further improvements

You can do much more with modals. Check out the second part here if you want to learn how to:

  • upload files
  • display notifications
  • view added data
  • learn some more tricks