print all data in paginated table/grid

direct Tabular data display

Recently our project has a page need to show tabular data from 30-6000 rows with 3-4 columns. At first, I thought this is pretty reasonable data to show in one page so I just throw the data into a ng-repeat table with my own implementation of filtering/sorting which is pretty straightforward in angular. Every time user select new category/type, fetch data from backend and replace the data in vm/$scope. Also with this implementation, it is quite easy to fulfill our another requirement which is export/print the page content. For export I just need to get the DOM content to the server and return as downloadable. For print, even easier, just call window.print() ,that’s it.

Performance issue with IE

Everything works fine until  our QA hits IE which is super slow when the data in the list is replaced from backend. Did some profiling in IE11, turns out the appendChild and removeChild calls are taking forever when it tries to clear the rows in the dom and put the new elements into dom. Also another slowness is from styleCalculation which it does for every column/row. Overall, IE takes 20s to render a page with 5000 rows and FF/safari/chrome need only 1-2 seconds. This forces us to abandon the straightforward way to use the more IE friendly way which is pagination with angular ui-grid. But this brings us to another problem which is print since data is now paginated and DOM only has 20 rows.

Server side render and client side print

What I eventually did is sending the model data back to server and do server side rendering and eventually send back to browser where an iFrame is created on the fly for printing. The pros of doing this is we have a lot of flexibility on content/layout by whatever manipulation/styling etc… The cons is we added more stuff to the stack and one more round trip comparing to the direct print.

server side

So on server side, when we get the REST call for print, we have a Thymeleaf template there for generating the html. I compared different java server side rendering engines like Velocity/Freemaker/Rythm etc, looks like Thymeleaf has the best Spring integration and most active development/release.

@Configuration
public class ThymeleafConfig
{
    @Autowired
    private Environment env;

    @Bean
    @Description("Thymeleaf template rendering HTML ")
    public ClassLoaderTemplateResolver exportTemplateResolver() {
        ClassLoaderTemplateResolver exportTemplateResolver = new ClassLoaderTemplateResolver();
        exportTemplateResolver.setPrefix("thymeleaf/");
        exportTemplateResolver.setSuffix(".html");
        exportTemplateResolver.setTemplateMode("HTML5");
        exportTemplateResolver.setCharacterEncoding(CharEncoding.UTF_8);
        exportTemplateResolver.setOrder(1);
        //for local development, we do not want template being cached so that we could do hot reload.
        if ("local".equals(env.getProperty("APP_ENV")))
        {
            exportTemplateResolver.setCacheable(false);
        }
        return exportTemplateResolver;
    }

    @Bean
    public SpringTemplateEngine templateEngine() {
        final SpringTemplateEngine engine = new SpringTemplateEngine();
        final Set<ITemplateResolver> templateResolvers = new HashSet<>();
        templateResolvers.add(exportTemplateResolver());
        engine.setTemplateResolvers(templateResolvers);
        return engine;
    }
}

With the engine we confined, we could used like:

            Context context = new Context();
            context.setVariable("firms", firms);
            context.setVariable("period", period);
            context.setVariable("rptName", rptName);
            context.setVariable("hasFirmId", hasFirmId);
            if (hasFirmId)
            {
                context.setVariable("firmIdType", FirmIdType.getFirmIdType(maybeFirmId).get());
            }

            return templateEngine.process("sroPrint", context);

Template with name sroPrint has some basic Theymleaf directives:

<html xmlns:th="http://www.thymeleaf.org">
<head>
<style>
    table thead tr th, table tbody tr td {
      border: 1px solid black;
      text-align: center;
    }
  </style>

</head>
<body>
<div>
<h4 th:text="${rptName}">report name</h4>
<div style="margin: 10px 0;"><b>Period:</b> <span th:text="${period}"></span>
<div>
  <h4 th:text="${rptName}">report name</h4>
  <div style="margin: 10px 0;"><b>Period:</b> <span th:text="${period}"></span></div>
  <table style="width: 100%; ">
    <thead>
    <tr>
      <th th:if="${hasFirmId}" th:text="${firmIdType}"></th>
      <th>crd #</th>
      <th>Firm Name</th>
    </tr>
    </thead>
    <tbody>
    <tr th:each="firm : ${firms}">
      <td th:if="${hasFirmId}" th:text="${firm.firmId}"></td>
      <td th:text="${firm.crdId}">CRD</td>
      <td th:text="${firm.firmName}">firm name</td>
    </tr>
    </tbody>
  </table>
</div>
</body>
</html>

client side

