0

Given the following snippet of code

var empowerInstance = null;

function onClick_btnSendMessage() {
    var childIFrame = window.document.getElementById("editorFrame");
    if (!empowerInstance) {
        empowerInstance = EditorAPI.getInstance(childIFrame.contentWindow, window.location.origin);
    }
    empowerInstance.document.hasChanged(hasChangedCallback);
}

function hasChangedCallback(returnValue) {
    console.log("empowerInstance.document.hasChanged = " + returnValue.isDirty);
    if (returnValue.success === true && returnValue.isDirty === true) {
        empowerInstance.document.save(saveCallback);
    }
}

function saveCallback(returnValue) {
    console.log("empowerInstance.document.save = " + returnValue.success);
    if (returnValue.success === false) {
        console.log(returnValue.message);
    }
}

window.addEventListener("DOMContentLoaded", function (event) {
    console.log("DOM fully loaded and parsed");
    if (typeof location.origin === "undefined")
        window.location.origin = window.location.protocol + "//" + window.location.host;
    document.getElementById("btnSendMessage").addEventListener("click", onClick_btnSendMessage);
});

Instead of wiring the button up , I'd like to fire the code from the activation of a Bootstrap tab event.

$('a[data-toggle="tab"]').on("shown.bs.tab", function (e) {

    onClick_btnSendMessage(); // Naive way, as this does not wait

    var target = $(e.target).attr("data-EditorUrl"); // activated tab
    var childIFrame = $("#editorFrame");
    childIFrame.attr("src", target);

});

So my question is "How do I wait on this function to complete before changing the source of childIFrame?".

empowerInstance.document.hasChanged(hasChangedCallback);

I conceptually understand the use of Promises and Callbacks, but writing one that functions correctly is a different story.

UPDATED

This version is refactored to eliminate the button handler, thus improving readability.

The usage is also important. When the page loads for the first time it is positioned on a tab. This tab is associated to a document that is hosted in an iFrame. If the user edits this document then tries to change tabs, I'd like to invoke the check for being dirty/save, then once saved, move to the next tab/document. There is also the case that switching between tabs/documents won't cause a save because the document is not dirty.

var empowerInstance = null;

function hasChangedCallback(returnValue) {
    console.log("empowerInstance.document.hasChanged = " + returnValue.isDirty);
    if (returnValue.success === true && returnValue.isDirty === true) {
        empowerInstance.document.save(saveCallback);
    }
}

function saveCallback(returnValue) {
    console.log("empowerInstance.document.save = " + returnValue.success);
    if (returnValue.success === false) {
        console.log(returnValue.message);
    }
}

$(function () {

    if (typeof location.origin === "undefined") {
        window.location.origin = window.location.protocol + "//" + window.location.host;
    }

    $('a[data-toggle="tab"]').on("shown.bs.tab", function (e) {

        var childIFrame = $("#editorFrame");
        if (!empowerInstance) {
            empowerInstance = EditorAPI.getInstance(childIFrame[0].contentWindow, window.location.origin);
        }
        empowerInstance.document.hasChanged(hasChangedCallback);// Need to wait for completion 

        var target = $(e.target).attr("data-EditorUrl"); // activated tab
        childIFrame.attr("src", target);

    });
});

Thank you, Stephen

Stephen Patten
  • 6,333
  • 10
  • 50
  • 84
  • I don't fully understand your code yet, but the answer to your question in general is "use a callback function". – 4castle May 06 '17 at 17:37
  • You can user a callback functions or promises. Promises are not supported by all browsers so you can use webpack or babel to mnahe that. – Mihir Bhende May 06 '17 at 17:45

2 Answers2

1

You can use some higher order functions to do what you want. Instead of passing the hasChangedCallback and saveCallback directly to the empowerInstance.document methods, you'll instead invoke a function that returns those callbacks, but also passes along your own callback that you'll call once all the async operations have finally completed. Here's what it'll look like:

$('a[data-toggle="tab"]').on("shown.bs.tab", function (e) {
    var target = $(e.target).attr("data-EditorUrl"); // activated tab

    onClick_btnSendMessage(function () {
        var childIFrame = $("#editorFrame");
        childIFrame.attr("src", target);
    });
});

