2

How can I call a function after all promises have been resolved? I have an HTML document with three tables. Using html2canvas(), I create a JPG by iterating over them in a forEach() loop:

JavaScript

var elements = document.querySelectorAll( 'table' );
elements = Array.from( elements );

var zip = new JSZip(),
    img = '';

elements.forEach( function( element ) {
    html2canvas( element ).then( canvas => {
        var styleID = element.getAttribute('id');

        img = new Image();
        img.src = canvas.toDataURL( 'image/jpeg' );
        document.body.appendChild( img );

        zip.file( styleID + '.jpg', img.src );
    }).then( generateZip );
});

function generateZip () {
    // Generate the zip file asynchronously
    zip.generateAsync({type:'blob'}).then( function( content ) {
        saveAs( content, 'archive.zip' );
    });
}

The problem is generateZip() gets called three times, once for each loop. How can I call generateZip() just one time, after all promises have been resolved, to create a single zip file?

GTS Joe
  • 3,612
  • 12
  • 52
  • 94

1 Answers1

3

You can use Promise#all to know when all the promises have completed before calling generateZip method. It is also interesting to use Promise#all because if one html2canvas fails the whole Promise will fail and generateZip will not be called.

The Promise.all() method returns a single Promise that resolves when all of the promises passed as an iterable have resolved or when the iterable contains no promises. It rejects with the reason of the first promise that rejects.

The data returned by Promise#all in the then callback is an array of your canvass.

var elements = document.querySelectorAll( 'table' );
elements = Array.from( elements );

var zip = new JSZip(),
    img = '';

function generateZip () {
    // Generate the zip file asynchronously
    zip.generateAsync({type:'blob'}).then( function( content ) {
        saveAs( content, 'archive.zip' );
    });
}

function prepareZip(canvas, element){
        var styleID = element.getAttribute('id');
        img = new Image();
        img.src = canvas.toDataURL( 'image/jpeg' );
        document.body.appendChild( img );
        zip.file( styleID + '.jpg', img.src );
}

Promise.all(elements.map(element=> html2canvas(element)))
.then(data=>{
  data.forEach((canvas, index)=>prepareZip(canvas, elements[index]));
  generateZip();
});

Solution without arrow functions:

var elements = document.querySelectorAll( 'table' );
elements = Array.from( elements );

var zip = new JSZip(),
    img = '';

function generateZip () {
    // Generate the zip file asynchronously
    zip.generateAsync({type:'blob'}).then( function( content ) {
        saveAs( content, 'archive.zip' );
    });
}

function prepareZip(canvas, element){
        var styleID = element.getAttribute('id');
        img = new Image();
        img.src = canvas.toDataURL( 'image/jpeg' );
        document.body.appendChild( img );
        zip.file( styleID + '.jpg', img.src );
}

Promise.all(elements.map(function(element){ return html2canvas(element); }))
.then(function(data){
  data.forEach(function(canvas, index){
    prepareZip(canvas, elements[index])
  });
  generateZip();
});
kemicofa ghost
  • 16,349
  • 8
  • 82
  • 131
  • How can I access the value of element in prepareZip()? I'm getting: ReferenceError: element is not defined. – GTS Joe Apr 02 '19 at 20:01
  • your code works, but arrow => functions are confusing to me (newbie). Could you write your code using regular function calls instead of arrow => functions? It'll be more readable to me. Thank you! – GTS Joe Apr 02 '19 at 20:24
  • 1
    @kemicoa Awesome, you're a rock star. The best! Thank you so much. Yeah, I have to learn more about arrow functions to get confortable with them. BTW, the quote in your signature, Tron: Legacy. Great movie! B-) – GTS Joe Apr 02 '19 at 20:42