Skip to main content

Pagination

The Table Widget supports two pagination strategies in Data Mode. Both work with relational databases (MySQL, PostgreSQL, etc.) and NoSQL databases (DynamoDB, MongoDB, etc.).


Strategy 1 — Pagination Without Lazy Loading

Use this when your dataset is small enough to return all records in a single backend call.

How it works:

  • Enable Pagination in the widget properties
  • Leave Lazy Loading disabled
  • The backend returns all records in a single response
  • The widget handles client-side pagination display

Required response shape:

The backend response must include a tableMeta key with rowCount:

{
"widgetData": [ ... ],
"tableMeta": {
"rowCount": 100
}
}
var tableData = new List<Dictionary<string, string>>();

int totalCount = 100;

for (int i = 1; i <= totalCount; i++)
{
tableData.Add(new Dictionary<string, string>
{
{ "_id", i.ToString() },
{ "_origformcode", "" },
{ "_code", "" },
{ "_plugincode", "" },
{ "desc", "Test item" },
{ "qty", "1" },
{ "unit", "ea" },
{ "price", "100" }
});
}

response.Set(HandlerResponse.WidgetData, tableData);

var meta = _pluginContextProvider.GetDataTableMeta();
meta["rowCount"] = totalCount;

response.Set(HandlerResponse.TableMeta, meta);

Strategy 2 — Pagination With Lazy Loading

Use this for large datasets where returning all records at once is impractical or expensive.

How it works:

  • Enable both Pagination and Lazy Loading in the widget properties
  • Each time the user navigates to a new page, the widget sends an onTableLoadData event to the backend
  • The backend returns only the requested page of records
  • A continuation token (nextToken) tracks position across requests

Required response shape:

{
"widgetData": [ ... ],
"tableMeta": {
"rowCount": 500,
"lastRequestLimit": 20,
"nextToken": "<opaque-token>",
"qId": "<unique-query-id>"
}
}
FieldDescription
rowCountTotal number of records. Use -1 if the total is unknown or expensive to compute (e.g., DynamoDB).
lastRequestLimitThe rowsPerPage value from the request, echoed back in the response.
nextTokenAn opaque token your backend calculates to represent the position of the next page. The widget sends this token back on the next request.
qIdA unique identifier for this query. Use Guid.NewGuid().ToString() or uniqid().

Incoming request metadata (request.DataTableMeta):

FieldDescription
rowsPerPageHow many rows the widget is requesting.
nextTokenThe token returned by the previous response (empty string on first load).

Backend Examples

Amazon DynamoDB

DynamoDB uses its own LastEvaluatedKey as the continuation mechanism. Serialize it to JSON to use as nextToken.

using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
using System.Text.Json;

public async Task<IActionResult> RunEvent([FromBody] PluginContextEventRequestDto request)
{
var client = new AmazonDynamoDBClient();

int limit = request.DataTableMeta.rowsPerPage;

Dictionary<string, AttributeValue>? exclusiveStartKey = null;

// Deserialize the incoming nextToken back to a DynamoDB key
if (!string.IsNullOrEmpty(request.DataTableMeta.nextToken))
{
exclusiveStartKey = JsonSerializer.Deserialize<Dictionary<string, AttributeValue>>(
request.DataTableMeta.nextToken
);
}

var queryRequest = new QueryRequest
{
TableName = "Orders",
KeyConditionExpression = "pk = :pk",
ExpressionAttributeValues = new Dictionary<string, AttributeValue>
{
[":pk"] = new AttributeValue { S = "ORDER" }
},
Limit = limit,
ExclusiveStartKey = exclusiveStartKey
};

var result = await client.QueryAsync(queryRequest);

// Map DynamoDB items to table rows
var tableData = new List<Dictionary<string, string>>();

foreach (var item in result.Items)
{
tableData.Add(new Dictionary<string, string>
{
["_id"] = item["id"].S,
["desc"] = item["desc"].S,
["qty"] = item["qty"].N,
["unit"] = item["unit"].S,
["price"] = item["price"].N
});
}

response.Set(HandlerResponse.WidgetData, tableData);

// Serialize LastEvaluatedKey as the nextToken for the widget
string? nextToken = null;

if (result.LastEvaluatedKey != null && result.LastEvaluatedKey.Count > 0)
{
nextToken = JsonSerializer.Serialize(result.LastEvaluatedKey);
}

var meta = _pluginContextProvider.GetDataTableMeta();

meta["rowCount"] = -1; // Total count is expensive in DynamoDB
meta["lastRequestLimit"] = limit;
meta["nextToken"] = nextToken;
meta["qId"] = Guid.NewGuid().ToString();

response.Set(HandlerResponse.TableMeta, meta);

return Ok(response);
}

MySQL

MySQL supports OFFSET-based pagination. The nextToken carries the current offset between requests.

public async Task<IActionResult> RunEvent([FromBody] PluginContextEventRequestDto request)
{
int limit = request.DataTableMeta.rowsPerPage;

int offset = 0;
if (!string.IsNullOrEmpty(request.DataTableMeta.nextToken))
offset = int.Parse(request.DataTableMeta.nextToken);

using var conn = new MySqlConnection(connectionString);
await conn.OpenAsync();

// Fetch the requested page
var cmd = new MySqlCommand(@"
SELECT id, descr, qty, unit, price
FROM orders
ORDER BY id
LIMIT @limit OFFSET @offset", conn);

cmd.Parameters.AddWithValue("@limit", limit);
cmd.Parameters.AddWithValue("@offset", offset);

var reader = await cmd.ExecuteReaderAsync();

var tableData = new List<Dictionary<string, string>>();

while (await reader.ReadAsync())
{
tableData.Add(new Dictionary<string, string>
{
["_id"] = reader["id"].ToString(),
["desc"] = reader["descr"].ToString(),
["qty"] = reader["qty"].ToString(),
["unit"] = reader["unit"].ToString(),
["price"] = reader["price"].ToString()
});
}

reader.Close();

// Total count is inexpensive in MySQL
var countCmd = new MySqlCommand("SELECT COUNT(*) FROM orders", conn);
int totalCount = Convert.ToInt32(await countCmd.ExecuteScalarAsync());

response.Set(HandlerResponse.WidgetData, tableData);

var meta = _pluginContextProvider.GetDataTableMeta();

meta["rowCount"] = totalCount;
meta["lastRequestLimit"] = limit;
meta["nextToken"] = (offset + limit).ToString(); // next offset
meta["qId"] = Guid.NewGuid().ToString();

response.Set(HandlerResponse.TableMeta, meta);

return Ok(response);
}

Choosing a Pagination Strategy

ScenarioRecommendation
Small dataset (< a few hundred rows)Pagination only, return all records
Large dataset, relational DBPagination + Lazy Loading, use OFFSET or keyset
Large dataset, DynamoDBPagination + Lazy Loading, use LastEvaluatedKey as token
Total count is unknown or expensiveSet rowCount: -1