javascripthtmldom

Callback which will be called when DOM element been MOUNTED to document with pure JavaScript


parent.appendChild(child) does not mean that child has been mounted to the document because the parent could be not mounted:

const parent = document.createElement("div"); // Created but not mounted
const child = document.createElement("span") // Created but not mounted
parent.appendChild(child); // Child has been appended to parent but both of them not mounted

I need to know when the child has been mounted by the callback.

document.querySelector("body").appendChild(parent);
// Well, it has been mounted now, but in my case I don't know when exactly is will be monted

As far as I has analyzed the topics Detect when Element attached to DOM in javascript it does not cover the actual mounting to the document. The MutationObserver has been recommended but no code samples which I could check does it satisfies to my case.

Use Case: Text Area Auto Resizing

The following text area resizing solution (source) will not work if the textarea is not mounted yet because in this case the scrollHeight will be 0:

const textarea = parent.querySelector("textarea");
textarea.style.height = textarea.scrollHeight + "px";
textarea.style.overflowY = "hidden";

textarea.addEventListener("input", function() {
  this.style.height = "auto";
  this.style.height = this.scrollHeight + "px";
});

To make it work, second and third lines of the code must be called when the text area actually has been mounted to the document.


Solution

  • Errors

    Don't Use References to null

    It's Ok to have a reference to an element that is not in the DOM:

    const textarea = parent.querySelector("textarea");
    

    But at page load textarea = null. So when you add statements like this:

    textarea.style.height = textarea.scrollHeight + "px";
    textarea.style.overflowY = "hidden";
    
    textarea.addEventListener("input", function() {
      this.style.height = "auto";
      this.style.height = this.scrollHeight + "px";
    });
    

    You are really doing this:

    null.style.height = null.scrollHeight + "px";
    null.style.overflowY = "hidden";
    
    null.addEventListener("input", function() {
      null.style.height = "auto";
      null.style.height = null.scrollHeight + "px";
    });
    

    which will give you an error about not being able to reference null. That code gets called early which isn't what you want. What you should do is wrap all of that error prone code into a function and call that function when <textarea> is "mounted" in the DOM.


    Examples 1 & 2

    Since there's no HTML posted and I don't know exactly how or even why a <textarea> is dynamically added within an unknown period of time, I made two examples using the Mutation Observer API despite the feeling that this might actually be a XY problem.

    Example 1

    This example is the simpler version.

    Example 2

    This example is more advanced.

    For a list of links to the resources used for Example 2 go to the bottom of this post.


    Example 1

    // Only for demonstration purposes
    let log = null;
    /**
     * textarea is null because it doesn't exist yet.
     * Wrap everything concerning textarea in a function
     * and you won't get anymore errors.
     */
    const initTextArea = () => {
      const node = document.querySelector("textarea");
      node.style.height = node.scrollHeight + "px";
      node.style.overflowY = "hidden";
      node.addEventListener("input", (e) => {
        e.target.style.height = "auto";
        e.target.style.height = e.target.scrollHeight + "px";
      });
    };
    /**
     * The following is a MutationObserver. It will
     * monitor any addition and removal of children
     * elements (nodes) within <body>. Once <textarea>
     * is mounted to DOM, the function initTextArea() will
     * run all the code for <textarea>.
     */
    /**
     * Reference the element to watch. (eg <body>)
     * This will be the first @param passed to the 
     * MutationObserver. This is referred to as the
     * "targetNode".
     */
    const target = document.body;
    /**
     * Define an object that will be the second @param
     * to be passed to the MutationObserver. 
     *   - "childList" instructs the MO to watch for any 
     *     elements (nodes) that are the direct children 
     *     of "targetNode". If any of these nodes are 
     *     removed or added, the MO will record them as a
     *     mutation. 
     *   - "subtree" extends "childlist" to cover the 
     *     descendants of the children nodes as well. In 
     *     this particular situation it isn't needed, but 
     *     I added it because it might be needed if your 
     *     actual HTML is different.
     */
    const config = {
      childList: true,
      subtree: false
    };
    /**
     * This is the callback function of the MO.
     * @param {array} mutations - An array of mutationRecord 
     *                            objects.
     * @param {object} observer - MO object
     */
    const mounted = (mutations, observer) => {
      // Iterate through the mutationRecord object array...
      for (let mutation of mutations) {
        // if a mR type is a "childList"...
        if (mutation.type === "childList") {
          /**
           * get the .addedNodes property which is a 
           * NodeList (array-like object) of the
           * newly added elements to "targetNode"
           * Iterate through .addedNodes....
           */
          for (let node of mutation.addedNodes) {
            // if a node is a <textarea>...
            if (node.tagName === "TEXTAREA") {
              /**
               * call initTextArea() function to 
               * style <textarea> and register an event
               * listener.
               */
              initTextArea();
              log("TEXTAREA mounted, observer will disconnect now.");
              // and terminate the MO (unless you still need it)
              return observer.disconnect();
            }
          }
        }
      }
    };
    // Instintate MO passing the callback function
    const observer = new MutationObserver(mounted);
    // Start MO passing "targetNode" and config object.
    observer.observe(target, config);
    // The remaining code is for demonstration purposes only
    log = (data) => console.log(data);
    document.querySelector("button")
      .onclick = e => {
        document.body.append(document.createElement("textarea"));
      };
    button {
      cursor: pointer
    }
    
    .as-console-wrapper {
      left: auto !important;
      top: 0;
      width: 60%;
    }
    <button>ADD</button><br>

    Example 2

    /**
     * Reference the element to watch. (eg <form>)
     * This will be the first @param passed to the 
     * MutationObserver. This is referred to as the
     * "targetNode".
     */
    const main = document.forms.main;
    /**
     * Reference all form controls of <form>
     * In this particular layout it would be all:
     *   - <button>
     *   - <textarea>
     */
    const io = main.elements;
    // Reference <button>
    const add = io.add;
    
    /**
     * "input" event handler delegates to any <textarea>
     * residing within <form>.
     * @param {object} e - Event object
     */
    const inputHandler = (e) => {
      const tgt = e.target;
      if (tgt.matches("textarea")) {
        tgt.style.height = "auto";
        tgt.style.height = tgt.scrollHeight + "px";
      }
    };
    
    /**
     * By registering the <form> to listen to the "input"
     * event for all of its children form controls
     * (basically all <textarea>s), all of them
     * are able to react when the "input" event
     * is fired on them. This includes any form
     * controls added dynamically in the future as
     * well. This is called Event Delegation.
     */
    main.addEventListener("input", inputHandler);
    
    /**
     * This function styles a given <textarea>.
     * @param {object} node - <textarea>
     */
    const styleTextArea = (node) => {
      node.style.height = node.scrollHeight + "px";
      node.style.overflowY = "hidden";
    };
    
    /**
     * The following is a MutationObserver. It will
     * monitor any addition and removal of children
     * elements (nodes) within <form>. Once <textarea>
     * is mounted to DOM, the function styleTextArea() will
     * add styles to it.
     */
    
    /**
     * Define an object that will be the second @param
     * to be passed to the MutationObserver. 
     *   - "childList" instructs the MO to watch for any 
     *     elements (nodes) that are the direct children 
     *     of "targetNode". If any of these nodes are 
     *     removed or added, the MO will record them as a
     *     mutation. 
     *   - "subtree" extends "childlist" to cover the 
     *     descendants of the children nodes as well. In 
     *     this particular situation it isn't needed, but 
     *     I added it because it might be needed if your 
     *     actual HTML is different.
     */
    const config = {
      childList: true,
      subtree: false
    };
    
    /**
     * This is the callback function of the MO.
     * @param {array} mutations - An array of mutationRecord 
     *                            objects.
     * @param {object} observer - MO object
     */
    const mounted = (mutations, observer) => {
      // Iterate through the mutationRecord object array...
      for (let mutation of mutations) {
        // if a mR type is a "childList"...
        if (mutation.type === "childList") {
          /**
           * get the .addedNodes property which is a 
           * NodeList (array-like object) of the
           * newly added elements to "targetNode"
           * Iterate through .addedNodes....
           */
          for (let node of mutation.addedNodes) {
            // if the node is a <textarea>...
            if (node.tagName === "TEXTAREA") {
              /**
               * call styleTextArea() function to style 
               * <textarea>
               */
              styleTextArea(node);
            }
          }
        }
      }
    };
    // Instintate MO passing the callback function
    const observer = new MutationObserver(mounted);
    // Start MO passing "targetNode" and config object.
    observer.observe(main, config);
    // The remaining code is for demonstration purposes only
    add.onclick = e => main.append(document.createElement(
      "textarea"));
    button {
      cursor: pointer
    }
    <!--
      There are a lot of advantages using the <form>. 
      Refer to the explanation above.
    -->
    <form id="main">
      <button id="add" type="button">ADD</button><br>
    </form>


    References