ASP.NET Core ajax modals with validation using Bootstrap (Part 2)

ASP.NET Core ajax modals with validation using Bootstrap (Part 2)

In the second part of this tutorial we’ll add more ajax powered functionality to modals. You’re going to learn how to:

  • upload files via ajax
  • display notifications after modal data has been saved
  • view stored data in a table
  • ajax reload table after modal data has been saved
  • make sure that the modal opens when button is dynamically generated

First part of this tutorial can be found here. This tutorial relies on the first part, so I expect you to read it and code your solution before starting part two. All the sources for this tutorial can my found in my Github repository.

Uploading files via ajax

In its current state it’s impossible to upload files. Since uploading files is a likely scenario I’ll show you how to do that.

We start by adding a picture field for our contact:

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

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

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

    public IFormFile Picture { get; set; }
}

Then we modify modal view to show picture upload form:

// _ContactModalPartial.cshtml
<div class="form-group">
    <label asp-for="Picture"></label>
    <input asp-for="Picture" class="form-control" />
    <span asp-validation-for="Picture" class="text-danger"></span>
</div>

Right now our new field is visible, but it won’t be sent along other data. It’s caused by the way we sent it. We use $.post and serialize() jQuery methods, which won’t work with files (learn why here).

The serialize() method creates a URL-encoded string contain all fields, but the data from file inputs is omitted. We’ll replace it with FormData. It is a data structure which can be used to send whole forms using ajax. Most importantly, we can use it to send files.

// site.js

// previously
var dataToSend = form.serialize();

// now
// since form variable is a jQuery object 
// and FormData requires a HTML form element
// we retrieve first element in set using .get(0)
var dataToSend = new Form(form.get(0));

As stated above $.post won’t work, so replace it with $.ajax call:

// site.js

// previously
$.post(actionUrl, dataToSend)

// now
$.ajax({ url: actionUrl, method: 'post', data: dataToSend, processData: false, contentType: false })

That’s it! You can send files via ajax using Bootstrap modals.

Showing notification on success

User opens modal, fills in the form, clicks “Save” button, modal closes, but nothing happens. Page is not refreshed. There is no notification. User doesn’t have any clue wheter it worked or not. I’ll show you how to fix it.

There are a couple of ways we can go about this. I’m going to show you how to solve this by showing a notification. In terms of UX user will clearly see that the action succeeded.

Simple version – plain text

In cases like this I like to place empty element inside a view. It’s a placeholder which we will use to display the notification. It helps us control how and where it’s displayed.

Add following div to your layout:

// /Views/Shared/_Layout.cshtml
// add it just above @RenderBody()
<div id="notification"></div>

Next step is to insert message there. We’ll need to modify our JavaScript method responsible for closing modal on success. Let’s remind ourselves how this piece of code looks like:

// site.js
var isValid = newBody.find('[name="IsValid"]').val() == 'True';
if (isValid) {
    placeholderElement.find('.modal').modal('hide');
}

Let us just put some static text into notification area:

if (isValid) {
    $('#notification').text('Data saved successfully!');
    placeholderElement.find('.modal').modal('hide');
}

That’s it for the easy version. All you do is to put the text inside container. You can think of more complicated variations e.g. you can put messages into modal data attributes and retrieve it from these data attributes. You can also return messages from your action, so you could use response data to display appropriate mesage.

Text notification screenshot.

Also, if you want to you can also use HTML here, you only have to replace text() with html():

$('#notification').html('<h1>Data saved successfully!</h1>');

Advanced version – ajax notifications

This is a more complicated scenario, but also it allows us to do more in certain aspects. We’ll generate correct messages from within action. After that messages will be fetched by another ajax request. We are also going to display notifications using alert component available in Bootstrap.

We’ll start the same way as we did in “simple” chapter. Let’s add a placeholder element to layout.

// /Views/Shared/_Layout.cshtml
// add it just above @RenderBody()
<div id="notification"></div>

Now, we have to create notifications somewhere. As you read earlier we’ll make it happen inside action.

[HttpPost]
public IActionResult Contact(Contact model)
{
    if (ModelState.IsValid)
    {
        // TODO: success add new notification here
        Contacts.Add(model);
    }

    return PartialView("_ContactModalPartial", model);
}

Contact action returns PartialView. How/where can we store our message? The best way is the TempData dictionary. It’s like Session, but whatever is stored there disappears after you read it.

Because the data dissapears after it’s retreived we need to take care of possible nulls. However, we’ll make it a bit easier and more reusable by creating helper method:

// put this in HomeController.cs
[NonAction]
private void CreateNotification(string message)
{
    TempData.TryGetValue("Notifications", out object value);
    var notifications = value as List<string> ?? new List<string>();
    notifications.Add(message);
    TempData["Notifications"] = notifications;
}

Create notification on success:

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

    return PartialView("_ContactModalPartial", model);
}

Great. Now what? Well, we need to let our JavaScript code fetch these notifications somehow. Since we want to do that using ajax request, the best way would be to create another action. This new action would return all notifications. It’s rather simple, so there is nothing new to explain here:

// put this in HomeController.cs
public IActionResult Notifications()
{
    TempData.TryGetValue("Notifications", out object value);
    var notifications = value as IEnumerable<string> ?? Enumerable.Empty<string>();
    return PartialView("_NotificationsPartial", notifications);
}

As you see, we need to create new view file. It will render notifications. This time it won’t display simple plain text messages. We are going to create nice Bootstrap powered alerts:

@model IEnumerable<string>
@foreach (var notification in Model)
{
    <div class="alert alert-primary" role="alert">
        @notification
        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
            <span aria-hidden="true">×</span>
        </button>
    </div>
}

Next thing to do is to get those notifications once we close modal window. We need to modify our JavaScript code with new ajax request:

