This article is a follow up to the Page Scrolling Sample
article I posted a little while ago.
The Page Scrolling Sample
article explains how to build a form that can capture information for any number of applicants. Where it appears that each applicant data is captured in its own form page, but in reality, the form is a single page form that simply changes which applicant it is currently displaying. This is great from a performance stand point, but requires some special attention if that data is needed to be printed. This article explains how to do that.
In order to do that we're going to need to be able to dynamically add the required number of pages to the form so that each page contains its own applicant. We'll also talk about some rather useful but not very well known print functions to handle pagination.
We're going to be printing a many page form. So it's likely desired to have a footer that contains Page X of Y at the bottom of each page. In a form with a fixed number of pages, this is easy enough, but in this form, at design time, we do not know how many pages are going to be required to print the contents, so we'll need something a little more flexible.
IBM Forms has a good number of properties that control how the form is printed. These are found in the forms global.global.printsettings.
printsettings include a way to handle what shows up in the header and footer of the form. The property we want to take advantage of is the footer property. If text is placed in one of the footer properties, then it'll show up in the footer of every form page that is printed. The footer text (as with every property within IBM Forms) can also be determined from the result of a compute. The footer property (as with the header property) have three sub options: left, right, and center - which control where the text will be displayed in the footer. In our example, we'll put our Page X of Y text in the right footer, so it'll always show up at the bottom right hand corner of the printed page.
If you look in the attached form, the right footer contains a compute:
'Page ' +. viewer.printSheet() +. ' of ' +. viewer.printTotalSheets()
The viewer.printSheet() function returns the current printed page number, and the viewer.printTotalSheets() function returns the total number of pages in the print job. So with this simple compute every printed page will have the correct Page X of Y. These 2 viewer package functions have been built into the Webform Server as well, but only when printng via PDF (controled via the printsettings ouputformat.)
The other printsetting property this form takes advantage of is the pages property - which is a filter that specifies which form pages should be printed. Here we specify that we want to omit the wizardPage from printing, but we want to print all other pages, so we use the 'omit' filter and specify the sid of the pages we do not wish to print.
Here's the entire printsettings we require for our form:
<right compute="'Page ' +. viewer.printSheet() +. ' of ' +. viewer.printTotalSheets()"></right>
Now that our form is setup to print the way we want, we need to add the logic that adds the appropriate number of pages to the form, and also ensures that each page is printing its own applicant. This is going to live in a compute.
The first thing we need to do is create that compute so that it fires only when the form is printing. The global.global.printing property changes from 'off' to 'on' when the form is being printed, so we'll start our compute with that:
<custom:onPrint xfdl:compute="toggle(global.global.printing, 'off', 'on') == '1' ? [perform print logic here] : ''"></custom:onPrint>
Then we'll need to figure out the number of applicants we'll need to print out, so we can build a compute that can calculate that. The applicant data lives in the formData instance, so we can use the get function for that
get('count(instance("formData")/applicants/applicant)', '', 'xforms')
The compute above uses the get() function, but it calls an xpath expression using the xpath function count() - so we count the number of applicants, and store the result in a global variable we'll use global.global.custom:numPages for that:
set('global.global.custom:numPages', get('count(instance("formData")/applicants/applicant)', '', 'xforms'))
now we want to use the forLoop function, so that we can perform some operations once for every applicant
forLoop('global.global.custom:index', '1', global.global.custom:numPages - '1', [put compute logic here that is needed for each applicant] )
which essentially will repeat once for every page we need - which is great. Now we need to create a page for every applicant, and tell that page to show a given applicant. You'll notice we've created a new global variable called global.global.custom:index - this will be incremental by 1 once for every time the loop runs, so effectively keeps our current page / applicant number. You'll see it referenced later.
We can create a new page using the duplicate function.
duplicate('printingPage1', 'page', 'global.global.custom:temp' , 'option', 'append_child', 'newPage')
The above code duplicates the printingPage1, and copies all its XML into a custom:temp option. The reason we need to do this is so that we can change an xpath reference of what's inside the page. If we attempted to do this to an actual page, the XForms engine would complain and throw errors, so we copy the page into a location where the XForms engine does not see it. Then once we setup the page the way we want, we'll copy the updated page into the form as a real page.
but before we copy the page into the form, we needed to update an xpath reference inside that page. We just duplicated the printingPage1 page, and it is currently setup to show the first applicant
<xforms:group ref="instance('formData')/applicants/applicant[position() = 1]">
That's great for the first page, but we need to change the ref attribute to reference the desired applicant for the new page we've created. We can use the setAttr function for that.
setAttr('global.global.custom:temp[page][xforms:group]', 'array', 'xforms:ref', 'instance("formData")/applicants/applicant[position() = ' +. global.global.custom:index +. ']')
Now we can copy this new page into the actual form. We use another call to duplicate to do this, but this time in the the last parameter we provide the new page a new sid (pages must have unique sids)
duplicate('global.global.custom:temp[page]', 'option', 'printingPage' +. (global.global.custom:index -'1') , 'page', 'after_sibling', 'printingPage' +. global.global.custom:index)
so the 2nd printed page will be called printingPage2, and the 3rd, printingPage3, etc.
So the loop will continue to run such that for every applicant, it will create a new page, and that new page will be assigned to show a given applicant. All this happens before the form is sent to print, so in the end the printed form should contain a printed page for every applicant.
If you wish to see the entire compute, I'd recommend using the compute editor of the Designer, as it preserves tabbing and indenting which helps to read and understand the compute logic. You can find the compute by selecting the globalpage -> Form Global in the outline view, and then the properties view, you can find the custom:onPrint compute under the Miscellaneous section.
So in this form we've used the technique of copying some xforms based content into a temporary location (hidden from the XForms engine), modified some xpath, and then copied it back into the form, where the xforms engine can see it, and it'll react appropriately. This can be done with any type of xforms based content, for example, a table. The same technique could be used to print a record scrolling table across many print pages -which i'll likely post a sample of at a later time.
A final note about the techniques used here. This form is setup with ondemand page loading enabled for a reason: When this technique of changing xpath at runtime was devised by development, they had planned to have the source content (ie our page to be copied) in a temporary variable from the start (as opposed to copying it there). I found that very difficult to work with, as the designer does not see the content, so you're either stuck with a lot of copy / pasting or stuck working strictly in source view. So i attempted to find a solution that would allow me to use the designer to build and maintain my printed pages, yet still dynamically duplicate the page at runtime. A few things were required to do this, on demand page loading (so the Viewer / WFS doesn't load the printing pages until print time), a bit more complicated compute, and all items on the printed page needed to be in panes (workaround for a bug). So if these restrictions are not acceptable to your application, then you can store the source content in a temporary variable right from the start (and you don't have to use on demand page loading, nor do you have to have all items inside panes).