DEV Community

Explorer
Explorer

Posted on • Edited on

🗑️ Add Custom Row Action Buttons to Joget Spreadsheet

🧩 Overview

In Joget, the Spreadsheet form element (powered by Handsontable) is perfect for managing tabular data inside forms.
However, it doesn’t natively provide built-in row-level delete buttons or a convenient add row feature.

In this guide, we’ll learn how to:
✅ Add a delete button to each row in a Joget Spreadsheet.
✅ Create a separate “Add Row” button that dynamically inserts new rows.
✅ Keep the entire implementation simple, secure, and fully functional.


⚙️ How It Works

💡 The approach uses a custom column renderer and a jQuery button event:

  • The custom renderer injects a Delete button in a column cell.
  • The button calls hot.alter("remove_row", row) to delete that row.
  • A separate Add Row button triggers hot.alter("insert_row_below") to insert a new row at the bottom.
  • Both actions are performed dynamically without reloading the form.

🧠 Important:

  • Set the spreadsheet column format type to Custom.
  • Paste the renderer script directly in the custom field configuration.
  • Avoid adding any extra comments inside the renderer function (to prevent Joget parser issues).

💻 Full Code


🧠 Example Use Cases

Employee Shift Planner – Quickly add or remove shift rows.
Project Task Tracker – Manage task entries dynamically.
Material Request Form – Add/remove item rows on demand.
Appointment List – Delete canceled appointments instantly.


🛠️ Customization Tips

💡 To change the delete button label:
Replace

with plain text (e.g., "Delete" or "Remove").

⚙️ To use a custom Spreadsheet ID:
Update this line:

to match your spreadsheet’s field ID.

🎨 To style buttons:
Apply CSS in a Custom HTML element:


🌟 Key Benefits

User-friendly: Adds clear visual controls to manage rows.
⚙️ Lightweight: Uses only jQuery and built-in Joget APIs.
🚀 Instant updates: No form reload needed.
🧩 Reusable: Works in any spreadsheet-based form.
🔧 Customizable: Easily adjust button text, color, or logic.


🔒 Security Note

⚠️ Ensure that delete actions are contextually safe

  • Only use this feature for client-side data entry (not for record deletion in the database).
  • If connected to backend records, validate all deletion logic on the server side before applying changes.

🧭 Final Thoughts

By adding these small enhancements, your Joget Spreadsheet becomes much more interactive and user-friendly.

Source Code

