skip to Main Content

I’m trying to have a MutationObserver watch for changes to a page title and immediately reset the title back to the original text. This partially works, but the first time the page title is changed, my callback keeps firing and the page grinds to a halt. I’ve tried disconnecting the observer in my callback, and that stops it from looping, but I actually need it to keep observing, so it isn’t a solution.

What am I doing wrong?

function resetTitle(title) {
    document.title = title[0].oldValue;
    console.info("Reset the page title.");
}

let observer = new MutationObserver(resetTitle);

observer.observe(document.querySelector('title'), {
  childList: true,
  subtree: true,
  characterDataOldValue: true
});

4

Answers


  1. First, when the document.title is set, the string replace all algorithm is used, which will replace all the previous content of the <title> element with a new TextNode.
    Doing so, there is no CharacterData mutation occurring, only a childList one, which will not fill the previousValue field.

    document.title = "original title";
    // StackSnippets don't set a title, so we need to wait after we set the initial
    // title before starting our observer
    setTimeout(() => {
      function resetTitle(title) {
          console.log("oldValue:", title[0].oldValue);
          console.log("type:", title[0].type);
      }
    
      let observer = new MutationObserver(resetTitle);
    
      observer.observe(document.querySelector('title'), {
        childList: true,
        subtree: true,
        characterDataOldValue: true
      });
      document.title = Math.random();
    }, 100)

    You could have used that if the title was set by modifying the existing TextNode directly:

    document.title = "original title";
    // StackSnippets don't set a title, so we need to wait after we set the initial
    // title before starting our observer
    setTimeout(() => {
      function resetTitle([record]) {
        console.log("oldValue:", record.oldValue);
      }
    
      let observer = new MutationObserver(resetTitle);
      observer.observe(document.querySelector('title'), {
        subtree: true,
        characterDataOldValue: true,
      });
    
      document.querySelector('title').firstChild.data = Math.random();
    }, 100);

    So what you need is actually to handle both the possible CharacterData mutation, and the childList one, by looking at the removedNodes[0].data to get the old value.
    But if you’re going to modify the title again in your handler, you will trigger once more the observer’s callback, with this time, the new title being set as the old one.
    So instead, the best is to store the original value that you want to keep from outside of the observer’s callback, and to check in the callback if the title needs an update or not:

    // Store the original title when you start the observer
    const originalTitle = document.title;
    function resetTitle([record]) {
      // Only if needed, even setting to the same value would trigger a childList change.
      if (document.title !== originalTitle) {
        document.title = originalTitle;
        console.log("resetting title");
      }
    }
    let observer = new MutationObserver(resetTitle);
    observer.observe(document.querySelector('title'), {
      childList: true,
      subtree: true,
      characterDataOldValue: true,
    });
    
    document.title = "original title";
    // Below setTimeout is for this demo only,
    // StackSnippets don't set a <title>, so we need to wait after we set the initial
    // title before starting our observer
    setTimeout(() => {
      // Store the original title when you start the observer
      const originalTitle = document.title;
      function resetTitle([record]) {
        // Only if needed, even setting to the same value would trigger a childList change.
        if (document.title !== originalTitle) {
          document.title = originalTitle;
          console.log("resetting title");
        }
      }
      let observer = new MutationObserver(resetTitle);
      observer.observe(document.querySelector('title'), {
        childList: true,
        subtree: true,
        characterDataOldValue: true,
      });
      document.querySelector('button.title').onclick = e =>
        document.title = Math.random();
      document.querySelector('button.data').onclick = e =>
        document.querySelector('title').firstChild.data = Math.random();
    }, 100);
    <button class=title>change through `document.title`</button>
    <button class=data>change through `TextNode.data`</button>
    Login or Signup to reply.
  2. You need to detach the observer, undo the title value programmatically & controlled, and only then you can reattach the same function with observer options.

    const elTitle = document.querySelector('title');
    
    const resetTitle = (mutList) => {
      mutList.forEach(mut => {
        if (mut.type === "childList") {
          observer.disconnect(); // Detach first (to prevent infinite loop)
          elTitle.textContent = mut.removedNodes[0].nodeValue; // Reset to old
          observer.observe(elTitle, observerOptions); // Reattach
        }
      });
    };
    
    const observer = new MutationObserver(resetTitle);
    const observerOptions = { childList: true };
    
    observer.observe(elTitle, observerOptions); // Init attach
    
    // THIS DEMO ONLY
    setInterval(() => { elTitle.textContent = "test"; }, 2000);
    Login or Signup to reply.
  3. This is how you want to do that (updated based on Kaiido’s correction below).

    var originalTitle = document.title;
        
        function resetTitle(title) {
            if( originalTitle != document.title ){    
            // disable the observer while you make the change
                    observer.disconnect();
                    document.title = originalTitle;
                    console.info("Reset the page title.");
            
                    // now enable it again
                    observer.observe(document.querySelector('title'), {
                    childList: true,
                    subtree: true,
                    characterDataOldValue: true
                    });
                }
            
        }
        
        let observer = new MutationObserver(resetTitle);
        
        observer.observe(document.querySelector('title'), {
          childList: true,
          subtree: true,
          characterDataOldValue: true
        });
    
    Login or Signup to reply.
  4. You have created a feedback loop. A simple way to avoid the feedback loop, without having to micromanage the subscription, is to simply compare the change to the current value.

    You might also want to cache your original title

    
    const originalTitle = document.title;
    
    function resetTitle(title) {
        // Avoid loop by ignoring self change.
        if (originalTitle === title[0].newValue) {
          return    
        }
        document.title = originalTitle;
        console.info("Reset the page title.");
    }
    

    As another approach, just ignore the event contents and check the title against the cached. This will also avoid the feedback.

    
    const originalTitle = document.title;
    
    function resetTitle(_title) {
        // Avoid loop by ignoring self change.
        if (document.title !== originalTitle) {
          document.title = originalTitle;
          console.info("Reset the page title.");
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search