function onClick_btnSendMessage(myCallback) {
    var childIFrame = window.document.getElementById("editorFrame");
    if (!empowerInstance) {
        empowerInstance = EditorAPI.getInstance(childIFrame.contentWindow, window.location.origin);
    }
    empowerInstance.document.hasChanged(getHasChangedCallback(myCallback));
}

function getHasChangedCallback(myCallback) {
    return function hasChangedCallback(returnValue,  myCallback) {
        console.log("empowerInstance.document.hasChanged = " + returnValue.isDirty);
        if (returnValue.success === true && returnValue.isDirty === true) {
            empowerInstance.document.save(getSaveCallback(myCallback));
        }
    } 
}

function getSaveCallback(myCallback) {
    return function saveCallback(returnValue) {
        console.log("empowerInstance.document.save = " + returnValue.success);
        if (returnValue.success === false) {
            console.log(returnValue.message);
        }

        myCallback && myCallback(); // make sure myCallback isn't null before invoking
    }
}

It's not exactly attractive, but it should get you what you want.

bmceldowney
  • 2,307
  • 13
  • 19
  • Very close! It seems as if the target value is always set to the first tab and never changes after that – Stephen Patten May 06 '17 at 18:33
  • Odd. Is the value of `e.target` changing? Maybe the value of the `data-EditorUrl` attribute is changed while the async stuff is happening? Try moving the `target` assignment outside the callback. I'll edit my answer to reflect. – bmceldowney May 06 '17 at 18:58
  • Yes, e.target is changed on each click of the tab. I've tried it without edits (dirty false) and with edits (dirty true) same behavior. – Stephen Patten May 06 '17 at 19:25
  • Thanks for the tip about the book, I'm going to start reading that someday :) – Stephen Patten May 07 '17 at 15:41
  • The function being passed to onClick_btnSendMessage is never being invoked – Stephen Patten May 07 '17 at 17:08
  • This seems like a very inconvenient and brittle way to do this, needing to pass your callback through like that – thedude May 07 '17 at 17:35
1

I've refactored your code to show how this can be done using promises.

function onClick_btnSendMessage() {
  var childIFrame = window.document.getElementById("editorFrame");
  if (!empowerInstance) {
    empowerInstance = EditorAPI.getInstance(childIFrame.contentWindow, window.location.origin);
  }
  var doc = empowerInstance.document;
  return hasChanged(doc).then(function() { return save(doc) })
}


function hasChanged(doc) {
  return new Promise(function(resolve, reject) {
    doc.hasChanged(function(returnValue) {
      if (returnValue.success === true && returnValue.isDirty === true) {
        resolve(returnValue)
      } else {
        reject(returnValue)
      }
    })
  })
}

function save(doc) {
  return new Promise(function(resolve, reject) {
    doc.save(function(returnValue) {
      if (returnValue.success === false) {
        console.log(returnValue.message);
        reject(returnValue)
      } else {
        resolve(returnValue)
      }
    })
  })
}

// ------

$('a[data-toggle="tab"]').on("shown.bs.tab", function(e) {

  onClick_btnSendMessage().then(function() {
    var target = $(e.target).attr("data-EditorUrl"); // activated tab
    var childIFrame = $("#editorFrame");
    childIFrame.attr("src", target);

  }).catch(function(error) {
     // handle the error
     console.error('Error!', error)
  })


});
thedude
  • 9,388
  • 1
  • 29
  • 30
  • I don't know if the usage of the rejects are correct, the only time I can think of that they would be used is if there was an error being trapped. – Stephen Patten May 07 '17 at 15:40
  • @StephenPatten that depends on your use case. Personally I think that if a save operation fails, the promise representing this operation should be rejected, but you can resolve the promise if you like. – thedude May 07 '17 at 17:31
  • should have clarified, the reject at the save is correct, but hasChanged is awkward being in the else block, that just means we don't call save. I'm a noob with this stuff so sorry if I'm not understanding usage correctly – Stephen Patten May 07 '17 at 17:39
  • I thought not calling save was the point, no? the condition in the if statement is false, so what should happen now? - I've updated the answer to show how error handling can be added – thedude May 07 '17 at 17:43
  • You are correct, if it's not dirty don't call save, but that doesn't mean its invalid, just not dirty so using reject didn't seem right, that all. – Stephen Patten May 07 '17 at 18:16
  • I used resolve in both branches of the if doc.hasChanged callback function – Stephen Patten May 07 '17 at 19:56