Long instructions...
Note that @████████'s stored procedure example works a little differently than mine. His takes in a page size and page count, whereas mine takes in the index number and page size. His is smarter because it would save the intermediate step I'm doing in LFForms to calculate the index from the page number. But my example is already fully built and tested this way, so I didn't want to edit it.
This is all done in on-prem Forms Version 11 Update 2.
Step 1 - Set-up two stored procedures in your database. One will do your record search, and the other will just return the total number of records for the same search.
USE [database_name_goes_here]
GO
/****** Object: StoredProcedure [dbo].[ExamplePaginated] Script Date: 5/17/2022 10:08:28 AM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author: Matthew Tingey
-- Create date: May 17, 2022
-- Description: Returns a list of database records, in a paginated format.
-- =============================================
CREATE PROCEDURE [dbo].[ExamplePaginated]
-- Add the parameters for the stored procedure here
@startRowIndex int,
@pageSize int,
--The @startRowIndex and @pageSize are the parameters for the paging, any other parameters are search criteria
--for use in the WHERE statement and need to match the other stored procedure.
@param1 VARCHAR(25),
@param2 VARCHAR(25)
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Get the paginated records
SELECT
e.[column1],
e.[column2],
e.[column3],
e.[column4],
e.[column5]
FROM [database_name_goes_here].[dbo].[table_name_goes_here] AS e
-- NOTE THAT THE WHERE STATEMENT MUST MATCH THE OTHER STORED PROCEDURE
WHERE
(
e.[column1] = @param1 --this checks for exact match of the parameter
OR
e.[column1] LIKE CONCAT('%', @param1, '%') --this checks for partial matches of the parameter
OR
@param1 = 'All' --this allows the parameter to be sent as "All" and return everything
)
AND
(
e.[column2] = @param2 --this checks for exact match of the parameter
OR
e.[column2] LIKE CONCAT('%', @param2, '%') --this checks for partial matches of the parameter
OR
@param2 = 'All' --this allows the parameter to be sent as "All" and return everything
)
ORDER BY e.[column1]
OFFSET @startRowIndex ROWS FETCH NEXT @pageSize ROWS ONLY
END
GO
USE [database_name_goes_here]
GO
/****** Object: StoredProcedure [dbo].[ExamplePaginatedTotal] Script Date: 5/17/2022 10:08:28 AM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author: Matthew Tingey
-- Create date: May 17, 2022
-- Description: Returns a total count of database records, for use with the paginated report.
-- =============================================
CREATE PROCEDURE [dbo].[ExamplePaginatedTotal]
-- Add the parameters for the stored procedure here
--The @startRowIndex and @pageSize parameters are not used on this procedure, but any other parameters are search criteria
--for use in the WHERE statement and need to match the other stored procedure.
@param1 VARCHAR(25),
@param2 VARCHAR(25)
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Get the paginated records
SELECT
COUNT(e.[column1]) AS total_count
FROM [database_name_goes_here].[dbo].[table_name_goes_here] AS e
-- NOTE THAT THE WHERE STATEMENT MUST MATCH THE OTHER STORED PROCEDURE (THE TOTALS)
WHERE
(
e.[column1] = @param1 --this checks for exact match of the parameter
OR
e.[column1] LIKE CONCAT('%', @param1, '%') --this checks for partial matches of the parameter
OR
@param1 = 'All' --this allows the parameter to be sent as "All" and return everything
)
AND
(
e.[column2] = @param2 --this checks for exact match of the parameter
OR
e.[column2] LIKE CONCAT('%', @param2, '%') --this checks for partial matches of the parameter
OR
@param2 = 'All' --this allows the parameter to be sent as "All" and return everything
)
END
GO
Step 2 - In Forms, add the following fields to your form. The labels do not matter because the fields are going to be rearranged/hidden by Javascript.
- Drop-down field. Leave it editable. Add choices like (10, 25, 50, 100, 250, 500). Set 10 as the default value. CSS Class Name (and variable name): page_size
- Number field. Leave it editable. Zero decimal places. Thousands delimiter off. Default value of 0. Formula: =((current_page-1)*page_size) CSS Class Name (and variable name): hidden_field_start_row_index
- Number field. Leave it editable. Zero decimal places. Thousands delimiter off. Default value of 0. CSS Class Name (and variable name): hidden_field_total_record_count
- Number field. Leave it editable. Range allowed 1-9999. Zero decimal places. Thousands delimiter off. Default value of 1. CSS Class Name (and variable name): current_page
- Number field. Mark it read-only. Range allowed 1-9999. Zero decimal places. Thousands delimiter off. Default value of 1. Formula: =ROUNDDOWN((hidden_field_total_record_count)/page_size)+1 CSS Class Name (and variable name): last_page
- Add any other fields for the search parameters you included in your stored procedures.
Step 3 - Add your table to the form, formatting it as you desire.
Step 4 - Add a custom HTML element below your table.
<div style="padding-bottom:8px" id="tableFooterPagination">
<div style="display: inline-block; width: 18%; text-align: left" id="tableFooterCurrentRange">
<b><i>0 - 0 of 0</i></b>
</div>
<div style="display: inline-block; width: 52%; text-align: center" id="tableFooterNavigation">
<div style="display: inline-block; width: 18%;">
</div>
<div style="display: inline-block; width: 6%;">
<button type="button" id="tableFooterNavigateLeft2">
<<
</button>
</div>
<div style="display: inline-block; width: 1%;">
</div>
<div style="display: inline-block; width: 4%;">
<button type="button" id="tableFooterNavigateLeft1">
<
</button>
</div>
<div style="display: inline-block; width: 1%;">
</div>
<div style="display: inline-block; width: 10%; text-align: center" id="tableFooterNagivateCurrentPage">
</div>
<div style="display: inline-block; width: 1%;">
</div>
<div style="display: inline-block; width: 2%; text-align: center">
of
</div>
<div style="display: inline-block; width: 1%;">
</div>
<div style="display: inline-block; width: 10%; text-align: center" id="tableFooterNagivateLastPage">
</div>
<div style="display: inline-block; width: 1%;">
</div>
<div style="display: inline-block; width: 4%;">
<button type="button" id="tableFooterNavigateRight1">
>
</button>
</div>
<div style="display: inline-block; width: 1%;">
</div>
<div style="display: inline-block; width: 6%;">
<button type="button" id="tableFooterNavigateRight2">
>>
</button>
</div>
<div style="display: inline-block; width: 18%;">
</div>
</div>
<div style="display: inline-block; width: 18%; text-align: right" id="tableFooterShowItems">
<b>Items per Page: </b>
</div>
</div>
Step 5 - Set-up lookups to populate your table and the total count from the two Stored Procedures you added in step 1.

Step 6 - Add Javascript to the form to rearrange the fields and manage the navigation of the table pages.
$(document).ready(function () {
//Changes to the form layout when it is loaded to facilitate the table footer.
$('.page_size').hide();
$('.hidden_field_total_record_count').hide();
$('.current_page').hide();
$('.last_page').hide();
$('.hidden_field_start_row_index').hide();
$('.page_size select').insertAfter('#tableFooterShowItems').addClass('page_size_select');
$('.page_size_select').css('width', '7%');
$('.current_page input').appendTo('#tableFooterNagivateCurrentPage').addClass('current_page_input');
$('.last_page input').appendTo('#tableFooterNagivateLastPage').addClass('last_page_input');
$('.current_page_input').css('text-align', 'center');
$('.last_page_input').css('text-align', 'center');
$('.current_page_input').css('width', '100%');
$('.last_page_input').css('width', '100%');
//Set the current page field to a max of the last page field.
$('.last_page_input').change(function() {
$('.current_page_input').attr('max', parseInt($('.last_page_input').val()));
});
//Reset the current page to 1 if the page size is changed.
$('.page_size_select').change(function() {
$('.current_page_input').val(1).trigger('change');
});
//Function that can be called (and re-called as needed) to set-up and manage
//the tableFooter for navigating the table details.
function UpdateFooterDetails()
{
var startIndex = parseInt($('.hidden_field_start_row_index input').val()) + 1;
var endIndex = parseInt($('.hidden_field_start_row_index input').val()) + parseInt($('.page_size_select').val());
var totalCount = parseInt($('.hidden_field_total_record_count input').val());
if(endIndex >= totalCount)
{
endIndex = totalCount;
}
var newHTML = '<b><i>' + startIndex + '-' + endIndex + ' of ' + totalCount + '</i></b>';
$('#tableFooterCurrentRange').html(newHTML);
$('.page_size_select').css('width', '7%');
$('.current_page_input').css('width', '100%');
$('.last_page_input').css('width', '100%');
if(parseInt($('.current_page_input').val()) == 1)
{
$('#tableFooterNavigateLeft2').attr('disabled', 'true').css('opacity',0.5);
$('#tableFooterNavigateLeft1').attr('disabled', 'true').css('opacity',0.5);
}
else
{
$('#tableFooterNavigateLeft2').removeAttr('disabled').css('opacity',1);
$('#tableFooterNavigateLeft1').removeAttr('disabled').css('opacity',1);
}
if(parseInt($('.current_page_input').val()) >= parseInt($('.last_page_input').val()))
{
$('#tableFooterNavigateRight1').attr('disabled', 'true').css('opacity',0.5);
$('#tableFooterNavigateRight2').attr('disabled', 'true').css('opacity',0.5);
}
else
{
$('#tableFooterNavigateRight1').removeAttr('disabled').css('opacity',1);
$('#tableFooterNavigateRight2').removeAttr('disabled').css('opacity',1);
}
}
//Trigger the update of footer details when form loads and when lookups are complete.
UpdateFooterDetails();
$(document).on('lookupcomplete', function() {
UpdateFooterDetails();
});
//Change the page number when the page navigation buttons are clicked.
$('#tableFooterNavigateLeft2').click(function() {
$('.current_page_input').val(1).trigger('change');
UpdateFooterDetails();
});
$('#tableFooterNavigateLeft1').click(function() {
$('.current_page_input').val(parseInt($('.current_page_input').val()) - 1).trigger('change');
UpdateFooterDetails();
});
$('#tableFooterNavigateRight1').click(function() {
$('.current_page_input').val(parseInt($('.current_page_input').val()) + 1).trigger('change');
UpdateFooterDetails();
});
$('#tableFooterNavigateRight2').click(function() {
$('.current_page_input').val(parseInt($('.last_page_input').val())).trigger('change');
UpdateFooterDetails();
});
});
The end result should look something like this:
