1

I am new to promises. I get how one works.

I have a function that is converting a html document with external references (images, styles, scripts) into a file with those resources embedded. it requires reading the resources from an asynchronous source (a JSZip instance).

I want the function to return only after all promises zip.file().async().then(...) are resolved. E.g. have a domDocument, then perform a series of asynchronous steps on it, then pick up the modifed domDocument afterwards. IT might be possible to modify the area that calls this function to use a then (e.g. processFile(folder,zip,node,dom).then(function(result)... if need be.

// folder is set elsewhere
// zipObject is a JSZip instance
// node is an xml document which contains path references to files mentioned in the html
// domDocument is a DOMParser instance which is the html file

// these are the queryselectors for elements I want to match and replace in the html
[
    {"selector":"script[src]:not([src*='//'])","attribute":"src","type":"string"},
    {"selector":"link[href$='.css']:not([href*='//'])","attribute":"href","type":"string"},
    {"selector":"img:not([src*='//'])","attribute":"src","type":"base64"},
    {"selector":"a[target='_blank']:not([href*='://'])","type":"link"}
].forEach(function(element) {

    // grab all instances of a particular selector
    [].forEach.call(domDocument.querySelectorAll(element.selector), function(instance) {

        if (element.type === "link") { // replace link with plain text
            var tNode = document.createTextNode(instance.textContent);
            instance.parentNode.replaceChild(tNode, instance);

        } else { // need to load from the linked document
            var value = unescape(instance.getAttribute(element.attribute)),
                    path = null;

                // images that are already encoded can be ignored
                if (element.type==="base64" && (-1!==value.indexOf("data:image"))) return;

            if (value.indexOf("../")==0) { // resolve dependancy filename
                var dep = node.parentNode.querySelector("dependency");
                if (null !== dep) {
                    var ref = node.parentNode.parentNode.querySelector("resource[identifier='" + dep.getAttribute("identifierref") + "']");
                    if (null !== ref) {
                        path = ref.querySelector("file[href]").getAttribute("href");
                    }
                }
            } else { // resolve path from filename
                path = folder + value;
            }

            // perform the (asynchronous) modification of the document
            zipObject.file(path).async(element.type).then(function success(content) {
                if (element.attribute === "href") {
                    // <link href="file.css"> ==> <style>(file contents)</style>
                    var style = document.createElement("style");
                    style.appendChild(document.createTextNode(content));
                    instance.parentNode.replaceChild(style,instance);
                } else if (element.type === "base64") {
                    // <img src="file.gif"> ==> <img src="data:image/gif;base64,(image-contents)">
                    var extn = value.substr(value.lastIndexOf(".") + 1).toLowerCase();
                    instance.setAttribute("src", "data:image/" + extn + ";base64," + content);
                } else {
                    // <sript src="file.js"></script> => <script>(file-contents</script>)
                    instance.removeAttribute(element.attribute);
                    instance.appendChild(document.createTextNode(content));
                }
            }, function error (e) {
                console.log("ConvertZipForHtml Error", e);
                // leave it alone I guess
            });
        }
    });
});

// want this to return the MODIFIED version of the string
return domDocument;

I can see that each iteration of the first array needs to be a promise and that the whole function should only return after all of the those promises resolve, but I can't figure out how to

/edit:

I think I see (thanks @Bergi). If I make a promise all which returns a promise for each item which only resolves after JSZip's async function resolves, then it seems to work asynchronously and my outer function can use a .then() after all promises are finished.

function myFunction(args) {
    function replaceElements(elements) {
        return Promise.all(elements.map(selectElements));
    }

    function selectElements(element) {
        return new Promise(function (resolve, reject) {
            var myNodeList = domDocument.querySelectorAll(element.selector);
            for (var i = myNodeList.length; i--;) { // (generally is faster than forwards!)
                var instance = myNodeList[i];
                // -- snip --
                zipObject.file(path).async(element.type).then(function success(content) {
                    // -- snip --
                    resolve(instance);
                })
            }
        });
    }

    return replaceElements([
        {"selector":"script[src]:not([src*='//'])", "attribute":"src", "type":"string"},
        {"selector":"link[href$='.css']:not([href*='//'])", "attribute":"href", "type":"string"},
        {"selector":"img:not([src*='//'])", "attribute":"src", "type":"base64"},
        {"selector":"a[target='_blank']:not([href*='://'])", "type":"link"}
    ]);
}

still grasping at the concepts in async and promises

frumbert
  • 2,323
  • 5
  • 30
  • 61
  • 1
    [Don't ever use `forEach`!](http://stackoverflow.com/a/37576787/1048572) – Bergi May 19 '17 at 04:40
  • @Bergi I can't really see how to incorporate the code on that link to my problem. – frumbert May 19 '17 at 06:33
  • 1
    In short: collect all the promises that you create in the loop body in an array, and then call `Promise.all` upon them. – Bergi May 19 '17 at 06:36
  • In my case the promises are the outputs of loading files from within a zip. after the content is loaded, it replaces the elements which are referenced in the outer loop. How would I reference these variables inside the promise.all? – frumbert May 19 '17 at 06:51
  • As Bergi said, using Promise.all waits for all members of the promise array to be resolved or for one to be rejected, in which case it would return an error that you would need to catch – 404answernotfound May 19 '17 at 07:43
  • Try this http://stackoverflow.com/a/43952261/7636961 it's async forEach which proceeds only on your explicit command. – Egor Stambakio May 19 '17 at 07:44
  • @wostex looks a little dangerous; a loop that won't resolve until you tell it. what happens if the thing you're waiting on throws an error? – frumbert May 19 '17 at 08:20
  • It's up to you how you handle errors. You can do `.catch((e) => { console.log(e); resume(); } )` – Egor Stambakio May 19 '17 at 08:29

0 Answers0