<script>
  {
    {
      renderer: function (instance, td, row, col, prop, value, cellProperties) {
        td.innerHTML = "";
        td.style.textAlign = "center";

        let deleteBtn = document.createElement("button");
        deleteBtn.type = "button";
        deleteBtn.className = "eaction";
        deleteBtn.innerText = "#i18n.ocs_delete_btn#";
        deleteBtn.style.marginRight = "5px";

        deleteBtn.onclick = function (e) {
          e.preventDefault();
          e.stopPropagation();

          console.log("Delete button clicked for row:", row);

          let hot = FormUtil.getField("appointments_spreadSheet").data("hot");
          if (!hot) {
            console.warn("Handsontable instance not found");
            return;
          }

          const $ta = $("textarea[name='appointments_spreadSheet_JSON_DATA']");
          let arr = [];
          try {
            arr = JSON.parse($ta.val() || "[]");
          } catch {
            arr = [];
          }

          console.log("JSON before deletion:", arr);

          arr.splice(row, 1);
          $ta[0].value = JSON.stringify(arr);

          console.log("JSON after deletion:", arr);

          const hooksBackup = hot.getSettings().afterRemoveRow;
          hot.updateSettings({ afterRemoveRow: null });
          hot.alter("remove_row", row);
          hot.updateSettings({ afterRemoveRow: hooksBackup });

          console.log("Row removed from Handsontable:", row);
        };

        td.appendChild(deleteBtn);

        let editBtn = document.createElement("button");
        editBtn.type = "button";
        editBtn.className = "eaction-reschedule";
        editBtn.innerText = "Reschedule";

        editBtn.onclick = function (e) {
          e.preventDefault();
          e.stopPropagation();

          console.log("Reschedule button clicked for row:", row);

          openAppointmentPopup(row, function (updatedData) {
            console.log("Reschedule row data:", updatedData);
          }, "reschedule");
        };

        td.appendChild(editBtn);

        let CancelBtn = document.createElement("button");
        CancelBtn.type = "button";
        CancelBtn.className = "eaction-cancel";
        CancelBtn.innerText = "Cancel";

        CancelBtn.onclick = function (e) {
          e.preventDefault();
          e.stopPropagation();

          console.log("Reschedule button clicked for row:", row);

          openAppointmentPopup(row, function (updatedData) {
            console.log("Reschedule row data:", updatedData);
          }, "cancel");
        };

        td.appendChild(CancelBtn);
      }
    }
  }


  { { renderer: function (instance, td, row, col, prop, value, cellProperties) { td.innerHTML = "<button type='button' class='eaction'>#i18n.ocs_delete_btn#</button>"; td.style.textAlign = "center"; $(document).ready(function () { let hot = FormUtil.getField("appointments_spreadSheet").data("hot"); let btn = td.querySelector(".eaction"); btn.onclick = function (e) { e.preventDefault(); e.stopPropagation(); hot.alter("remove_row", row); }; }); } } }

  { { renderer: function (instance, td, row, col, prop, value, cellProperties) { td.innerHTML = ""; td.style.textAlign = "center"; let deleteBtn = document.createElement("button"); deleteBtn.type = "button"; deleteBtn.className = "eaction"; deleteBtn.innerText = "#i18n.ocs_delete_btn#"; deleteBtn.style.marginRight = "5px"; deleteBtn.onclick = function (e) { e.preventDefault(); e.stopPropagation(); console.log("Delete button clicked for row:", row); let hot = FormUtil.getField("appointments_spreadSheet").data("hot"); if (!hot) { console.warn("Handsontable instance not found"); return; } const $ta = $("textarea[name='appointments_spreadSheet_JSON_DATA']"); let arr = []; try { arr = JSON.parse($ta.val() || "[]"); } catch { arr = []; } console.log("JSON before deletion:", arr); arr.splice(row, 1); $ta[0].value = JSON.stringify(arr); console.log("JSON after deletion:", arr); const hooksBackup = hot.getSettings().afterRemoveRow; hot.updateSettings({ afterRemoveRow: null }); hot.alter("remove_row", row); hot.updateSettings({ afterRemoveRow: hooksBackup }); console.log("Row removed from Handsontable:", row); }; td.appendChild(deleteBtn); let editBtn = document.createElement("button"); editBtn.type = "button"; editBtn.className = "eaction-edit"; editBtn.innerText = "Edit"; editBtn.onclick = function (e) { e.preventDefault(); e.stopPropagation(); console.log("Edit button clicked for row:", row); openAppointmentPopup(row, function (updatedData) { console.log("Edited row data:", updatedData); }); }; td.appendChild(editBtn); } } }





  {
    renderer: function (instance, td, row, col, prop, value, cellProperties) {
      td.innerHTML = "";
      td.style.textAlign = "center";

      const hot = FormUtil.getField("appointments_spreadSheet").data("hot");
      const $ta = $("textarea[name='appointments_spreadSheet_JSON_DATA']");
      let arr = [];
      try {
        arr = JSON.parse($ta.val() || "[]");
      } catch {
        arr = [];
      }

      const rowData = arr[row] || {};
      const isNewRow = !rowData.id;

      if (isNewRow) {
        let deleteBtn = document.createElement("button");
        deleteBtn.type = "button";
        deleteBtn.className = "eaction";
        deleteBtn.innerText = "#i18n.ocs_delete_btn#";
        deleteBtn.style.marginRight = "5px";

        // for delete button
        <button type="button" class="btn btn-sm btn-outline-danger">
          <i class="fas fa-times"></i>
        </button>

        deleteBtn.onclick = function (e) {
          e.preventDefault();
          e.stopPropagation();

          if (!hot) {
            return;
          }

          arr.splice(row, 1);
          $ta[0].value = JSON.stringify(arr);
          $($ta).val(JSON.stringify(arr)).trigger("change");

          const hooksBackup = hot.getSettings().afterRemoveRow;
          hot.updateSettings({ afterRemoveRow: null });
          hot.alter("remove_row", row);
          hot.updateSettings({ afterRemoveRow: hooksBackup });
        };

        td.appendChild(deleteBtn);
        return;
      }

      let editBtn = document.createElement("button");
      editBtn.type = "button";
      editBtn.className = "eaction-reschedule";
      editBtn.innerText = "Reschedule";

      // i want to use the below button
      <button class="btn btn-sm btn-warning me-1">
        <i class="fas fa-calendar-alt"></i> <span data-ar="إعادة جدولة" data-en="Reschedule">Reschedule</span>
      </button>

      editBtn.onclick = function (e) {
        e.preventDefault();
        e.stopPropagation();

        openAppointmentPopup(row, function (updatedData) {
          arr[row] = { ...arr[row], ...updatedData };
          $ta[0].value = JSON.stringify(arr);
          $($ta).val(JSON.stringify(arr)).trigger("change");

          if (!isNewRow && hot) {
            hot.updateSettings({
              cells: function (r, c, prop) {
                if (r === row) return { readOnly: true };
              }
            });
          }
        }, "reschedule");
      };

      td.appendChild(editBtn);

      let cancelBtn = document.createElement("button");
      cancelBtn.type = "button";
      cancelBtn.className = "eaction-cancel";
      cancelBtn.innerText = "Cancel";

      // for cancel button
      <button class="btn btn-sm btn-danger" onclick="showCancelModal(1)">
        <i class="fas fa-times"></i> <span data-ar="الغاء" data-en="Cancel">Cancel</span>
      </button>

      cancelBtn.onclick = function (e) {
        e.preventDefault();
        e.stopPropagation();

        openAppointmentPopup(row, function (updatedData) {
          arr[row] = { ...arr[row], ...updatedData };
          $ta[0].value = JSON.stringify(arr);
          $($ta).val(JSON.stringify(arr)).trigger("change");

          if (!isNewRow && hot) {
            hot.updateSettings({
              cells: function (r, c, prop) {
                if (r === row) return { readOnly: true };
              }
            });
          }
        }, "cancel");
      };

      td.appendChild(cancelBtn);
    }
  }





  {
    {
      renderer: function (instance, td, row, col, prop, value, cellProperties) {
        td.innerHTML = "";
        td.style.textAlign = "center";

        const hot = FormUtil.getField("appointments_spreadSheet").data("hot");
        const $ta = $("textarea[name='appointments_spreadSheet_JSON_DATA']");
        let arr = [];

        try {
          arr = JSON.parse($ta.val() || "[]");
        } catch {
          arr = [];
        }

        const rowData = arr[row] || {};
        const isNewRow = !rowData.id;

        if (isNewRow) {
          let deleteBtn = document.createElement("button");
          deleteBtn.type = "button";
          deleteBtn.id = "deleteBtn_" + row;
          deleteBtn.className = "btn btn-sm btn-outline-danger button form-button";
          deleteBtn.innerHTML = '<i class="fas fa-times"></i>';
          deleteBtn.style.marginRight = "5px";

          deleteBtn.onclick = function (e) {
            e.preventDefault();
            e.stopPropagation();

            if (!hot) {
              return;
            }

            arr.splice(row, 1);
            $ta[0].value = JSON.stringify(arr);
            $($ta).val(JSON.stringify(arr)).trigger("change");

            const hooksBackup = hot.getSettings().afterRemoveRow;
            hot.updateSettings({ afterRemoveRow: null });
            hot.alter("remove_row", row);
            hot.updateSettings({ afterRemoveRow: hooksBackup });
          };

          td.appendChild(deleteBtn);
          return;
        }

        let editBtn = document.createElement("button");
        editBtn.type = "button";
        editBtn.id = "editBtn_" + row;
        editBtn.className = "btn btn-sm btn-warning me-1 button form-button";
        editBtn.innerHTML =
          '<i class="fas fa-calendar-alt"></i> <span data-ar="إعادة جدولة" data-en="Reschedule">Reschedule</span>';

        editBtn.onclick = function (e) {
          e.preventDefault();
          e.stopPropagation();

          openAppointmentPopup(
            row,
            function (updatedData) {
              arr[row] = { ...arr[row], ...updatedData };
              $ta[0].value = JSON.stringify(arr);
              $($ta).val(JSON.stringify(arr)).trigger("change");
            },
            "reschedule"
          );
        };

        td.appendChild(editBtn);

        let cancelBtn = document.createElement("button");
        cancelBtn.type = "button";
        cancelBtn.id = "cancelBtn_" + row;
        cancelBtn.className = "btn btn-sm btn-danger button form-button";
        cancelBtn.innerHTML =
          '<i class="fas fa-times"></i> <span data-ar="الغاء" data-en="Cancel">Cancel</span>';

        cancelBtn.onclick = function (e) {
          e.preventDefault();
          e.stopPropagation();

          openAppointmentPopup(
            row,
            function (updatedData) {
              arr[row] = { ...arr[row], ...updatedData };
              $ta[0].value = JSON.stringify(arr);
              $($ta).val(JSON.stringify(arr)).trigger("change");
            },
            "cancel"
          );
        };

        td.appendChild(cancelBtn);
      },
    }
  }



</script>
Enter fullscreen mode Exit fullscreen mode

Top comments (0)