Back to overview

Building an Inline Line Item Editor

Developer Tutorials Published on 13.01.2026

Building an Inline Line Item Editor

How to implement a 1:n relationship editor within a single form using Custom Snippets and Events in PHP.


The Challenge

In business applications like invoicing, you often need to edit a parent record (e.g., an Invoice) and its children (e.g., Line Items) simultaneously. Standard CRUD generators usually handle one table at a time. To provide a seamless user experience, we implemented an inline table editor that allows adding, editing, and deleting positions directly within the main form.

This solution relies on two key components of our architecture:

  • Form Snippets: PHP files that inject custom HTML into the standard edit form.
  • Custom Events: PHP scripts that execute logic after a record is saved.

1 The Frontend: form_bottom_verkauf_snippet.php

We created a file named form_bottom_verkauf_snippet.php in our setup folder. The system automatically includes this file at the bottom of the "Verkauf" (Sales) form.

1.1 Data Loading

First, the snippet fetches existing positions from the database if we are in edit mode. It also pre-loads product data to populate dropdowns and enable autofill.

// Fetch existing items
$positionen = DBGet($positionen_table, null, ['*'], 'verkaufsvorgang = ?', [$record_id], 'sort_order ASC');

// Fetch products for the dropdown
$leistungen_raw = DBGet($leistungen_table, null, ['id', 'bezeichnung', 'preis', ...]);
1.2 The HTML Structure

We use a standard HTML table inside a Bootstrap card. Crucially, we use a specific naming convention for input fields to distinguish between existing records and new ones.

  • Existing: name="positions[existing_123][menge]"
  • New: name="positions[new_0][menge]"
1.3 JavaScript Logic

A robust JavaScript block handles the interactivity:

  • Dynamic Rows: A <template> tag holds the HTML for a new row. When "Add Position" is clicked, we clone this template, update the index, and append it to the table.
  • Calculations: Event listeners on quantity, price, and discount inputs trigger a recalculation function. This updates the line total and the footer totals (Net, Tax, Gross) in real-time.
  • Choices.js: We initialize Choices.js on the product dropdowns to support searching and grouping by category.
  • Deletions: When a row is removed, we add its ID to a hidden input deleted_positions so the backend knows what to delete.

2 The Backend: event_verkauf_after_save.php

The standard form only saves the main "Verkauf" record. To save the positions, we use the after_create and after_update events. The system looks for a file named event_verkauf_after_save.php and executes it inside the saving transaction.

2.1 Handling Deletions

First, we check the deleted_positions hidden field.

if (!empty($_POST['deleted_positions'])) {
    $deleted_ids = explode(',', $_POST['deleted_positions']);
    // Execute DELETE query for these IDs
    $stmt = $pdo->prepare("DELETE FROM `$positionen_table` WHERE id IN (...)");
    $stmt->execute($deleted_ids);
}
2.2 Saving Rows

We iterate through the $_POST['positions'] array. The keys tell us whether to INSERT or UPDATE.

foreach ($_POST['positions'] as $key => $pos_data) {
    // Calculate values (e.g. line total) server-side for security
    $zeilenpreis = $menge * $listenpreis * (1 - ($rabatt / 100));
    
    if (str_starts_with($key, 'existing_')) {
        $pos_id = (int)str_replace('existing_', '', $key);
        DBUpdate($positionen_table, ..., $pos_id, ...);
    } elseif (str_starts_with($key, 'new_')) {
        DBInsert($positionen_table, ..., $pdo);
    }
}
2.3 Updating Parent Totals

Finally, we recalculate the total sum of all positions for this sales record and update the parent record. This ensures that the "Verkauf" table always contains the correct cached totals for listing views.

Conclusion

By decoupling the UI logic (Snippet) from the persistence logic (Event), we maintained a clean architecture. The main form controller doesn't need to know about the specific logic of line items, making the system highly modular and upgrade-safe.