Allowing users to conveniently and swiftly edit selected DataGrid rows
NOTE
Consider using a link column and a popup rather than the inline edit mode. The popup method is more extensible and more rubust than the inline edit method.
Row-Level-Edit.mp4
Current version 2.4
2.0 Added checkbox column support; changed header-based column definition to column count instead; added text and value definition for dropdowns. Required changes
- FormField Type: change "name" property to "column" (see Type Setup)
- Script (see Global Script Setup)
- Update script to version below
- Change input parameter name "EditColumnHeader" to "LinkColumn"
- Change input parameter name "IdentityColumnHeader" to "IdentityColumn"
- Event handler (see Edit.Click Event Script)
- Added support for dropdown text and value definition (see example below)
2.1 Added support for DataGrid searching and sorting; added support for passing column names in the "columns" parameter (optional). Required changes
- Callback script: The result is no longer passed back to the callback script as an array, but as an object consisting of the columns in the DataGrid.
2.2 Some minor bug fixes
2.3 Added more datagrid areas to invoke click-away form dismiss; added optional return boolean for callback script
2.4 Fixed "control in template" bug; enhanced "FormField" data list type
- Check the Enable Style Sheet checkbox in the application properties
- Use the instructions from this repo to setup the database and DataGrid for this sample
- The DataGrid must contain an Edit column and the Edit column must have a click event handler
- Create a Type called "FormField"
- Add the following properties to the type
- "column" (Any)
- "type" (Any)
- "required" (Any)
- "min" (Any)
- "max" (Any)
- "data" (List)
- "Item" (Object)
- "text" (Any)
- "value" (Any)
- "Item" (Object)
- Create a Global Script called "EditableRow"
- Add the input parameters below to the Global Script
- DataGridClass
- LinkColumn
- FormFields
- IdentityColumn
- IdentityValue
- CallbackScript
- Drag a JavaScript action into the script
- Add the Javascript below unchanged into the JavaScript code property
/* Stadium Script Version 2.4 https://github.com/stadium-software/datagrid-inline-row-edit */
let scope = this;
let callback = ~.Parameters.Input.CallbackScript;
let classInput = ~.Parameters.Input.DataGridClass;
if (typeof classInput == "undefined") {
console.error("The DataGridClass parameter is required");
return false;
}
let dgClassName = "." + classInput;
let dg = document.querySelectorAll(dgClassName);
if (dg.length == 0) {
console.error("The class '" + dgClassName + "' is not assigned to any DataGrid");
return false;
} else if (dg.length > 1) {
console.error("The class '" + dgClassName + "' is assigned to multiple DataGrids. DataGrids using this script must have unique classnames");
return false;
} else {
dg = dg[0];
}
dg.classList.add("stadium-inline-edit-datagrid", "datagrid-inline-edit-form");
let datagridname = dg.id.split("_")[1].replace("-container","");
let table = dg.querySelector("table");
let dataGridColumns = getColumnDefinition();
let rowFormFields = ~.Parameters.Input.FormFields;
let IDColumn = ~.Parameters.Input.IdentityColumn;
let IDValue = ~.Parameters.Input.IdentityValue;
let EditLink = ~.Parameters.Input.LinkColumn;
if (!isNumber(IDColumn)) {
IDColumn = getElementIndex(dataGridColumns, IDColumn) + 1;
}
if (!isNumber(EditLink)) {
EditLink = getElementIndex(dataGridColumns, EditLink) + 1;
}
let idColumnName = dataGridColumns[IDColumn - 1];
let rowNumber;
let options = {
childList: true,
subtree: true,
characterData: true,
};
let observer = new MutationObserver(resetDataGrid);
initForm();
document.onkeydown = function (evt) {
evt = evt || window.event;
let isEscape = false;
if ("key" in evt) {
isEscape = (evt.key === "Escape" || evt.key === "Esc");
} else {
isEscape = (evt.keyCode === 27);
}
if (isEscape) {
resetDataGrid();
}
};
/*--------------------------------------------------------------------------------------*/
function initForm() {
let IDCells = table.querySelectorAll("tbody tr td:nth-child(" + IDColumn + ")");
table.querySelector("thead").addEventListener("click", resetDataGrid);
table.querySelector("tfoot").addEventListener("click", resetDataGrid);
dg.querySelector(".data-grid-header").addEventListener("click", resetDataGrid);
for (let i = 0; i < IDCells.length; i++) {
let rowtr = IDCells[i].parentElement;
let IDCell = convertToNumber(IDCells[i].textContent);
if (IDCell == IDValue) {
rowNumber = i+1;
} else {
rowtr.classList.add("opacity");
rowtr.addEventListener("click", resetDataGrid);
}
}
if (!rowNumber) {
console.error("The row was not found");
return false;
}
let row = table.querySelector("tbody tr:nth-child(" + rowNumber + ")");
row.classList.add("edit-orig");
let cells = row.querySelectorAll("td");
let editform = document.createElement("tr");
editform.classList.add("edit-form", "editing");
editform.setAttribute("data-id",IDValue);
let rowData = getElementFromObjects(scope[`${datagridname}Data`], IDValue, idColumnName);
for (let i = 0; i < cells.length; i++) {
let el, type, data, min, max, required;
let colNum = i+1;
let ffield = getElementFromObjects(rowFormFields, colNum, "column");
let name = dataGridColumns[i];
if (!ffield) {
ffield = getElementFromObjects(rowFormFields, name, "column");
}
if (ffield) {
type = ffield.type;
data = ffield.data;
min = ffield.min;
max = ffield.max;
required = ffield.required;
}
let origCell = row.querySelector("td:nth-child(" + colNum + ")");
let origStyles = origCell.getAttribute("style");
let cell = document.createElement("td");
cell.setAttribute("style", origStyles);
let value = rowData[name];
if (type == "text") {
el = document.createElement("input");
el.value = value;
el.setAttribute("stadium-form-name", name);
el.classList.add("form-control");
}
if (type == "number") {
el = document.createElement("input");
el.setAttribute("type", "number");
if (min) el.setAttribute("min", min);
if (max) el.setAttribute("max", max);
el.addEventListener("keydown",function(e) {
if (e.key.toLowerCase() === "e") {
e.preventDefault();
}
}, false);
el.value = value;
el.setAttribute("stadium-form-name", name);
el.classList.add("form-control");
}
if (type == "date") {
el = document.createElement("input");
el.setAttribute("type", "date");
el.classList.add("form-control");
if (min) {
let dmin = new Date(min);
min = dmin.getFullYear() + '-' + ('0' + (dmin.getMonth() + 1)).slice(-2) + '-' + ('0' + dmin.getDate()).slice(-2);
el.setAttribute("min", min);
}
if (max) {
let dmax = new Date(max);
max = dmax.getFullYear() + '-' + ('0' + (dmax.getMonth() + 1)).slice(-2) + '-' + ('0' + dmax.getDate()).slice(-2);
el.setAttribute("max", max);
}
el.setAttribute("stadium-form-name", name);
if (value) {
let d = new Date(value);
el.value = d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2);
}
}
if (type == "checkbox") {
el = document.createElement("input");
el.setAttribute("stadium-form-name", name);
el.setAttribute("type", "checkbox");
if (value == "true" || value == "Yes" || value == "1") {
el.setAttribute("checked", "");
}
}
if (type == "dropdown") {
el = document.createElement("select");
for (let j = 0; j < data.length; j++) {
let option = document.createElement("option");
if (typeof data[j].text != "undefined" && typeof data[j].value != "undefined") {
option.text = data[j].text;
option.value = data[j].value;
} else if (typeof data[j] == "object") {
let ob = Object.entries(data[j]);
option.text = ob[0][0];
option.value = ob[0][1];
} else {
option.text = data[j];
option.value = data[j];
}
if (option.text == value) {
option.selected = true;
}
el.appendChild(option);
}
el.setAttribute("stadium-form-name", name);
el.classList.add("form-control");
}
if (!type && !origCell.querySelector(":is(button, a, [type='checkbox'])") && name != "RowSelector") {
cell.textContent = value;
el = document.createElement("input");
el.value = value;
el.setAttribute("stadium-form-name", name);
el.setAttribute("type", "hidden");
}
if (colNum == EditLink) {
el = document.createElement("button");
el.classList.add("stadium-form-save");
el.innerText = "Save";
el.addEventListener("click", saveButtonClick);
}
if (el) {
el.classList.add("stadium-inline-form-control");
if (required) el.setAttribute("required", "");
cell.appendChild(el);
}
editform.appendChild(cell);
}
insertAfter(editform, row);
observer.observe(table, options);
}
function resetDataGrid(){
observer.disconnect();
let editorig = table.querySelector(".edit-orig");
if (editorig) editorig.classList.remove("edit-orig");
let editform = table.querySelector(".edit-form");
if (editform) editform.remove();
let trs = table.querySelectorAll("tr");
for (let i = 0; i < trs.length; i++) {
trs[i].classList.remove("opacity");
trs[i].removeEventListener("click", resetDataGrid);
}
table.querySelector("thead").removeEventListener("click", resetDataGrid);
table.querySelector("tfoot").removeEventListener("click", resetDataGrid);
dg.querySelector(".data-grid-header").removeEventListener("click", resetDataGrid);
}
async function saveButtonClick() {
let isValid = true;
let row = table.querySelector(".edit-form");
let IDVal = row.getAttribute("data-id");
let cells = row.cells;
let callbackData = {};
let objData = {};
for (let i = 0; i < cells.length; i++) {
let formField = cells[i].querySelector("[stadium-form-name]:not([stadium-form-name='']");
if (formField) {
if (formField.reportValidity()) {
let fieldValue = convertToNumber(formField.value);
if (formField.getAttribute("type") == "checkbox") fieldValue = formField.checked;
if (dataGridColumns[i] != "RowSelector") callbackData[dataGridColumns[i]] = fieldValue;
if (formField.tagName == "SELECT") fieldValue = formField.options[formField.selectedIndex].text;
objData[dataGridColumns[i]] = fieldValue;
} else {
isValid = false;
}
} else if (IDColumn-1 == i) {
IDVal = convertToNumber(IDVal);
callbackData[dataGridColumns[i]] = IDVal;
objData[dataGridColumns[i]] = IDVal;
}
}
if (isValid) {
let returned = await scope[callback](callbackData);
if (typeof returned !== "undefined" && returned.success !== false || (typeof returned === "undefined")) {
updateDataModelRow(IDVal, objData);
resetDataGrid();
}
}
}
function insertAfter(newNode, existingNode) {
existingNode.parentNode.insertBefore(newNode, existingNode.nextSibling);
}
function getElementIndex(haystack, needle) {
return haystack.indexOf(needle);
}
function getElementFromObjects(haystack, needle, column) {
return haystack.find(obj => {return obj[column] == needle;});
}
function updateDataModelRow(id, rowData) {
let handler1 = {};
let dgData = scope[`${datagridname}Data`];
let result = dgData.map(el => el[dataGridColumns[IDColumn-1]] == id ? new Proxy(rowData, handler1) : el);
scope[`${datagridname}Data`] = result;
}
function getColumnDefinition() {
let cols = [];
let colDefs = scope[`${datagridname}ColumnDefinitions`];
if (table.querySelector("thead th:nth-child(1) input[type=checkbox")) cols.push("RowSelector");
for (let i = 0; i < colDefs.length; i++) {
cols.push(colDefs[i].name);
}
return cols;
}
function isNumber(str) {
if (typeof str == "number") return true;
return !isNaN(str) && !isNaN(parseFloat(str));
}
function convertToNumber(val) {
if (!isNumber(val)) {
let no;
if (typeof val == "string") no = val.replace(/ /g,"");
if (isNumber(no)) return Number(no);
} else {
val = Number(val);
}
return val;
}- Drag a DataGrid control to the page (see above)
- Add a class of your choosing to the DataGrid Classes property thato uniquely identifies the DataGrid on this page (e.g datagrid-inline-edit)
- Note: If multiple editable DataGrids are shown on one page, each DataGrid must have a unique classname
- Populate the DataGrid with data (see above)
NOTE: The DataGrid must contain an Edit column (a clickable row-level column) and that column must have a click event handler (!)
- Drag a List action into the event script and name the List "FormFields"
- Set the List Item Type property to "Types.FormField"
- Define the editable columns of your datagrid and their form fields
- column: The column number (start counting at 1; include all datagrid columns) OR the column property name as it appears in the list of columns and the column "Name" property

