Search code examples
c#asp.net-mvc-4bootstrap-4razor-2

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


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();
    }
}

Solution

  • 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