The tried and true spreadsheet is the king of all business application user experiences. On the web, we’ve been able to recreate and dress-up the spreadsheet experience using HTML table tags. It’s a common design choice in many create, update, read, and delete (CRUD) apps to see a table being the anchor point to the user experience.

This post will show how we can take advantage of a little known HTML feature to reduce the complexity of your Razor views for folks building these pages. We’ll also be using ASP.NET tag helpers to leverage the full power of route handling.

The Problem

Imagine that we have an HTML table with many rows. Each row is associated with a data record and identified by an integer. We want the ability to delete each row by clicking a button contained in that row. We also remember that it’s always a good rule of thumb to use the POST HTTP method to initiate a destructive action.

HTML Razor Table with delete buttons

The HTML Button Trick

My previous understanding was that each row would require its own unique form tag, along with the necessary attributes and values needed to satisfy the endpoint handler.

<form id="remove" method="post" asp-page-handler="Remove">
  <button type="submit" name="id" value="@person.id">Remove</button>
</form>

Imagining a page with *100 rows, we can start to understand the overhead the requirement could add to our pages. A JavaScript library may look tempting, as we could hijack each button and make the request using some frontend programming.

I’m here to tell you to put away your JavaScript. We can achieve our goals by purely using HTML.

As you can see from the tweet, HTML button tags can submit any form found on the page using the form attribute. Like I said in a previous post, the HTML button is underappreciated.

The trick also works with Razor Pages! Let’s see it in action.

The Magic Of HTML Buttons and Forms

Our goal will be to add remove functionality to each row without bloating our HTML output. First, let’s look at our page model. I’ll be using Bogus to create a list of people. You can learn more about using Bogus in my previous post. It’s a fantastic tool for demos and testing.

public class IndexModel : PageModel
{
    private static int _personId = 0;
    private static readonly List<Person> Data = new Faker<Person>()
        .RuleFor(m => m.Id, f => _personId++)
        .RuleFor(p => p.Name, f => f.Name.FullName())
        .RuleFor(m => m.Age, f => f.Random.Number(18, 65))
        .RuleFor(m => m.CompanyName, f => f.Company.CompanyName())
        .RuleFor(m => m.Country, f => f.Address.Country())
        .RuleFor(m => m.City, f => f.Address.City())
        .Generate(20);

    private readonly ILogger<IndexModel> _logger;

    public IndexModel(ILogger<IndexModel> logger)
    {
        _logger = logger;
    }

    public List<Person> People => Data;

    public RedirectToPageResult OnPostRemove(int id)
    {
        People.RemoveAll(p => p.Id == id);
        return RedirectToPage("Index");
    }
}

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    public string CompanyName { get; set; }
    public string City { get; set; }
    public string Country { get; set; }
}

As we can see, it’s remarkably unremarkable. Our next step is to write the Razor for our view.

@page 
@model IndexModel 
@{ ViewData["Title"] = "Home page"; }

<div class="text-center">
  <h1 class="display-4">Welcome</h1>
  <p>
    Learn about
    <a href="https://docs.microsoft.com/aspnet/core">
      building Web apps with ASP.NET Core
    </a>
    .
  </p>
</div>

<!-- Editable table -->
<div class="card">
  <h3 class="card-header text-center font-weight-bold text-uppercase py-4">
    Editable table
  </h3>
  <div class="card-body">
    <!-- The form that remove buttons will use -->
    <form id="remove" method="post"></form>

    <div id="table" class="table-editable">
      <span class="table-add float-right mb-3 mr-2">
        <a href="#!" class="text-success">
          <i class="fas fa-plus fa-2x" aria-hidden="true"></i>
        </a>
      </span>
      <table
        class="table table-bordered table-responsive-md table-striped text-center"
      >
        <thead>
          <tr>
            <th class="text-center">Id</th>
            <th class="text-center">Person Name</th>
            <th class="text-center">Age</th>
            <th class="text-center">Company Name</th>
            <th class="text-center">Country</th>
            <th class="text-center">City</th>
            <th class="text-center">Remove</th>
          </tr>
        </thead>
        <tbody>
          @foreach (var person in Model.People) {
          <tr>
            <td class="pt-3-half">@person.Id</td>
            <td class="pt-3-half">@person.Name</td>
            <td class="pt-3-half">@person.Age</td>
            <td class="pt-3-half">@person.CompanyName</td>
            <td class="pt-3-half">@person.Country</td>
            <td class="pt-3-half">@person.City</td>
            <td>
              <span class="table-remove">
                <button
                  type="submit"
                  name="id"
                  value="@person.Id"
                  form="remove"
                  asp-page-handler="Remove"
                  class="btn btn-danger btn-rounded btn-sm my-0"
                >
                  Remove
                </button>
              </span>
            </td>
          </tr>
          }
        </tbody>
      </table>
    </div>
  </div>
</div>

The HTML is where it gets interesting. The first important factor is the presence of a form tag outside the HTML table. The form will be our proxy for all buttons performing a POST to our remove handler. Secondly, we can see the button has some additional attributes.

<button
  type="submit"
  name="id"
  value="@person.Id"
  form="remove"
  asp-page-handler="Remove"
  class="btn btn-danger btn-rounded btn-sm my-0"
>
  Remove
</button>

These attributes include name, value, form, and the tag helper of asp-page-handler. ASP.NET will translate asp-page-handler to formaction when compiling our Razor page.

When we click the button, our form submits to our page handler, and we hit our breakpoint. MAGIC!

hitting breakpoint from button on page

Conclusion

HTML is a deeply rich spec with many features I’ve yet to discover. This particular feature for buttons to submit specific forms on the page has ramifications on HTML structure, CSS design, and page performance. We can use the POST method to ensure crawlers don’t accidentally perform unintended actions while keeping the payload of our HTML to a minimum. Our design teams get the added benefit of not having to worry about CSS styling forms inside of tables.

All around, this is a win for everyone. I’ve pushed it to a GitHub repository for folks who want to try out the sample.

Thanks and please leave a comment.