Background
Reports consist of PDF files that have been uploaded and assigned to a form using the NextGen Designer (NGD). From the Reports tab in NGD, form field values can be overlaid atop the PDF in order to generate a "completed" Report PDF when the form is submitted/finished by the Mi-Apps client. Reports can also be generated in HTML format directly on the Mi-Apps client, allowing them to be viewed or printed in the app even while offline.
Starting with version 12.0 of the Mobile Impact Platform, Reports can be scripted to generate customized page content. In this article we will walk through an example of how to use this feature to bind the contents of a Grid, which may have an indefinite number of rows, onto a report.
Design
Consider the following form example in which breed information is entered and then collected onto a grid named "Breeds on Premises."
Each row of the BreedsOnPremises grid represents one breed entry and will be display as a table row in the form's Report. (Note that the additional form field bindings - Bio-Security, Sanitation, Health of Birds - may be ignored for the purpose of this example.)
Each cell binding is configured on the Binding Properties panel with a row and column value corresponding to the source grid.
In this way, the report will contain a table of data that mirrors the contents of the grid.
Report generation script
The report can be generated in client script using _form.generateReportHTML() which will return an HTML representation of the completed form. In the code shown below, the report HTML is generated and then displayed in a popup window.
function showReport() { _form.generateReportHTML(0, { scale: 1.0, targetPrinting: false }) .then(function (html) { if (_form.platform.isWeb()) { var myWindow = window.open("", "My Report", ""); myWindow.document.write(html); myWindow.document.close(); setTimeout(function () { myWindow.focus(); myWindow.print(); }, 1000); } else { _form.printHTML(html); } }); }
Note: the _form.platform.isWeb() method is used to indicate whether the script code is running on a mobile device or in a web browser. In the case of a web browser, _form.platform.isWeb() is true and a new browser window is created. In the case of a mobile device, it is false and the report HTML can be printed directly using the built-in _form method.
Custom report generation
To further extend the form's capabilities, let's examine the custom report generation feature. You may have noticed that in this example so far we are limited to displaying up to the maximum number of rows contained in the Report's table (in this case, 15). If our form's grid contains more than 15 items, we won't see them all because we have only created bindings for 15 rows. Since the report PDF is a static page, we cannot grow the printed table to accommodate more items.
In the example shown below, some additional parameters are sent to _form.generateReportHTML:
function showReport() { _form.generateReportHTML(0, { scale: 1.0, targetPrinting: true, pageList: [1, 1, 1], beforeBindingRendered: setBindings }) .then(function (html) { if (_form.platform.isWeb()) { var myWindow = window.open("", "My Report", ""); myWindow.document.write(html); myWindow.document.close(); setTimeout(function () { myWindow.focus(); myWindow.print(); }, 1000); } else { _form.printHTML(html); } }); }
Two parameters have been added. pageList contains an array of numbers that represent the page numbers of the original report. Because 1 is provided three times, the generated report will contain three pages, each of which contains the first page of the original report. If a value of [1, 2, 2, 3] had been provided, the generated report would contain four pages with the second page from the original report being repeated once.
The second parameter is called beforeBindingRendered and specifies a callback function that your script will define. This callback function will be called once for every bound field on the report. This call will then be repeated for every page in the generated report. The reason the callback function is executed is to give you an opportunity to override the value that will be printed on the report. By default, the value that has been entered on the form by the Mi-Apps user will be used in the report. However, if a new value is returned by this callback function, then the new value will be printed on the report.
The callback method specified by beforeBindingRendered receives a single JSON object as an input parameter.
function setBindings(callbackParams) { //(to be implemented) }
The fields of this JSON object are defined as follows:
- fieldname: The name of the form field that is being overridden
- pageIndex: The overall page index, which represents the page number within the entire report that is being generated
- reportPage: The page number of the original report that is being used to generate this page
- pageInstance: The number of times this report page has been repeated
- gridCol, gridRow: The column and row number of the current cell (Optional; only used when callback is issued for a Grid field)
- imageNumber: The index of the current image (Optional; only used when callback is issued for an ImageAnnotation field)
Example: suppose the form script calls _form.generateReportHTML with a pageList of [1,2,3,2,4,2]. For a field that is bound to each page on the report, the callback function will be called six times (once per generated page) with the following values for callbackParams:
[pageIndex: 1,reportPage: 1, pageInstance:1],
[pageIndex: 2,reportPage: 2, pageInstance:1],
[pageIndex: 3,reportPage: 3, pageInstance:1],
[pageIndex: 4,reportPage: 2, pageInstance:2],
[pageIndex: 5,reportPage: 4, pageInstance:1],
[pageIndex: 6,reportPage: 2, pageInstance:3]
Spanning a grid across multiple pages
Let's get back to our original example in which we want our grid contents to span across multiple report pages. The first 15 rows of the grid can be displayed normally, according to the default behavior, on the first generated report page. This is no different from how things were done in our original example shown above. However, the second generated report page must contain rows 16-30. This is where we will use our callback method to change the values that are displayed on the report. Finally, the third generated report page will contain rows 31-45.
function setBindings(callbackParams) { const NUM_REPORT_ROWS = 15; if (callbackParams.fieldName == "BreedsOnPremises") { if (callbackParams.pageInstance > 1) { var row = (callbackParams.pageInstance - 1) * NUM_REPORT_ROWS + (callbackParams.gridRow - 1); if (row < _form.gridRowData("BreedsOnPremises").length) { return _form.gridRowData("BreedsOnPremises")[row][callbackParams.gridCol-1]; } } } }
A few notes about this code:
- NUM_REPORT_ROWS defines the number of rows that are displayed on a single page of the report. Each subsequent page after the first will display the row corresponding to the original row index offset by a multiple of NUMBER_REPORT_ROWS.
- An extra check is performed to verify that the calculated row is less than the length of the grid. In the case that the grid contains, say, 43 rows worth of data, then we cannot display rows 44 and 45 on the third page because they do not exist.
- Once the index of the row to be displayed has been calculated, the callback method returns the grid value corresponding to that row and the original column index.
- Row and column indexes sent as callback parameters are 1-indexed, however the function _form.gridRowData accepts parameters as zero-indexed.
- Also note that
In this example we are generating three pages as a fixed report length. This was done for simplicity, however in a typical scenario the number of generated pages will depend on the contents of the grid. To do this, the value of pageList sent to generateReportHTML will need to be dynamically generated.
var numPages = _form.gridRowData("BreedsOnPremises").length / NUM_REPORT_ROWS + 1 var pageList = []; for (var i=0; i<numPages; i++) { pageList.push(1); }