Continuing javascript execution after total DOM body replacement using DOM mutation observers

Originally published on eve.gd on 12 July 2016.

Read this post on the web (with images, PDF and citation details)

As part of the translation platform we’re building, I needed to implement the following workflow:

The problem was that whenever I ran $(“body”).html(this.original_document); in the first of these cases, the javascript would stop executing. I think this is because the object that triggered the event was destroyed, even though the script was in the head tag.

In other words, whenever I replace the body tag of the HTML, my javascript immediately stops and I can’t call the next function. I also tried using setTimeout to somehow defer this into a space that wouldn’t be picked up by garbage collection or anything else.

I eventually worked out that I can solve this problem by using DOM Mutation Observers. The first thing I do, now, when the page loads is setup a DOM Mutation Observer thus (I’m using a mixture of coffeescript and javascript here):

this.continue_action = ""
`
var target = document.body;
var observer = new MutationObserver(this.handle_state_continuity);
var config = { attributes: true, childList: true, characterData: true };

observer.observe(target, config);
`

This basically says call the handle_state_continuity function whenever the contents of the body changes at any depth, in any way. Inside the event-firing method, I then have:

  startSubstitution: (event = {}) =>
    if this.original_document == ""
      this.original_document = `$("body").html()`

    if this.original_document != `$("body").html()`
      this.continue_action = "substitute"
      console.log("Annotran: Restoring DOM to original state.")
      `
      $("body").html(this.original_document);
      `
      return null
    else
      console.log("Annotran: Starting substitution directly.")
      this.makeSubstitution(event)

The important part to note here is that, in the case where it changes the body html, it first preserves the next action: this.continue_action = “substitute”.

The final part needed to glue this all together is the handle_state_continuity function, which for me looks like this:

  handle_state_continuity: (mutation = null) =>
    if this.continue_action != ""
      this.continue_action = ""
      console.log("Annotran: Starting substitution via state machine.")
      this.makeSubstitution(null)

And, tada! Now, when startSubstitution is called twice, the console output is:

Annotran: Starting substitution directly. [first time] Annotran: Restoring DOM to original state. [second time] Annotran: Starting substitution via state machine.