Search code examples
javascriptangularjspdfmake

running a "background task" in javascript


Is it possible, in Javascript, to run a function in background ?

I am generating a pdf with pdfmake tool in an angularJS app, but the pdf generation is quite long (3-4 seconds) and during this time, the ui freeze completely.

I would like to run a background task and force the pdf download without freezing the user ui, is it possible ?

Here how I am running pdfmake (pdfmake and _ are custom factories):

'use strict';

angular.module('App')

    .service('CatalogPdfService', ['pdfmake', '_', '$q', '$filter',
        function (pdfmake, _, $q, $filter) {

            var $translate = $filter('translate');
            var listDate = new Date();

            return {
                download: download
            };

            function download(data) {

                listDate = _.first(data).publishedOn;
                console.log('start download');
                var deferred = $q.defer();
                var filename = $translate('APP.EXPORT.pdf.catalog.title', {date: $filter('amDateFormat')(listDate, 'DDMMYYYY')}) + '.pdf';
                create(data).download(filename, function () {
                    console.log('end download');
                    deferred.resolve();
                });
                return deferred.promise;
            }

            function create(data) {

                // group data by category
                var dataByCategory = _.groupBy(data, function (d) {
                    return d.category;
                });

                // group categories data by subcategory
                _.forEach(dataByCategory, function (d, i) {
                    dataByCategory[i] = _.groupBy(d, function (d) {
                        return d.subcategory;
                    });
                });

                var content = {
                    table: {
                        headerRows: 1,
                        widths: ['*', 20, 10, 20, 20, 20, 20, 40, 20, 30],
                        body: [
                            [
                                {text: $translate('APP.EXPORT.pdf.catalog.header.article')      , style: 'headings', alignment: 'left'},
                                {text: $translate('APP.EXPORT.pdf.catalog.header.mine')         , style: 'headings'},
                                {text: $translate('APP.EXPORT.pdf.catalog.header.rank')         , style: 'headings'},
                                {text: $translate('APP.EXPORT.pdf.catalog.header.origin')       , style: 'headings'},
                                {text: $translate('APP.EXPORT.pdf.catalog.header.transporter')  , style: 'headings'},
                                {text: $translate('APP.EXPORT.pdf.catalog.header.culture')      , style: 'headings'},
                                {text: $translate('APP.EXPORT.pdf.catalog.header.label')        , style: 'headings'},
                                {text: $translate('APP.EXPORT.pdf.catalog.header.unit')         , style: 'headings'},
                                {text: $translate('APP.EXPORT.pdf.catalog.header.packing')      , style: 'headings'},
                                {text: $translate('APP.EXPORT.pdf.catalog.header.price')        , style: 'headings'}
                            ]
                        ]
                    },
                    layout: {
                        hLineWidth: function (i) {
                            return (i == 0) ? 0 : 1;
                        },
                        vLineWidth: function (i) {
                            return 0;
                        },
                        hLineColor: function (i, node) {
                            return '#ccc';
                        }
                    }
                };

                _.forEach(dataByCategory, function (data, category) {
                    content.table.body = content.table.body.concat(renderCategory(category, data));
                });

                var dd = {};
                dd.content = renderHeader().concat(content);
                dd.header = function (currentPage, pageCount) {
                    return {
                        text: $translate('APP.EXPORT.pdf.catalog.pagecount', {start: currentPage.toString(), end: pageCount.toString()}),
                        alignment: 'right',
                        color: '#666',
                        margin: [0, 20, 40, 0]
                    };
                };
                dd.styles = {
                    title: {
                        fontSize: 15,
                        bold: true
                    },
                    headings: {
                        italics: true,
                        alignment: 'center'
                    },
                    flag: {
                        alignment: 'center',
                        italics: true,
                        color: '#666'
                    },
                    category: {
                        bold: true,
                        fontSize: 12,
                        margin: [0, 10, 0, 0] // Left, Top, Right, Bottom
                    },
                    subcategory: {
                        bold: true,
                        fontSize: 10,
                        margin: [0, 7, 0, 5] // Left, Top, Right, Bottom
                    }
                };

                dd.defaultStyle = {
                    fontSize: 8
                };

                return pdfmake.createPdf(dd);
            }

            function renderHeader() {
                return [
                    {image: logo(), height:40, width: 86},
                    {
                        margin: [0, 10, 0, 20],
                        table: {
                            widths: [100, 100, 100, '*'],
                            body: [
                                [
                                    {text: $translate('APP.COMMON.address', {char: '\n'})},
                                    {text: '\n' + $translate('APP.COMMON.phone')},
                                    {text: '\n' + $translate('APP.COMMON.fax')},
                                    {text: '\n' + $translate('APP.EXPORT.pdf.catalog.listno', {date: $filter('amDateFormat')(listDate, 'DD/MM/YYYY')}) , alignment: 'right'}
                                ]
                            ]
                        },
                        layout: {
                            hLineWidth: function (i) {
                                return (i == 0) ? 0 : 1;
                            },
                            vLineWidth: function (i) {
                                return 0;
                            }
                        }
                    }];
            }

            function renderCategory(name, data) {

                var category = [
                    [
                        {text: name, style: 'category', colspan: 10},
                        '', '', '', '', '', '', '', '', ''
                    ]
                ];

                _.forEach(data, function (data, name) {
                    category = category.concat(renderSubcategory(name, data));
                });

                return category;
            }

            function renderSubcategory(name, data) {

                var subcategory = [
                    [
                        {text: name, style: 'subcategory', colspan: 10},
                        '', '', '', '', '', '', '', '', ''
                    ]
                ];

                _.forEach(data, function (product) {
                    subcategory.push(renderProduct(product));
                });

                return subcategory;
            }

            function renderProduct(product) {
                return [
                    product.name,
                    {
                        text: (product.isInPrivateList ? 'Oui' : ''),
                        style: 'flag'
                    },
                    {
                        text: (null === product.rank ? '' : String(product.rank)),
                        style: 'flag'
                    },
                    {
                        text: (product.origin || ''),
                        style: 'flag'
                    },
                    {
                        text: (product.transporter || ''),
                        style: 'flag'
                    },
                    {
                        text: (product.label || ''),
                        style: 'flag'
                    },
                    {
                        text: (product.culture || ''),
                        style: 'flag'
                    },
                    {
                        text: product.unit,
                        margin: [0, 0, 5, 0],
                        italics: true,
                        alignment: 'right'
                    },
                    {
                        text: (product.quantity || '1'),
                        italics: true,
                        fillColor: '#eee',
                        alignment: 'center'
                    },
                    {
                        text: product.unitPrice,
                        margin: [0, 0, 5, 0],
                        italics: true,
                        fillColor: '#eee',
                        alignment: 'right'
                    }
                ];
            }

            function logo() {
                return 'data:image/jpeg;base64,blabla bigbase64 string'
            }
        }]);