// site.js

var isValid = newBody.find('[name="IsValid"]').val() === 'True';
if (isValid) {
    $.get('/Home/Notifications').done(function (notifications) {
        $('#notification').html(notifications);
    });
    placeholderElement.find('.modal').modal('hide');
}

It works now, but our work is not done yet. Did you notice that I’ve hardcoded URL to notifications action inside site.js? It’s considered a bad practice. Let’s fix it.

We still need to put that url somewhere. Add data attribute to notifications placeholder element:

// /Views/Shared/_Layout.cshtml

<div id="notification" data-url="@Url.Action("Notifications", "Home")"></div>

Then we change our modal close function once again:

// site.js

var isValid = newBody.find('[name="IsValid"]').val() === 'True';
if (isValid) {
    var notificationsPlaceholder = $('#notification');
    var notificationsUrl = notificationsPlaceholder.data('url');
    $.get(notificationsUrl).done(function (notifications) {
        notificationsPlaceholder.html(notifications);
    });
    placeholderElement.find('.modal').modal('hide');
}

And now we’re done 🙂

Screenshot of an ajax notification produced using Bootstrap.

Reloading table using ajax

At this point we have a nice modal which saves the data and shows us notifications. Yet something feels missing. It would be even better if we could see new contact in a table once the data is saved.

Right now we don’t event display saved contacts. We see nothing even after we refresh our page. Firstly we need to modify Index action method to pass contacts list to view:

// HomeController.cs

public IActionResult Index()
{
    return View(Contacts);
}

Then, in order to display it, we modify the view:

@model IEnumerable<Contact>
@{  ViewData["Title"] = "Contact form"; }
<!-- 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>

<!-- Contacts table -->
<table class="table mt-5">
    <thead>
        <tr>
            <th>Name</th>
            <th>Email</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var contact in Model)
        {
        <tr>
            <td>@contact.FirstName @contact.LastName</td>
            <td>@contact.Email</td>
        </tr>
        }
    </tbody>
</table>

It’s good time for you to test it now. Run the app, add new contact and hit F5 to refresh the page.

Showing saved data in a table.

What we’re going to do now is to remove the need to refresh the page manually. We do this with another ajax request. It will get the rendered table. This works similary to notification (advanced version).

Since we’ll be replacing table HTML we set its id and we add data-url:

<table id="contacts" class="table mt-5" data-url="@Url.Action("Index")">

Did you notice that I used the Index action? Currently it returns whole page (much more than just the table). I’ll show you neat trick in a moment, but before we get to that let’s see the modifications for our JavaScript code. It looks very much like the notification code. The difference here is that we do not insert HTML into placeholder (html()), but instead we replace whole HTML element (replaceWith()):

// site.js

if (isValid) {
    var notificationsPlaceholder = $('#notification');
    var notificationsUrl = notificationsPlaceholder.data('url');
    $.get(notificationsUrl).done(function (notifications) {
        notificationsPlaceholder.html(notifications);
    });

    var tableElement = $('#contacts');
    var tableUrl = tableElement.data('url');
    $.get(tableUrl).done(function (table) {
        tableElement.replaceWith(table);
    });

    placeholderElement.find('.modal').modal('hide');
}

It works, but it’s glitchy. Reason for that is the Index method, which returns whole page instead of table.

It is possible to detect wheter the request is regular or ajax. We can use this knowledge to make Index return whole page or just the part of it (table). In case of ajax request we’ll return PartialView which will return only the table HTML.

Now it’s time I’ll show you the trick promised earlier. You can determine if request is sent via ajax by inspecting headers. Ajax requests send X-Requested-With header with value XMLHttpRequest:

// HomeController.cs

public IActionResult Index()
{
    var isAjax = Request.Headers["X-Requested-With"] == "XMLHttpRequest";
    if (isAjax)
    {
        return PartialView("_Table", Contacts);
    }

    return View(Contacts);
}

New partial view file consist of the table we already have in Index.cshtml:

@* /Views/Home/_Table.cshtml *@

@model IEnumerable<Contact>

<table id="contacts" class="table mt-5" data-url="@Url.Action("Index")">
    <thead>
        <tr>
            <th>Name</th>
            <th>Email</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var contact in Model)
        {
        <tr>
            <td>@contact.FirstName @contact.LastName</td>
            <td>@contact.Email</td>
        </tr>
        }
    </tbody>
</table>

Since this is the same table as the one in Index.cshtml, we can remove duplicated code by reusing partial view. Let’s render _Table.cshtml inside Index.cshtml:

@* /Views/Home/Index.cshtml *@

@model IEnumerable<Contact>
@{  ViewData["Title"] = "Contact form"; }
<!-- 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>

<!-- Contacts table -->
@await Html.PartialAsync("_Table", Model)

That’s all, you can now test it.

Opening modals with dynamically created buttons

At times we generate buttons dynamically. It can happen when you use some kind of front-end library or framework, maybe you create this button on your own somewhere in your front-end code. Currently our code doesn’t handle this scenario.

Reason for this is the event handler. It is attached on page load. So everything that happens after that time won’t have the click event required to open modal:

$('button[data-toggle="ajax-modal"]').click(function (event) {
    // ...
});

Fortunately the fix is easy. The on() function helps to set up event handler for an area (e.g. HTML element with dynamic content).

Let’s make sure our jQuery code finds all buttons within document on click event:

$(document).on('click', 'button[data-toggle="ajax-modal"]', function (event) {
    // ...
});

Instead of finding all elements matching critera upon document ready we set up a handler for all clicks on elements inside document.

Note that using on() on document is suboptimal. It runs the event handler every time user clicks. Ideally you would set it up on container with  dynamic elements.