Out-of-the-flow DOM Manipulation - javascript

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.

Related

Check whether element from variable has a refference in current DOM

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>

Protractor check if element has children

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("");

JS select child of this with specific class

I am writing a GreaseMonkey script that goes through a page with various elements and each element has text and a button. It uses document.getElementsByClassName to find the parent elements, and it has a for loop to do something to each parent element. In this loop, I need to select a child node with a specific class and find its text value (innerHTML). I can't figure out how to select the child with a specific class of this element.
You'll want to grab the currently iterated element and use querySelector()
For example:
var elements = document.getElementsByClassName('class');
for (var i = 0, len = elements.length; i < len; i++) {
var child = elements[i].querySelector('.class_of_desired_element');
//do stuff with child
}
Note the dot before the class name in querySelector as it works similar to jQuery.
Try querySelectorAll(), which you can use to find elements within the current element.
var parent = document.getElementsByClassName('parentClass'),
parent[0].querySelectorAll('.childClass');
Depending on exactly what you are looking to do, you could also skip selecting the parent, if you don't explicitly need a reference to it.
document.querySelectorAll('.parentClass .childClass');
https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelectorAll
You can use
var yourelement = document.getElementsByClass("");
var i = yourelement.nextSibling;
var e = i.nextSibling;
and keep getting the nextSibling of the element till you get it.
However, like #teddy said in the comments, I would suggest you use jQuery. It has a MUCH easier way to do it:
var value = $('.parentClass .childClass').html();

Javascript Sections

I have a website I want to take that always has the same section with the same id with all the content I want to display. I'm not very amazing at javascript and I'm wondering how I could remove everything but a specific section.
Would the best approach be to just do a loop that goes through all the elements in the DOM and remove everything but the section with the id I want to keep? If I go that approach how do I keep it from removing all the elements inside that section?
Perhaps another way to do this more efficiently would be:
document.body.innerHTML = document.getElementById( 'saveContentId' ).innerHTML
Removing one node includes all its children, so you won't need to loop over all elements in the whole document. I see two possibilities:
get the section, remove all its siblings in the current parent, and then walk up the DOM tree until document.body, while removing all siblings.
get the section and detach it from the document. Then clear document.body and re-attach the section there
The first solution seems cleaner to me, so here some sample code:
function removeEverythingBut(el) {
while (el != document.body) {
var par = el.parentNode;
for (var i=par.childNodes.length-1; i>=0; i--)
if (par.childNodes[i] != el)
par.removeChild(par.childNodes[i]);
el = par;
}
}
// usage:
removeEverythingBut(document.getElementById("my-section"));
you can save only the element you want and delete all other elements. Also I recommend using Jquery

JQuery Event Listeners in a For Loop

I am trying to create some basic button rollover functionality using Jquery and toggleClass. I am building a list of elements by cloning a DIV from my HTML and duplicating it multiple times (its populating a list of data from a database). To do this I am using a for loop. Here is the currently working code.
var displayNode = document.getElementById('phoneDisplayContainer');
for(var i=0; i<length; i++) {
//Clone the original container display.
var clonedDisplay = displayNode.cloneNode(true);
clonedDisplay.setAttribute('id', 'phoneDisplayContainer' + i);
//Remove hidden class from cloned Element. NOT CROSS BROWSER!
clonedDisplay.classList.remove('hidden');
var children = clonedDisplay.getElementsByTagName('div');
//Fill new nodes children containers with data.
children[1].innerHTML = contact.phone[i].type;
children[2].innerHTML = contact.phone[i].number;
children[3].setAttribute('onclick', 'PhoneUtility.edit(' + i + ');');
children[3].setAttribute('id', 'phoneEditDisplay' + i);
children[4].setAttribute('onclick', 'PhoneUtility.remove(' + i + ');');
//Hidden elements
var hidden = new Array(children[3], children[4]);
//Set rollover events.
clonedDisplay.setAttribute('onmouseover', '$("#' + children[3].id + '").toggleClass("hidden");');
clonedDisplay.setAttribute('onmouseout', '$("#' + children[3].id + '").toggleClass("hidden");');
//Append the new node to the display container
phoneContainer.appendChild(clonedDisplay);
}
}
Is there a way to use Jquery event listeners instead of having to set onmouseover and onmouseout directly on the element?
I tried this:
$(clonedDisplay).mouseover(function() {
$(children[3]).toggleClass('hidden');
});
With no luck. It just displays performs the rollover effect on the last element in the list. This is actually my first attempt at using jQuery so any other suggestions to ways I could jQuery inside the code would be helpful too.
EDIT: I'd also like to toggle multiple children from the arraylist mentioned in the for loop. How would I set this up? I can't seem to pass an array to the jquery command without getting errors.
The following code after your for loop should let you assign all the mouseover and mouseout handlers in one go to apply to all the clones:
$('div[id^="phoneDisplayContainer"]').mouseover(function() {
$(this).find("div").eq(3).toggleClass("hidden");
}).mouseout(function() {
$(this).find("div").eq(3).toggleClass("hidden");
});
// or, given that both handlers do the same thing:
$('div[id^="phoneDisplayContainer"]').on("mouseover mouseout", function() {
$(this).find("div").eq(3).toggleClass("hidden");
})
(If you're using jQuery older than 1.7 use .bind() instead of .on().)
The above says to find all the divs with an id beginning with "phoneDisplayContainer" and assign event handlers. Within the handler, find the fourth descendant div and toggle the class.
You don't show your HTML or CSS, but you could do this all in your CSS if you like. Assuming you can assign a common class ("parentDiv") to the divs that you want to trap the hover event on, and a common class ("childDiv") to their child div (the one to be hidden), you can do this:
.parentDiv .childDiv { visibility: hidden; }
.parentDiv:hover .childDiv { visibility: visible; }
(Obviously you can give more meaningful class names to fit your structure.)
Otherwise, again if you can assign those classes to the appropriate divs then after your loop you can do this with jQuery:
$(".parentDiv").on("mouseover mouseout", function() {
$(this).find(".childDiv").toggleClass('hidden');
});
Basically the same as what I said initially, but using classes for selectors. If you feel like I'm pushing a class-based solution on you that's because I am: it tends to make this sort of thing much easier.

Categories