Now on the client side we need to consume the HTML string from the client side. The flow is we create an iFrame, write the html into it and call browser print on that iFrame and remove the element from DOM. The below implementation is inside the success callback of $http call for getting that dom string. It is in pure js without jQuery, with which it might be a bit more concise.


var printIFrame = document.createElement('iframe');
document.body.appendChild(printIFrame);
printIFrame.style.position = 'absolute';
printIFrame.style.top = '-9999px';
printIFrame.style.left = '-9999px';
var frameWindow = printIFrame.contentWindow || printIFrame.contentDocument || printIFrame;
var wdoc = frameWindow.document || frameWindow.contentDocument || frameWindow;
wdoc.write(res.data);
// tell browser write finished
wdoc.close();
$scope.$emit('UNLOAD');
// Fix for IE : Allow it to render the iframe
frameWindow.focus();
try {
    // Fix for IE11 - printng the whole page instead of the iframe content
    if (!frameWindow.document.execCommand('print', false, null)) {
        // document.execCommand returns false if it failed -http://stackoverflow.com/a/21336448/937891
        frameWindow.print();
    }
    // focus body as it is losing focus in iPad and content not getting printed
    document.body.focus();
}
catch (e) {
    frameWindow.print();
}
frameWindow.close();
setTimeout(function() {
    printIFrame.parentElement.removeChild(printIFrame);
}, 0);

PDF/XLS Export

For xls/pdf export, it is similar to the other POST that I have before. The only difference is the dom string was passed from client there. Here we generate the dom string in server side.

angularjs bootstrapui print iframe modal

1. Print modal fit in one screen

If the content of the modal is in one screen, we could just put a css in the modal template to hide all other content

/*general styles*/
.printable{display:none;}
/* print styles*/
@media print {
 .printable {display:block;}
 .screen {display:none;}
}

Or put a wrap to the parent content and assign an id to it then in the print css, use the id and set it as display: none

2. Print modal with long iframe

If we load a iframe with long content in the modal, the above print will just print the on-screen part content which is not desirable since we definite want to print the whole iframe.

2.1 add a function in the modal controller

	$scope.printIframe = function (iFrameId) {
		var frame = window.frames[iFrameId];
		if (!frame) {
			console.log('No iFrame found for id: ' + iFrameId);
			return;
		}
		frame.focus();
		frame.print();
	};

2.2 assign id to the iframe and call the print function from the template

      <iframe ng-src="{{request.url}}" frameborder="0" style="overflow-x:hidden; height: 100%; width: 100%;" id="modalIframe" name="modalIframe"></iframe>

....

<button class="btn btn-primary" ng-click="printIframe('modalIframe')">Print</button>

The other part is just regular setup

//popup for detail data
$scope.openDetail = function (mmid) {
//	$event.preventDefault();
	var detailUrl = '/someurl?rptid=' + $scope.rptId + '&firmname=' + $scope.reportData.firmName + '&mmid=' + mmid;
	$scope.detailRequest = {url:detailUrl};
	$modal.open({
		templateUrl: 'detailModalContent.html',
		controller: 'ModalInstanceCtrl',
		resolve: {
			request: function () {
				return $scope.detailRequest;
			}
		},
		// set width to 900px(bootstrap);
		size: 'lg'
	});
};

3. Print modal with long content(no iFrame)

The problem here is if the content is longer than the modal view port, then we can only see the viewable area in the print.
The only CSS that works for me when the modal is LONGER than the view port is:

@media print {
    .modal {
        position: absolute;
        left: 0;
        top: 0;
        margin: 0;
        padding: 0;
        overflow: visible!important;
    }
}

Note: position:absolute and overflow:visible are MUST have.

export html to excel/pdf with angularjs and servlet(spring mvc)

As we konw, the XHR request that angularjs uses cannot init a download.

iFrame version

One way to achieve is thru an invisible iframe and assign the url to the iframe for download.

The basic idea is to construct a iframe on the fly and append a html FORM in it and then submit the form to server in javascript. On the server side, return the stream with ‘Content-disposition’ header.

Angularjs code

just pass in fileType(here is pdf/xls) in your ng-click or from some other delegations

	$scope.exportReport = function (fileType) {
		var printIframe = angular.element("<iframe class='hidden'>");
		var formElement = angular.element("<form>");
		formElement.attr("action", "/scrc/rest/trace/export/" + fileType);
		formElement.attr("method", "post");
		var contentElement = angular.element("<input>").attr("type", "hidden").attr("name",
				"domContent").val(angular.element('.report-outer-wrapper').html());
		//build file name
		var fileName = $scope.reportData.mpid + '_' + $scope.reportData.periodDate.replace(' ', '_') + '_' + $scope.reportData.viewType;
		var fileNameElement = angular.element("<input>").attr("type", "hidden").attr("name",
				"fileName").val(fileName);
		formElement.append(contentElement);
		formElement.append(fileNameElement);
		printIframe.append(formElement);
		angular.element('body').append(printIframe);
		formElement.submit();
	};

