Bootstrap 4 modal popup button functionality prevented by clickable <tr> element

1k Views Asked by At

I have the following GUI for a simple inventory management application:

enter image description here

My problem is... The <tr> element's @Url.Action(...) is preventing the delete (trash) button from popping up a bootstrap modal. The page navigates away to the row items "Details" page. However, it appears the modal would popup if it didn't navigate away.

Table Row Markup:

<tr onclick="location.href = '@(Url.Action("Action", "Controller", new { id = item.ItemID }))'" class="inventory-row">
    ... <!-- Item Info -->
</tr>

Delete Button Markup:

@Html.ActionLink("<i class='fas fa-trash-alt'></i>", "#", null, new { @class = "btn btn-danger", @data_toggle = "modal", @data_target = "#DeleteConfirmationModal" })

Tried/Alt Delete Button Markup:

<button type="button" class="btn btn-danger" data-toggle="modal" data-target="#DeleteConfirmationModal">
    <i class="fas fa-pencil-alt"></i>
</button>

My goal is to popup a simple "Are you sure you want to delete item?" modal.

How can I get my delete button to popup the bootstrap modal while keeping the functionality that clicking on a table row goes to the items "Details" page?

I'd prefer to use razor syntax to accomplish this but you know the saying... at the end of the day I just need something that will work.

In the event it helps... this question is how I got the edit (pencil) button to function properly and this question is how I get the modal to pop up when the page doesn't navigate away.

EDIT:

I believe the edit (pencil) button only works because it navigates to the items edit page before the <tr> element has a chance to navigate to the details page. The delete (trash) button brings up a modal nested on the same page therefore allowing the <tr> element to navigate to the details page.

Do I need to reevaluate my interface design? or is it possible to have all but the last <td> element clickable effectively removing the conflict from the buttons?

EDIT 2:

Here is a link to the markup for the table: link

EDIT 3:

Delete method in controller:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id)
{
    try
    {
        Item.RemoveItem(id);
        return RedirectToAction("Index");
    }
    catch
    {
        return View();
    }
}
3

There are 3 best solutions below

13
StaticBeagle On BEST ANSWER

Since the trash button is inside the tr, one thing you can try is to handle the click event of the trash button itself and stop the event from propagating up. I'm assuming you are using bootstrap so the following solution is using jQuery. This should work for a button or ActionLink but usually when you use an <a>, you want to use that anchor's href as the placeholder for the data-target. I decided to use a button. So in you html

<button type="button" class="btn btn-danger" data-toggle="modal" data-target="#DeleteConfirmationModal">
    <i class="fas fa-trash-alt"></i>
</button>

In your javascript:

$(document).ready(function() {
  $(".btn.btn-danger").click(function(e) {
    e.stopPropagation()
    let target = $(this).data('target')
    $(target).modal('show')
  })
})

Here is a demo:
https://www.bootply.com/paKZLt8L94

How to implement this in MVC
Add an anti-forgery token to your view (Replace ControllerName below with your Controller)

// The purpose of this form is to create a Anti-forgery token
// needed by the controller. Insert this anywhere in your html
@using (Html.BeginForm("DeleteConfirmed", "ControllerName", FormMethod.Post, new { id = "delete-forklift-form" }))
{
    @Html.AntiForgeryToken()
    <input type="hidden" id="forklift-Id" name="id" value="" /> 
}

Add the modal markup somewhere in your view (at the end is ok)

<!-- Modal -->
<div class="modal fade" id="DeleteConfirmationModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title" id="exampleModalLabel">Modal title</h5>
        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">×</span>
        </button>
      </div>
      <div class="modal-body">
        Are you sure you want to delete this item?
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
        <button id="delete-confirm" type="button" class="btn btn-primary">Delete</button>
      </div>
    </div>
  </div>
</div>

Add the item ID as an data-attribute to your buttons:

// We need this ID to be passed to the delete action
// This is the foreach statement in you view from the pastebin link
@foreach(var item in Model.ForkliftStockInventoryList)
{
    // ... rest of the stuff
    <button type="button" class="btn btn-danger" data-fid="@item.ForkliftID" data-toggle="modal" data-target="#DeleteConfirmationModal">
        <i class="fas fa-trash-alt"></i>
    </button>  
}

Wrap your JavaScript in a Scripts block
Put the modified JavaScript code at the end of your view:

@section Scripts{
    <script>
        $(document).ready(function () {
            // handler for the trash button
            $(".btn.btn-danger").click(function (e) {

                e.stopPropagation()
                let target = $(this).data('target')
                let forkliftId = $(this).data('fid')
                // update the forklift to delete in the hidden field
                $('#forklift-Id').val(forkliftId)
                $(target).modal('show')
            })

            // handler for the delete confirmation on the modal
            $('#delete-confirm').click(function () {
                $('#delete-forklift-form').submit()
            })
        })
    </script>
}

