Skip to main content

DataTable Widget Events

The datatable widget is one of the most complex components and triggers multiple event types. Understanding these events is crucial for proper backend implementation.

Datatable Load Events

Triggered: When form renders or pagination changes Event Type: onTableLoadData or onTableEditDataLoad Async Behavior: Runs asynchronously when form loads

Request Example
{
"widgetName": "customerstbl",
"widgetEvent": "onTableLoadData",
"formData": { /* current form data */ },
"DataTableMeta": {
"serverPaginationEnabled": true,
"rowsPerPage": 50,
"pageIndex": 0,
"nextToken": "",
"previousToken": "",
"paginationDirection": "first",
"rowCount": 0,
"qId": ""
}
}
C# Handler Example
else if (eventType == "ONTABLELOADDATA")
{
// Parse pagination metadata
var tableMeta = requestBody.GetProperty("DataTableMeta");
int pageIndex = tableMeta.GetProperty("pageIndex").GetInt32();
int rowsPerPage = tableMeta.GetProperty("rowsPerPage").GetInt32();

// Load data from database with pagination
var tableData = await _dbContext.Customers
.Skip(pageIndex * rowsPerPage)
.Take(rowsPerPage)
.Select(c => new Dictionary<string, string>
{
{ "_id", c.Id.ToString() },
{ "_sk", c.Id.ToString() },
{ "_code", "CUSTOMER" },
{ "name", c.Name },
{ "email", c.Email },
{ "status", c.Status }
})
.ToListAsync();

response["widgetData"] = tableData;

// For server-side pagination
response["tableMeta"] = new
{
rowCount = await _dbContext.Customers.CountAsync(),
nextToken = "", // Your pagination token if needed
serverPaginationEnabled = true,
pageIndex = pageIndex,
rowsPerPage = rowsPerPage
};
}

Table Row Actions

Triggered: When user clicks action button in a table row Event Type: onTableRunActionEvent Actions: edit, delete, copy, confirm, sign, download, etc.

Request Example
{
"widgetName": "customerstbl",
"widgetEvent": "onTableRunActionEvent",
"widgetValue": "{\"action\":\"edit\",\"rowData\":{\"_id\":\"123\",\"_sk\":\"123\",\"_code\":\"CUSTOMER\"}}",
"widgetContext": "{\"referer\":\"/forms/main\",\"hasParentForm\":false}"
}
C# Handler Example
else if (widgetName == "CUSTOMERSTBL" && eventType == "ONTABLERUNACTIONEVENT")
{
// Parse the widget value JSON
var widgetValue = requestBody.GetProperty("widgetValue").GetString();
var actionData = JsonSerializer.Deserialize<JsonElement>(widgetValue);

string action = actionData.GetProperty("action").GetString();
var rowData = actionData.GetProperty("rowData");
string rowId = rowData.GetProperty("_id").GetString();

if (action == "edit")
{
// Open the customer form for editing
feCommands.Add(new
{
command = "OpenRecord",
args = new
{
id = Guid.NewGuid().ToString(),
pluginCode = "NONE",
formCode = "CUSTOMERFORM",
guid = rowId,
pGuid = (string)null,
isFormEditorMode = false,
originator = (object)null,
parent = (object)null,
folderData = (object)null,
prefillData = (object)null
}
});
}
else if (action == "delete")
{
// Delete the record from database
await _dbContext.Customers
.Where(c => c.Id.ToString() == rowId)
.ExecuteDeleteAsync();

// Show success message
feCommands.Add(new
{
command = "ShowMessage",
args = new
{
type = "success",
message = "Customer deleted successfully"
}
});

// Reload the table widget
feCommands.Add(new
{
command = "ReloadWidget",
args = new { widgetName = widgetName.ToLower() }
});
}
else if (action == "download")
{
// Trigger download of customer data
string filePath = await GenerateCustomerPDF(rowId);

feCommands.Add(new
{
command = "Download",
args = new
{
fileName = "customer.pdf",
filePath = filePath
}
});
}
}

Editable Table Row Actions

Triggered: When inline editing is enabled and user saves/edits a row Event Type: onTableEditRunActionEvent Actions: add, edit, delete (inline editing mode)