Java code in Spring controller

The buildExportContent() is just to put the style and content into a html’s head and body and return a String.

    @RequestMapping(value = "/export/{fileType}", method = RequestMethod.POST)
    @ResponseBody
    public void handleExport(@PathVariable("fileType") String fileType, @RequestParam("domContent") String domContent,
        @RequestParam("fileName") String fileName, HttpServletResponse response) throws IOException, DocumentException
    {
        OutputStream out = response.getOutputStream();
        String finalFileName = fileName + "." + fileType;
        response.setHeader("Content-disposition", "attachment; filename=" + finalFileName);
        String styles = "";
        if ("pdf".equalsIgnoreCase(fileType))
        {
            String bootstrapStylePath = context.getRealPath("/css/bootstrap/bootstrap.min.css");
            styles = Files.toString(new File(bootstrapStylePath), StandardCharsets.UTF_8);
            //we need xhtml here.
            String exportContent = reportService.buildExportContent(domContent, styles).replaceAll("<br>", "<br/>");
            ITextRenderer renderer = new ITextRenderer();
            renderer.setDocumentFromString(exportContent);
            renderer.layout();
            renderer.createPDF(out);
        }
        else if ("xls".equalsIgnoreCase(fileType))
        {
            styles = " table{border:1px solid gray !important}; " + " td, th {border:1px solid gray !important}; ";
            //we need xhtml here.
            String exportContent = reportService.buildExportContent(domContent, styles).replaceAll("<br>", "<br/>");
            out.write(exportContent.getBytes(StandardCharsets.UTF_8));
        }
        out.close();

    }

Blob version

another way is to use Blob and generate the url with browser. however only IE>10 support this. WHat a sad story.

<a download="content.txt" ng-href="{{ url }}">download

in your controller:

var content = 'file content';
var blob = new Blob([ content ], { type : 'text/plain' });
$scope.url = (window.URL || window.webkitURL).createObjectURL( blob );

in order to enable the URL:

app = angular.module(...);
app.config(['$compileProvider',
    function ($compileProvider) {
        $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|tel|file|blob):/);
});

Please note that

Each time you call createObjectURL(), a new object URL is created, even if you’ve already created one for the same object. Each of these must be released by calling URL.revokeObjectURL() when you no longer need them. Browsers will release these automatically when the document is unloaded; however, for optimal performance and memory usage, if there are safe times when you can explicitly unload them, you should do so.

Source: MDN

Another blob version

Pure html Form version

Bassically use a HTML form with ng-submit, on return add ‘Content-disposition’ header so that the page would not re-direct but just a file download.

code in Angular:

I did some customization to the form element so that the content of the report could be posted to the server side for the flying saucer to render and process to pdf file.

	$scope.printExcel = function (e) {
		var form = angular.element(e.target);
		form.attr("action", "/scrc/rest/trace/pdf");
		var contentElement = angular.element("<input>").attr("type", "hidden").attr("name",
				"printContent").val(angular.element('.report-outer-wrapper').html());
		form.append(contentElement);
		form.submit();
	};

HTML:

                       <form name="printPdfForm" method="POST" ng-submit="printPdf($event)">
                           <button type="submit" class="print-button">
                              <img src="/scrc/images/icon_pdf.gif" border="0" alt="download pdf"/>
                           </button>
                        </form>

Code in Spring controller.