In your _Layout view you are rendering jQuery after is being used that's why none of the jQuery stuff was working. Push @RenderSection("scripts", required: false) to the end so that all the scripts in the layout get rendered before the scripts in each view. It's very important that you wrap the <script>...</script> in the Forklift index view in a Scripts block (see above)

Change

 @RenderSection("scripts", required: false)
    @*<script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>*@
    <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.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T" crossorigin="anonymous"></script> 

To

 <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.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T" crossorigin="anonymous"></script>
    @RenderSection("scripts", required: false)
// RenderSection scripts at the end
2
Billy On

This markup would work but you need to provide that modal ID:

<button type="button" class="btn btn-danger" data-toggle="modal" data-target="#DeleteConfirmationModal">
    <i class="fas fa-pencil-alt"></i>
</button>

I'm not sure if you missed the ID by mistake...

EDIT:

I see, I think it's because you're using <button> and I've had the same issue.

Try this markup:

<a href="#" class="btn btn-danger" data-toggle="modal" data-target="#DeleteConfirmationModal">
    <i class="fas fa-pencil-alt"></i>
</a>

EDIT:

Oh I see what you mean, yes it's your <tr> event that fires. Add this to your button click event: event.stopPropagation();

Documentation: https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation

19
pool pro On

So looking at you code, Since you have an edit button you should ad a view button and remove the action link from the <tr> tag and move it to a button. this will stop the page from navigating away with the onclick event on the whole row. with the 3 buttons on the right users can know how to click rather than making the whole row clickable.

EDIT:

So i have set up the following as if it was MVC with jQuery loading after the footer, Had issues with loading the script in the page as it would error on the buttons with multiple rows. I have never been a fan of e.stopPropagation() as it has always caused other issues on pages but seams on this single page with no other jQuery being called it should work. here is a working example.

    <html>

    <head>
        <title>Parcel Sandbox</title>
        <meta charset="UTF-8" />
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
     crossorigin="anonymous">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp"
     crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css">
    </head>

    <body>
        <table>
            <tbody>
                <tr onclick="location.href = 'https://www.google.com'" class="inventory-row">
                    <td>col 1</td>
                    <td>col 2</td>
                    <td>col 3</td>
                    <td>col 4</td>
                    <td>
                        <button type="button" class="btn btn-warning" data-toggle="modal" data-target="#DeleteConfirmationModal">
            <i class="fa fa-trash"></i>
          </button>
                    </td>
                    <td>
                        <button type="button" class="btn btn-danger" data-toggle="modal" data-target="#DeleteConfirmationModal">
            <i class="fa fa-trash"></i>
          </button>
                    </td>
                </tr>
                <tr onclick="location.href = 'https://www.google.com'" class="inventory-row">
                    <td>col 1</td>
                    <td>col 2</td>
                    <td>col 3</td>
                    <td>col 4</td>
                    <td>
                        <button type="button" class="btn btn-warning" data-toggle="modal" data-target="#DeleteConfirmationModal">
            <i class="fa fa-trash"></i>
          </button>
                    </td>
                    <td>
                        <button type="button" class="btn btn-danger" data-toggle="modal" data-target="#DeleteConfirmationModal">
            <i class="fa fa-trash"></i>
          </button>
                    </td>
                </tr>
            </tbody>
        </table>

        <!-- Modal -->
        <div class="modal fade" id="DeleteConfirmationModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
            <div class="modal-dialog" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title" id="exampleModalLabel">Modal title</h5>
                        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
              <span aria-hidden="true">×</span>
            </button>
                    </div>
                    <div class="modal-body">
                        TEST TEXT...
                    </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 changes</button>
                    </div>
                </div>
            </div>


    <script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.3.1.min.js"></script>
            <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
             crossorigin="anonymous"></script>
            <script src="/src/index.js"></script>
    </body>

    </html>

For the script: place the script in its own file, here i have it in index.js. Include this in layout under jquery script load:

@RenderSection("Scripts",false/*required*/)

Include the file on the view:

@section Scripts
{
  <script src="@Url.Content("~/Scripts/index.js")"></script>
}

index.js:

$(".btn-warning").click(function (e) {
  e.stopPropagation()
  let target = $(this).data('target')
  $(target).modal('show')
})
$(".btn-danger").click(function (e) {
  e.stopPropagation()
  let target = $(this).data('target')
  $(target).modal('show')
})