DEV Community

Explorer
Explorer

Posted on

πŸ“Š Tabulator Custom Data Grid in Joget with Real-Time Cell Validation & Dynamic Headers

Overview

Joget DX provides robust built-in Form Grids for tabular data entry. However, enterprise applications frequently require advanced grid behaviors such as custom field-level validators, dynamic column visibility toggles based on process type, formatted currency/number inputs, and custom row actions.

By embedding Tabulator JSβ€”a lightweight, feature-rich JavaScript table libraryβ€”into a Joget Custom HTML element, developers can deliver a high-performance spreadsheet-like user experience inside Joget forms while keeping data synchronized with Joget form storage.


How It Works

  1. Tabulator Initialization: The script dynamically initializes Tabulator on an HTML container (<div id="custom-grid"></div>).
  2. Dynamic Header & Visibility Rules: Depending on the selected transaction or document type, column definitions are dynamically updated to hide or show relevant fields using setColumns().
  3. Real-Time Validation & Formatting: Custom JavaScript formatters and validators enforce max length rules, currency formatting (with commas), date pickers (flatpickr), and conditional read-only logic.
  4. Data Synchronization: On form submission or cell updates, table data is serialized into JSON format and synced with a hidden Joget Form field so that Joget stores the grid payload seamlessly.

Where to Use in Joget

  • Form Builder: Place the script inside a Custom HTML field in your Joget Form. Bind the table output to a hidden Text Area or Text Field to store the JSON dataset.
  • Userviews: Embed within custom Userview pages to display dynamic interactive reports with inline editing capability.

Full Code

<!-- Tabulator CSS & JS Dependencies -->
<link href="https://unpkg.com/tabulator-tables@5.4.4/dist/css/tabulator.min.css" rel="stylesheet" />
<script type="text/javascript" src="https://unpkg.com/tabulator-tables@5.4.4/dist/js/tabulator.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/luxon@3.3.0/build/global/luxon.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>

<!-- Grid Container -->
<div id="tabulator-custom-grid"></div>

<style>
  .flatpickr-calendar { font-size: 12px; }
  .flatpickr-input { font-size: 12px; padding: 4px 8px; }
  .tabulator .invalid { border: 2px solid #ff4d4f !important; background-color: #fff1f0 !important; }
</style>

<script>
$(function () {
  // Reference hidden Joget storage element
  const hiddenStorageField = document.getElementById("list_grid_items");

  // Number & Amount Validators
  function amountValidator(cell, value) {
    if (!value) return true;
    const strValue = String(value);
    const parts = strValue.split(".");
    const wholePart = parts[0];
    const decimalPart = parts[1] || "";
    return wholePart.length <= 13 && decimalPart.length <= 2;
  }

  // Currency & Thousand Separator Formatter
  const commasFormatter = () => {
    return (cell) => {
      let value = cell.getValue();
      if (!value) return "";
      const [integerPart, decimalPart] = String(value).split('.');
      const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
      return decimalPart ? `${formattedInteger}.${decimalPart}` : formattedInteger;
    };
  };

  // Custom Date Picker Editor
  const customDateEditor = function (cell, onRendered, success, cancel, editorParams) {
    const input = document.createElement("input");
    input.type = "text";
    if (cell.getValue()) {
      input.value = cell.getValue();
    }
    flatpickr(input, {
      dateFormat: "Y/m/d",
      onClose: function (selectedDates, dateStr) {
        success(dateStr);
      }
    });
    onRendered(function () {
      input.focus();
    });
    return input;
  };

  // Column Definitions
  const columnsConfig = [
    { title: "#", formatter: "rownum", width: 50, headerSort: false, frozen: true },
    {
      title: "Item Code",
      field: "item_code",
      editor: "input",
      width: 120,
      validator: "required",
      headerSort: false
    },
    {
      title: "Category",
      field: "category",
      editor: "list",
      editorParams: { values: ["General", "Transfer", "Adjustment", "Tax"] },
      width: 130,
      headerSort: false
    },
    {
      title: "Debit Amount",
      field: "debit_amount",
      editor: "input",
      width: 140,
      formatter: commasFormatter(),
      validator: amountValidator,
      headerSort: false
    },
    {
      title: "Credit Amount",
      field: "credit_amount",
      editor: "input",
      width: 140,
      formatter: commasFormatter(),
      validator: amountValidator,
      headerSort: false
    },
    {
      title: "Posting Date",
      field: "posting_date",
      editor: customDateEditor,
      width: 130,
      headerSort: false
    },
    {
      title: "Description",
      field: "description",
      editor: "input",
      validator: "maxLength:50",
      headerSort: false
    }
  ];

  // Initialize Tabulator Grid
  const table = new Tabulator("#tabulator-custom-grid", {
    data: hiddenStorageField && hiddenStorageField.value ? JSON.parse(hiddenStorageField.value) : [{}],
    layout: "fitColumns",
    columns: columnsConfig,
    history: true,
    reactiveData: true
  });

  // Sync grid data to Joget hidden input on cell edit
  table.on("cellEdited", function (cell) {
    if (hiddenStorageField) {
      hiddenStorageField.value = JSON.stringify(table.getData());
    }
  });
});
</script>
Enter fullscreen mode Exit fullscreen mode

Example Use Cases

  • Financial Journal Voucher Entry: Entering GL debit/credit entries where amounts require real-time thousands formatting and balance checking.
  • Complex Inventory Adjustments: Adding batch inventory lines with custom unit validation and date selection.
  • Timesheet & Line Item Expense Claims: Managing multi-row claims with dynamic column visibility based on claim type (e.g., travel vs. office supplies).

Customization Tips

  1. Dynamic Column Toggling: Use table.setColumns(newColumnConfig) inside an onChange event of a Joget Select Box to dynamically show or hide columns based on user choices.
  2. Conditional Cell Backgrounds: Add CSS classes dynamically in formatters (e.g., cell.getElement().classList.add("invalid")) to highlight invalid entries immediately.
  3. Handling Large Datasets: Enable Tabulator pagination (pagination: "local", paginationSize: 10) to maintain browser rendering speed for large tables.

Key Benefits

  • Enhanced UX: Provides spreadsheet-like editing directly inside standard web forms.
  • Instant Client Validation: Prevents invalid submissions before the form touches the server.
  • Seamless Integration: Uses standard JSON stringification to bind directly to Joget form storage.

Security Note

Always re-validate grid JSON data on the server side using a BeanShell Form Binder or Form Validator plugin prior to storing values in backend database tables. Never rely exclusively on client-side JavaScript validation for security-sensitive fields.


Final Thoughts

Tabulator JS offers a powerful bridge between standard Joget form components and highly customized data entry interfaces. By combining client-side formatting with standard Joget form fields, developers can satisfy demanding UX requirements without sacrificing platform maintainability.

Top comments (0)