The excel version is basically just return to the client the same content with a Content-disposition header which is for the origin server to suggest a default filename if the user requests that the content is saved to a file. It would be nice if we could do this in the client side with javascipt so that we can save a trip(Please notify me if you know how to do it.).

    @RequestMapping(value = "/pdf", method = RequestMethod.POST)
    @ResponseBody
    public void handlePdf(@RequestParam("printContent") String printContent, HttpServletResponse response) throws Exception
    {
        String fileName = "result.pdf";
        response.setHeader("Content-disposition", "attachment; filename=" + fileName);
        OutputStream out = response.getOutputStream();

        String content = printContent.replaceAll("<br>", "<br/>");
        ITextRenderer renderer = new ITextRenderer();
        renderer.setDocumentFromString(getPdfHtml(content));
        renderer.layout();

        renderer.createPDF(out);
        out.close();
    }

    private String getPdfHtml(String content) throws Exception
    {
        StringBuilder sb = new StringBuilder();
        sb.append("<html>");
        sb.append("<head><style language='text/css'>");
        //if performance is bad we could ignore this big css
        String rcStylePath = context.getRealPath("/css/bootstrap/bootstrap.min.css");
        sb.append(Files.toString(new File(rcStylePath), StandardCharsets.UTF_8));
        sb.append("</style></head>");
        sb.append("<body>");
        sb.append(content);

        sb.append("</body>");
        sb.append("</html>");

        return sb.toString();
    }

    @RequestMapping(value = "/excel", method = RequestMethod.POST)
    @ResponseBody
    public void handleExcel(@RequestParam("printContent") String printContent, HttpServletResponse response) throws IOException
    {
        String fileName = "result.xls";
        response.setHeader("Content-disposition", "attachment; filename=" + fileName);
        OutputStream out = response.getOutputStream();

        out.write(getExcelHtml(printContent).getBytes(StandardCharsets.UTF_8));
        out.close();
    }

    private String getExcelHtml(String content) throws IOException
    {
        StringBuilder sb = new StringBuilder();
        sb.append("<html>");
        sb.append("<head><style language='text/css'>");

        sb.append(" table{border:1px solid gray !important}; ");
        sb.append(" td, th {border:1px solid gray !important}; ");
        sb.append("</style></head>");
        sb.append("<body>");
        sb.append(content);

        sb.append("</body>");
        sb.append("</html>");

        return sb.toString();
    }

For some IE8 issue, look at my other POST

page-break property in css

There isn’t an actual page-break property in CSS. It is actually a set of 3 properties: page-break-before, page-break-after and page-break-inside. These properties help define how the document is supposed to behave when printed. For example, to make a printed document more book-like.

Properties

page-break-before

The page-break-before property adds a page-break before the element to which it is applied.

Note: this property is in progress of being replaced by the more generic break-before property. This new property also handles column and region breaks while being syntactically compatible with page-break-before. Thus before using page-break-before, check if you can use break-before instead.

A common use case for this is to apply it to the selector #comments so a user printing a page with comments could easily choose to print the whole document but stop before the comments cleanly.

page-break-after

The page-break-after property adds a page-break after the element to which it is applied.

Note: this property is in progress of being replaced by the more generic break-after property. This new property also handles column and region breaks while being syntactically compatible with page-break-after. Thus before usingpage-break-after, check if you can use break-after instead.

page-break-inside

The page-break-inside property adds a page-break inside the element to which it is applied.

Syntax

page-break-after  : auto | always | avoid | left | right
page-break-before : auto | always | avoid | left | right
page-break-inside : auto | avoid

The left and right values for page-break-before and page-break-after refer to a spread layout (like a book) where there are distinct left and right pages. They work like this:

  • left forces one or two page breaks after the element so that the next page is formatted as a left page.
  • right forces one or two page breaks after the element so that the next page is formatted as a right page.

Consider always as a mix of both. The specification says:

A conforming user agent may interpret the values ‘left’ and ‘right’ as ‘always’.

Example

@media print {
  h2 { 
    page-break-before: always;
  }
  h3, h4 {
    page-break-after: avoid;
  }
  pre, blockquote {
    page-break-inside: avoid;
  }
}

This code snippet does 3 things:

  • it forces a page-break before all h2 headings (perhaps h2 tags in your document are chapter titles that deserve a fresh page)
  • it prevents page-breaks right after sub-headings because that looks odd
  • it prevents page-breaks inside pre tags and block-level quotes

It could also be applied to @media page when handling PDF file using flying saucer

REference 1 , reference 2

print the page without link

While print, we do not want those long links which makes the page not readable.

add some style to the @media print should be able to achieve this.

Some server (rails/java) is required to use a parent selector. The body element is perfect for selecting what should be the entire page.

   <style>
      @media print {
         body a:link:after, a:visited:after {
            content: "";
         }
      }
   </style>

chrome window.print() window.close()

Chrome used to open a seperate window containing just the element i wanted printed and its relative css. That window then spawned the .print command which opened another window containing print preview.

Now Chrome no longer opens a new window for their print preview. Instead it opens a “dialog with overlay” in the current page to be printed.

So if you use

<script type="text/javascript">
   window.print();
   window.close();
</script>

to print the current page, the window will close and nothing will be printed.

For some reason Chrome don’t start the timeout timer until the print dialog is closed.
what we can do is:

<script type="text/javascript">
   window.print();
   setTimeout('window.close()', 10);
</script>

basically seTimeout is called only when the document is focused which happens only when the overlay print dialog is closed.