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.
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.
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.
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.
This example is the simpler version.
The MutationObserver
monitors the <body>
for any addition or removal of child elements.
The callback function mounted()
passed into the MO
will invoke function initTextArea()
if the currently added node is a <textarea>
.
initTextArea()
assigns the special styles and registers an "input" event listener to the <textarea>
.
The MO
is then terminated.
A <button>
is added to simulate a <textarea>
being dynamically added at an arbitrary amount of time. You can add as many <textarea>
s as you like but only the first <textarea>
has been modified.
This example is more advanced.
The MutationObserver
monitors the <form>
for any addition or removal of child elements.
The "input" event handler is bound to the <form>
and delegates exclusively to any <textarea>
within the <form>
even if the <textarea>
was added dynamically at a later time. Note: <form>
is the only element that can handle "input" and "change" events as an ancestor event listener.
The callback function mounted()
passed into the MO
will invoke function styleTextArea()
if the currently added node is a <textarea>
.
styleTextArea()
applies the special styles to the currently added node.
The MO
doesn't terminate itself like it did in Example 1.
Like in Example 1, there's a <button>
that adds <textarea>
s indefinitely, but the difference is that every <textarea>
is modified.
For a list of links to the resources used for Example 2 go to the bottom of this post.
// 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>
/**
* 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>