Request Example - Add New Row
{
"widgetName": "customerstbl",
"widgetEvent": "onTableEditRunActionEvent",
"widgetValue": "{\"action\":\"add\",\"data\":\"{\\\"name\\\":\\\"New Customer\\\",\\\"email\\\":\\\"new@example.com\\\"}\"}",
"formData": { /* current form data */ }
}
C# Handler Example
else if (widgetName == "CUSTOMERSTBL" && eventType == "ONTABLEEDITRUNACTIONEVENT")
{
// Parse the widget value JSON
var widgetValue = requestBody.GetProperty("widgetValue").GetString();
var actionData = JsonSerializer.Deserialize<JsonElement>(widgetValue);

string action = actionData.GetProperty("action").GetString();
string dataJson = actionData.GetProperty("data").GetString();
var rowData = JsonSerializer.Deserialize<Dictionary<string, string>>(dataJson);

if (action == "add")
{
// Add new row to database
var newCustomer = new Customer
{
Id = Guid.NewGuid(),
Name = rowData["name"],
Email = rowData["email"],
Status = rowData.GetValueOrDefault("status", "Active")
};

await _dbContext.Customers.AddAsync(newCustomer);
await _dbContext.SaveChangesAsync();

// Return updated table data
var tableData = await _dbContext.Customers
.Select(c => new Dictionary<string, string>
{
{ "_id", c.Id.ToString() },
{ "_sk", c.Id.ToString() },
{ "name", c.Name },
{ "email", c.Email },
{ "status", c.Status }
})
.ToListAsync();

response["widgetData"] = tableData;

// Update form data with the new table content
formData[widgetName.ToLower()] = JsonSerializer.Serialize(tableData);
response["formData"] = formData;
}
else if (action == "edit")
{
// Update existing row
var customerId = rowData["_id"];
var customer = await _dbContext.Customers.FindAsync(Guid.Parse(customerId));

if (customer != null)
{
customer.Name = rowData["name"];
customer.Email = rowData["email"];
customer.Status = rowData.GetValueOrDefault("status", customer.Status);

await _dbContext.SaveChangesAsync();
}

// Return updated table data
var tableData = await _dbContext.Customers
.Select(c => new Dictionary<string, string>
{
{ "_id", c.Id.ToString() },
{ "_sk", c.Id.ToString() },
{ "name", c.Name },
{ "email", c.Email },
{ "status", c.Status }
})
.ToListAsync();

response["widgetData"] = tableData;
formData[widgetName.ToLower()] = JsonSerializer.Serialize(tableData);
response["formData"] = formData;
}
}

Batch Actions

Triggered: When user selects multiple rows and performs batch action Event Type: onTableRunBatchActionEvent Actions: delete, download, createsigncontainer

Request Example - Batch Delete
{
"widgetName": "customerstbl",
"widgetEvent": "onTableRunBatchActionEvent",
"widgetValue": "{\"action\":\"delete\",\"data\":[{\"_sk\":\"123\",\"_code\":\"CUSTOMER\"},{\"_sk\":\"456\",\"_code\":\"CUSTOMER\"}]}"
}
C# Handler Example
else if (widgetName == "CUSTOMERSTBL" && eventType == "ONTABLERUNBATCHACTIONEVENT")
{
// Parse the widget value JSON
var widgetValue = requestBody.GetProperty("widgetValue").GetString();
var actionData = JsonSerializer.Deserialize<JsonElement>(widgetValue);

string action = actionData.GetProperty("action").GetString();
var dataArray = actionData.GetProperty("data").EnumerateArray();

var idsToProcess = dataArray
.Select(d => d.GetProperty("_sk").GetString())
.ToList();

if (action == "delete")
{
// Delete multiple records
await _dbContext.Customers
.Where(c => idsToProcess.Contains(c.Id.ToString()))
.ExecuteDeleteAsync();

// Show success message
feCommands.Add(new
{
command = "ShowMessage",
args = new
{
type = "success",
message = $"{idsToProcess.Count} customers deleted successfully"
}
});

// Return updated table data
var tableData = await _dbContext.Customers
.Select(c => new Dictionary<string, string>
{
{ "_id", c.Id.ToString() },
{ "_sk", c.Id.ToString() },
{ "name", c.Name },
{ "email", c.Email },
{ "status", c.Status }
})
.ToListAsync();

response["widgetData"] = tableData;
}
else if (action == "download")
{
// Generate ZIP file with all selected customer PDFs
string zipPath = await GenerateCustomersBatchPDF(idsToProcess);

feCommands.Add(new
{
command = "Download",
args = new
{
fileName = "customers.zip",
filePath = zipPath
}
});
}
}