Solution

  • You could use a Web Worker to generate the PDF. But you should be aware of some restrictions when using them. Here is a good reference.

    I created a factory in Angular for doing work on a worker thread. Something like this:

    /*
    Here's an example on how to get this sack of moldering spuds to do something:
    
     var myWorker = new MyWorker({ fn: function() {
        this.onmessage = function(args) {
            setTimeout(function() {
                this.postMessage('Got args: ' + args.data);
            }, 20000);
        };
     } });
    
     myWorker.do('Test').then(function(message) {
        alert(message);
     });
     */
    
    'use strict';
    
    angular.module('myApp')
        .factory('MyWorker', function($q) {
            var _worker;
    
            var MyWorker = function(settings) {
                _init(settings);
            };
    
            MyWorker.prototype.do = function(args) {
                var deferred = $q.defer();
    
                _worker.onmessage = function(message) {
                    deferred.resolve(message.data);
                };
    
                //Fire up the blades.
                if (args)
                    _worker.postMessage(args);
                else
                    _worker.postMessage();
    
                return deferred.promise;
            };
    
            MyWorker.prototype.destroy = function() {
                _worker.terminate();
            };
    
            function _init(settings) {
                if (settings.script)
                    _worker = new Worker(settings.script);
                //Need to make this IE (10+) friendly.
                else if (settings.fn) {
                    var blobUrl = window.URL.createObjectURL(new Blob(
                        ['(', settings.fn.toString(), ')()'],
                        { type: 'application/javascript' }
                    ));
    
                    _worker = new Worker(blobUrl);
                }
            };
    
            return MyWorker;
        });
    

    This will give you a rough idea about how it can be implemented in AngularJS, but seriously take it with a grain of salt.