Assume I have an element in a variable:
var element = document.getElementsByTagName("div")[0]
// here can be any kind of getting element, e. g. React ref, Chrome's devtools $0, etc.
At some point of time my markup is changing (like in SPA), and element from variable has been removed from DOM, but it still available in the element with all properties, such as parentElement, etc.
The question is: how to check, if my DOM element from element is present in DOM?
I tried to check the element.getBoundingClientRect(), and yes, there are some differences: element that removed from DOM has all the zeroes in his bounding rect. But there is one thing: element with display: none also has all the zeroes in its bounding rect, despite of it is still presents in the DOM (physically, lets say). This is not acceptable in my case, because I need to differ hidden element from removed element.
You can use contains for this purpose
function contains() {
const result = document.body.contains(element);
console.log(result);
}
const element = document.getElementById('app');
contains();
element.classList.add('hide');
contains();
element.parentNode.removeChild(element);
contains();
.hide {
display: none;
}
<div id="app">App</div>
Related
Is there a convenient way to check if an HTMLElement is an offsetParent?
I have a situation where I need to determine an element's offsetParent before it is inserted in the DOM. I can access the element's immediate parent, before insertion.
There doesn't seem to be any properties on HTMLElements that indicate whether or not it is an offsetParent.
Is there a good way to do this?
There is to my knowledge unfortunately nothing in the DOM API that does expose this information on the Element itself.
According to specs, an ancestor can be an offsetParent if
The element is a containing block of absolutely-positioned descendants
This means that not only positioned elements will qualify, but any element with a transform, or a filter property, a will-change with a value containing any of the aforementioned ones will also do.
However this behavior was not always specified this way, so it may return false positives in some browsers.
Also, it may be that in the future other CSS properties will affect what makes a containing block, or even in the present since I only got these from the tip of my head...
With that in mind, the surest is to append a test element inside your element and to check its offsetParent.
However, this will create forced reflows, so use it sporadically.
document.querySelectorAll('.container > div')
.forEach(elem => {
elem.textContent = isOffsetParent(elem) ? 'offsetParent' : 'not offsetParent';
});
function isOffsetParent(elem) {
const test = document.createElement('span');
elem.appendChild(test);
const result = test.offsetParent === elem;
elem.removeChild(test);
return result;
}
<div class="container">
<div class="is-offset-parent" style="position:relative"></div>
<div class="can-be-offset-parent" style="transform:translate(0)"></div>
<div class="can-be-offset-parent" style="filter:blur(1px)"></div>
<div class="is-not"></div>
</div>
But if you really wish some unsafe way which may need to be updated, then you could check all the properties I mentioned before using getComputedStyle(elem).
I had a situation in which I wanted to focus either an input tag, if it existed, or it's container if it didn't. So I thought of an intelligent way of doing it:
document.querySelector('.container input, .container').focus();
Funny, though, querySelector always returns the .container element.
I started to investigate and came out that, no matter the order in which the different selectors are put, querySelector always returns the same element.
For example:
var elem1 = document.querySelector('p, div, pre');
var elem2 = document.querySelector('pre, div, p');
elem1 === elem2; // true
elem1.tagName; // "P".
My question is: What are the "reasons" of this behavior and what "rules" (if any) make P elements have priority over DIV and PRE elements.
Note: In the situation mentioned above, I came out with a less-elegant but functional solution:
(document.querySelector('.container input') ||
document.querySelector('.container') ).focus();
document.querySelector returns only the first element matched, starting from the first element in the markup. As written on MDN:
Returns the first element within the document (using depth-first
pre-order traversal of the document's nodes|by first element in
document markup and iterating through sequential nodes by order of
amount of child nodes) that matches the specified group of selectors.
If you want all elements to match the query, use document.querySelectorAll (docs), i.e. document.querySelectorAll('pre, div, p'). This returns an array of the matched elements.
The official document says that,
Returns the first element within the document (using depth-first pre-order traversal of the document's nodes|by first element in document markup and iterating through sequential nodes by order of amount of child nodes) that matches the specified group of selectors.
So that means, in your first case .container is the parent element so that it would be matched first and returned. And in your second case, the paragraph should be the first element in the document while comparing with the other pre and div. So it was returned.
That's precisely the intended behavior of .querySelector() — it finds all the elements in the document that match your query, and then returns the first one.
That's not "the first one you listed", it's "the first one in the document".
This works, essentially, like a CSS selector. The selectors p, div, pre and pre, div, p are identical; they both match three different types of element. So the reason elem1.tagName == 'P' is simply that you have a <p> on the page before any <pre> or <div> tags.
You can try selecting all elements with document.querySelectorAll("p.a, p.b") as shown in the example below and using a loop to focus on all elements that are found.
<html>
<body>
<p class="a">element 1</p>
<p class="b">element 2</p>
<script>
var list=document.querySelectorAll("p.a, p.b");
for (let i = 0; i < list.length; i++) {
list[i].style.backgroundColor = "red";
}
</script>
</body>
</html>
I have a click event where child elements get appended to a parent element and then get removed on another click event. I want to test if those elements got removed from the parent. So is there something like
var container = element(by.css('.container'));
expect(container.length).toEqual(0);
that checks if there are any children elements?
There are special methods for checking if an element is present:
elm.isPresent();
parentElm.isElementPresent(childElm);
browser.isElementPresent(elm);
And here are the differences between them:
In protractor, browser.isElementPresent vs element.isPresent vs element.isElementPresent
Note that you can still find all elements inside a container and check the count:
var container = element(by.css('.container'));
expect(container.all(by.xpath("./*")).count()).toEqual(0);
Another alternative could be to check the inner HTML:
expect(container.getInnerHTML()).toEqual("");
Got this from https://developers.google.com/speed/articles/javascript-dom
From what I understand, appending/removing elements causes reflow. As does changing class. But in the solution, you are appending and removing, thus, causing two times the number of reflows as the problem code. Of course, not all reflows are equal, so are class name change reflows more expensive than appending/removing reflows? What am I missing that makes the solution code more efficient than the problem code?
This pattern lets us create multiple elements and insert them into the
DOM triggering a single reflow. It uses something called a
DocumentFragment. We create a DocumentFragment outside of the DOM (so
it is out-of-the-flow). We then create and add multiple elements to
this. Finally, we move all elements in the DocumentFragment to the DOM
but trigger a single reflow.
The problem
Let's make a function that changes the className attribute for all
anchors within an element. We could do this by simply iterating
through each anchor and updating their href attributes. The problems
is, this can cause a reflow for each anchor.
function updateAllAnchors(element, anchorClass) {
var anchors = element.getElementsByTagName('a');
for (var i = 0, length = anchors.length; i < length; i ++) {
anchors[i].className = anchorClass;
}
}
The solution
To solve this problem, we can remove the element from the DOM, update
all anchors, and then insert the element back where it was. To help
achieve this, we can write a reusable function that not only removes
an element from the DOM, but also returns a function that will insert
the element back into its original position.
/**
* Remove an element and provide a function that inserts it into its original position
* #param element {Element} The element to be temporarily removed
* #return {Function} A function that inserts the element into its original position
**/
function removeToInsertLater(element) {
var parentNode = element.parentNode;
var nextSibling = element.nextSibling;
parentNode.removeChild(element);
return function() {
if (nextSibling) {
parentNode.insertBefore(element, nextSibling);
} else {
parentNode.appendChild(element);
}
};
}
Now we can use this function to update the anchors within an element
that is out-of-the-flow, and only trigger a reflow when we remove the
element and when we insert the element.
function updateAllAnchors(element, anchorClass) {
var insertFunction = removeToInsertLater(element);
var anchors = element.getElementsByTagName('a');
for (var i = 0, length = anchors.length; i < length; i ++) {
anchors[i].className = anchorClass;
}
insertFunction();
}
Suppose you want to change classes of 1 million elements.
Doing it directly would cause 1 million reflows -one for each class-.
But if you remove its parent from the DOM, change all classes, and insert it back, that's only 2 reflows -because changing elements outside the document doesn't cause reflow-.
So basically, removing and reinserting is more efficient if you have lots of elements. No need to do it if you only have a few.
So a document fragment lives 'in memory', not on the page. Manipulating that doesn't trigger any repaints/flows because the fragment is not visually represented anywhere. When you put it on the page, once you're done manipulating it, the browser knows its structure, classes, content, etc, so will only need to reflow/paint once.
In the first example, as you loop through the anchors and change the class name (presumably changing its style as well), it will immediately apply that class, find the new style, and repaint that link. Then do the same for the next one. This is slow.
By yanking it all out into memory and manipulating the DOM there, you only have one repaint/flow when you reinsert the parent wrapper element back into the page.
According to the solution:
To solve this problem, we can remove the element from the DOM, update
all anchors, and then insert the element back where it was.
So, in this case it will trigger 2 reflows (one for remove, and one for insert). So this solution applies when you want to modify more than 2 elements at a time.
When I add text in my text field before and after the existing paragraphs the remove button functions perfectly. However, if I click the remove button before adding elements you have to click TWICE to remove the paragraphs that were not created by a function.
What could be wrong here? I watch the DOM in Firebug as I'm adding and removing, and before the new elements are added, my remove button does not target "firstDiv" on the first click, but does so on the second click.
Here is the problem function:
function removeIt() {
firstDiv.removeChild(firstDiv.lastChild);
}
Here is the fiddle: http://jsfiddle.net/nxpeD/2/
Thanks for the help!
That's because you have text nodes (spaces) at the end, so the last paragraph isn't the last child (it is the last element child).
Then, use
function removeIt() {
firstDiv.removeChild(firstDiv.lastElementChild);
}
Demo: http://jsfiddle.net/nxpeD/6/
Compatibility: To make it work on old browsers, you could also use
function removeIt() {
if (firstDiv.lastElementChild) {
firstDiv.removeChild(firstDiv.lastElementChild);
} else {
var last;
while((last = firstDiv.lastChild).nodeType !== 1) {
firstDiv.removeChild(last);
}
firstDiv.removeChild(last);
}
}
References
lastChild
lastElementChild
Use:
firstDiv.removeChild(firstDiv.lastElementChild);
Since there are formatting new line chars in your html, that will be considered as a child as well of the div. So you need to use lastElementChild to get the element and ignore the formatting and and other text nodes outside.
Demo
The last_child returned is a node. If its parent is an element, then the child is generally an Element node, a Text node, or a Comment node. Returns null if there are no child elements.
lastElementChild