🧩 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
rendererfunction (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>


Top comments (0)