- type: The type of the column. Supported are
- text
- date
- number
- checkbox
- dropdown
- required: A boolean (add "true" if required)
- min: A minimum value for number or date columns
- max: A maximum value for number or date columns
- data: A list of text & value objects for dropdowns (see example below)
- column: The column number (start counting at 1; include all datagrid columns) OR the column property name as it appears in the list of columns and the column "Name" property
= [{
"column":3,
"type": "text"
},{
"column": "LastName",
"type": "text"
},{
"column": 5,
"type": "number",
"min": "0",
"max": "10",
"required": "true"
},{
"column": 6,
"type": "number",
"min": "0",
"max": "10",
"required": "true"
},{
"column": "StartDate",
"type": "date",
"min": "01-01-2010",
"max": "01-01-2024"
},{
"column": 8,
"type": "date",
"required": "true"
},{
"column": 9,
"type": "checkbox"
},{
"column": 10,
"type": "checkbox"
},{
"column": 11,
"type": "dropdown",
"data": [{"text":"","value":""}, {"text":"Subscribed","value":"1"}, {"text":"Unsubscribed","value":"2"}, {"text":"No data","value":"3"}],
"required": "true"
}]- Drag the Global Script called "EditableRow" into the event script
- Complete the Input properties for the script
- DataGridClass: The unique classname you assigned to the DataGrid
- LinkColumn: The column number (e.g. 1) OR the column property name as it appears in the list of columns and the column "Name" property
- FormFields: Select the List called "FormFields" from the dropdown
- IdentityColumn: The column number (e.g. 2) OR the column property name as it appears in the list of columns and the column "Name" property
- IdentityValue: The value from the IdentityColumn that uniquely identifies the row
- CallbackScript: The name of the page-level script that will process the updated data (e.g. SaveRow)
- Add a script under the Page (e.g. "SaveRow")
- Add an input parameter to the script (e.g. "RowData")
- Drag a Notification action into the script
- In the Notification Message property, select the RowData parameter from the Script Input Parameters category
- The Notification will display the updated row data as it is passed back to the "SaveRow" script. Example:
{
"ID":"1",
"FirstName":"Martina",
"LastName":"Vaughn",
"NoOfChildren":10,
"NoOfPets":9,
"StartDate":"2023-10-01",
"EndDate":"2023-10-02",
"Healthy":true,
"Happy":false,
"Subscription":1
}Returning an output parameter called "success" with the value of "false" from the callback script will result cause the form to remain in the DataGrid. This allows for the implementation of custom data validation functions in the callback script.
- Add an Output Parameter to the callback script called "success"
- Use a SetValue action in the callback script to set the "success" output parameter to "false" to prevent the form from being removed from the DataGrid
- In all other cases, the form will be removed from the DataGrid
The CSS below is required for the correct functioning of the module. Some elements can be customised using a variables CSS file.
Stadium 6.6 or higher
- Create a folder called "CSS" inside of your Embedded Files in your application
- Drag the two CSS files from this repo datagrid-inline-edit-variables.css and datagrid-inline-edit.css into that folder
- Paste the link tags below into the head property of your application
<link rel="stylesheet" href="{EmbeddedFiles}/CSS/datagrid-inline-edit.css">
<link rel="stylesheet" href="{EmbeddedFiles}/CSS/datagrid-inline-edit-variables.css">Versions lower than 6.6
- Copy the CSS from the two css files into the Stylesheet in your application
- Open the CSS file called datagrid-inline-edit-variables.css from this repo
- Adjust the variables in the :root element as you see fit
- Overwrite the file in the CSS folder of your application with the customised file
- Do not change any CSS other than the variables provided in the *-variables.css file
To upgrade the CSS in this module, follow the steps outlined in this repo
- Using an underscore (_) in the Control, Page or Template name can cause the script to break