Create Record Event

Triggered: When user clicks "Add New" button in table toolbar Event Type: onTableCreateRecordEvent or onFolderCreateRecordEvent

C# Handler Example
else if (widgetName == "CUSTOMERSTBL" && eventType == "ONTABLECREATERECORDEVENT")
{
// Open a new customer form
feCommands.Add(new
{
command = "OpenRecord",
args = new
{
id = Guid.NewGuid().ToString(),
pluginCode = "NONE",
formCode = "CUSTOMERFORM",
guid = "new",
pGuid = (string)null,
isFormEditorMode = false,
originator = (object)null,
parent = (object)null,
folderData = (object)null,
prefillData = new Dictionary<string, string>
{
{ "status", "New" },
{ "createdDate", DateTime.Now.ToString("yyyy-MM-dd") }
}
}
});
}

Checkbox Click Event

Triggered: When user clicks a checkbox in a table row Event Type: onTableRunActionEvent with action onrowcheckboxclicked

Request Example
{
"widgetName": "customerstbl",
"widgetEvent": "onTableRunActionEvent",
"widgetValue": "{\"action\":\"onrowcheckboxclicked\",\"rowData\":{\"_id\":\"123\",\"_sk\":\"123\"}}"
}
C# Handler Example
else if (eventType == "ONTABLERUNACTIONEVENT")
{
// Parse the widget value JSON
var widgetValue = requestBody.GetProperty("widgetValue").GetString();
var actionData = JsonSerializer.Deserialize<JsonElement>(widgetValue);

string action = actionData.GetProperty("action").GetString();

if (action == "onrowcheckboxclicked")
{
// Track selected rows or trigger related actions
// The frontend automatically manages selection state

var rowData = actionData.GetProperty("rowData");
string rowSk = rowData.GetProperty("_sk").GetString();

// Optionally store selection in widgetRelatedData
response["widgetRelatedData"] = new
{
customerstbl = new[] { rowSk }
};

// Or trigger custom logic based on selection
// For example, load related data when a row is selected
}
}

File Upload Event

Triggered: When user uploads files through datatable upload button Event Type: onFileUploadCreateDocumentEvent Note: Uses FormData multipart upload, not JSON. Requires a separate upload endpoint.

C# Upload Handler - Separate Endpoint
[HttpPost("uploadfiles")]
[AllowAnonymous]
public async Task<IActionResult> UploadFiles(
[FromForm] IFormFileCollection files,
[FromForm] string widgetName,
[FromForm] string widgetEvent,
[FromForm] string formData,
[FromForm] string widgetContext)
{
var uploadedFiles = new List<Dictionary<string, string>>();

foreach (var file in files)
{
// Save file to storage (S3, local disk, etc.)
var fileName = $"{Guid.NewGuid()}_{file.FileName}";
var filePath = Path.Combine("uploads", fileName);

using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}

// Create document record in database
var document = new Document
{
Id = Guid.NewGuid(),
FileName = file.FileName,
FilePath = filePath,
FileSize = file.Length,
ContentType = file.ContentType,
UploadedAt = DateTime.UtcNow
};

await _dbContext.Documents.AddAsync(document);

uploadedFiles.Add(new Dictionary<string, string>
{
{ "_id", document.Id.ToString() },
{ "fileName", document.FileName },
{ "fileSize", document.FileSize.ToString() }
});
}

await _dbContext.SaveChangesAsync();

// Return response with commands to refresh UI
var response = new Dictionary<string, object>
{
{ "success", true },
{ "uploadedFiles", uploadedFiles },
{
"FECommand", new List<object>
{
new
{
command = "ReloadWidget",
args = new { widgetName }
},
new
{
command = "ShowMessage",
args = new
{
type = "success",
message = $"{files.Count} file(s) uploaded successfully"
}
}
}
}
};

return Ok(response);
}

Important Notes

  1. Async Loading: When a form with multiple datatables loads, each table triggers its onTableLoadData event asynchronously and in parallel
  2. Required Row Fields: Always include _id, _sk, and _code in row data for proper action handling
  3. Pagination: Use DataTableMeta for server-side pagination implementation
  4. widgetRelatedData: Use this to track row selections across requests
  5. Performance: For large tables, always implement server-side pagination (backendpagination: true)