Building Dynamic Form Fields with jQuery: A Step-by-Step Guide for Invoices

When building business applications like accounting or ERP systems, handling line items is a classic challenge. A static form won’t cut it—users need the flexibility to add or remove rows on the fly, whether they are entering three items or thirty.

In this tutorial, we will walk through how to build a clean, dynamic tabular form using jQuery. We’ll use a practical example: an invoice creation form where users can add and remove item rows dynamically before submitting the data to a backend (like Django or Laravel).

The Complete Solution

Here is the complete HTML and jQuery setup. It features standard invoice headers (Invoice Number and Date) and a dynamic table for line items.

HTML

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Create Invoice</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/mixins/_utilities.scss" rel="stylesheet">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
</head>
<body class="bg-light py-5">

    <div class="container">
        <div class="row justify-content-center">
            <div class="col-lg-10">
                
                <div class="card shadow-sm">
                    <div class="card-header bg-primary text-white py-3">
                        <h1 class="h3 mb-0">Add Invoice</h1>
                    </div>
                    <div class="card-body p-4">
                        
                        <form method="POST" action="/">
                            
                            <div class="row g-3 mb-4">
                                <div class="col-md-6">
                                    <label for="invoice_number" class="form-label fw-bold">Invoice Number:</label>
                                    <input type="text" name="invoice_number" id="invoice_number" class="form-control" placeholder="INV-0001">
                                </div>
                                <div class="col-md-6">
                                    <label for="date" class="form-label fw-bold">Date:</label>
                                    <input type="date" name="date" id="date" class="form-control">
                                </div>
                            </div>
                            
                            <hr class="text-muted my-4">
                            
                            <div class="d-flex justify-content-between align-items-center mb-3">
                                <h2 class="h5 mb-0 text-secondary">Invoice Items</h2>
                                <button type="button" id="add_row" class="btn btn-outline-success btn-sm">
                                    + Add Row
                                </button>
                            </div>
                            
                            <div class="table-responsive">
                                <table id="invoice_items" class="table table-bordered table-hover align-middle">
                                    <thead class="table-light">
                                        <tr>
                                            <th style="width: 45%;">Item Name</th>
                                            <th style="width: 20%;">Quantity</th>
                                            <th style="width: 20%;">Price</th>
                                            <th style="width: 15%;" class="text-center">Action</th>
                                        </tr>
                                    </thead>
                                    <tbody>
                                        <tr>
                                            <td><input type="text" name="item_name[]" class="form-control item_name" required></td>
                                            <td><input type="number" name="quantity[]" class="form-control quantity" required min="1"></td>
                                            <td><input type="number" name="price[]" class="form-control price" required step="0.01"></td>
                                            <td class="text-center">
                                                <button type="button" class="btn btn-outline-danger btn-sm remove_row">Remove</button>
                                            </td>
                                        </tr>
                                    </tbody>
                                </table>
                            </div>
                            
                            <div class="d-grid gap-2 d-md-flex justify-content-md-end mt-4">
                                <button type="submit" class="btn btn-primary px-4">Save Invoice</button>
                            </div>
                        </form>
                        
                    </div>
                </div>
                
            </div>
        </div>
    </div>
    
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

    <script>
        $(document).ready(function() {
            // 1. Add row when "Add Row" button is clicked
            $("#add_row").click(function() {
                var row = `
                    <tr>
                        <td><input type="text" name="item_name[]" class="form-control item_name" required></td>
                        <td><input type="number" name="quantity[]" class="form-control quantity" required min="1"></td>
                        <td><input type="number" name="price[]" class="form-control price" required step="0.01"></td>
                        <td class="text-center"><button type="button" class="btn btn-outline-danger btn-sm remove_row">Remove</button></td>
                    </tr>
                `;
                $("#invoice_items tbody").append(row);
            });
            
            // 2. Remove row when "Remove" button is clicked
            $(document).on("click", ".remove_row", function() {
                if ($("#invoice_items tbody tr").length > 1) {
                    $(this).closest("tr").remove();
                } else {
                    alert("An invoice must have at least one item.");
                }
            });
        });
    </script>
</body>
</html>

How It Works: Breaking Down the Logic

1. Handling Array Data in the Backend (name[])

Look closely at the input names in the table rows:

  • name="item_name[]"
  • name="quantity[]"
  • name="price[]"

The square brackets [] are crucial. They tell your backend server (whether it’s Django, Laravel, or simple PHP) to treat these fields as an array/list rather than a single value. When the form is posted, the backend will receive ordered lists of items, quantities, and prices that you can loop through synchronously to save your line items to the database.

2. Appending Rows Dynamically

To add a new row, we bind a click event handler to the #add_row button.

JavaScript

$("#add_row").click(function() {
    var row = `...`; // Template literal containing the HTML string
    $("#invoice_items tbody").append(row);
});

Every time the button is clicked, jQuery grabs the string template of our table row and appends it to the end of the <tbody>.

3. Delgated Event Listening for Deletions

If you try to bind a regular click event directly to the .remove_row buttons like this: $(".remove_row").click(...), it will fail for newly added rows. Why? Because regular bindings only apply to elements that exist in the DOM when the page first loads.

To bypass this, we use event delegation:

JavaScript

$(document).on("click", ".remove_row", function() {
    $(this).closest("tr").remove();
});

By listening to the document (or a static parent element like #invoice_items) and passing the .remove_row selector as a parameter, jQuery catches the click event as it bubbles up, regardless of when the row was added.

Inside the function, $(this).closest("tr") tells jQuery to look up the DOM tree to find the nearest table row parent container and safely strip it from the page.

Pro-Tips for Production

  • Data Validation: Adding required attributes to your input elements ensures users can’t accidentally submit completely blank rows.
  • Keep One Row Minimum: It’s good practice to add a safety check (like the one included in the script above) to make sure a user doesn’t delete all rows, leaving an empty invoice.
  • Total Calculation: You can extend this logic by adding an on("input", ...) listener to the .quantity and .price fields to dynamically compute line-item totals and a grand total at the bottom of the table in real time!

Leave a Comment