I was asked a question in yestoday's interview:
How do you reverse a <li> list
efficiently?
for example, if there is a list:
<ul id="list">
<li>1</li>
<li>2</li>
...
<li>10000</li>
</ul>
then after the reverse, the list would look like:
<ul id="list">
<li>10000</li>
...
<li>2</li>
<li>1</li>
</ul>
The most efficient code I can come up with is this:
function reverse(){
var list = document.getElementById("list");
var node_list = list.childNodes;
var fragment = document.createDocumentFragment();
for(var i=node_list.length-1; i>=0; i--){
fragment.appendChild(node_list[i]);
}
list.appendChild(fragment);
}
But it's still really slow(takes about 10s in Chrome). Any idea?
UPDATE:
I think there is something wrong with my Chrome... I installed a Chromium and tested the code above in it, it takes less a second.
I guess the point of the interview question basically is the fact that innerHTML is way faster than any DOM operation in every browser. So, don't use DocumentFragment, use a simple string instead:
var ul = document.getElementById("list");
var lstLi = ul.childNodes;
var str = '';
for (var i=lstLi.length - 1; i >= 0; i--) {
str += '<li>' + lstLi[i].innerHTML + '</li>';
}
ul.innerHTML = str;
http://jsfiddle.net/bKeuD/
The way the DOM work, you don't need to re-create the zone. All you need to do is move your element inside the ul that already exist. An optimal solution would be something along this :
var ul = document.getElementById("lstLi");
var lstLi = ul.childNodes;
for (var i=0, c = lstLi.length; i < c; i++) {
ul.insertBefore(lstLi[i], ul.firstChild);
}
Basicly what this does is that it iterate over each element an put them in first. In the end your list will be reversed.
You can do it this way:
var list = document.getElementsByTagName("ul")[0],
items = list.childNodes,
itemsLen = items.length;
while (itemsLen--) {
list.appendChild(items[itemsLen]);
}
Test: http://jsbin.com/ohegu4/2/edit
And if your problem is that you don’t want to block the browser, you can do that:
var list = document.getElementsByTagName("ul")[0],
items = list.childNodes,
itemsLen = items.length;
(function reversePart() {
var iterations = 10; // Number of processed elements every 100ms
window.setTimeout(function(){
while (iterations-- && itemsLen--) {
list.appendChild(items[itemsLen]);
}
if (itemsLen) {
reversePart();
}
}, 100); // Delay between each process : 100ms
})();
Test (with 100000 li, yeah!): http://jsbin.com/ubugi3/2/edit
Pumbaa80's string-based method is the fastest way I found to reverse the list. But just in case you really want to reverse it using DOM methods (e.g. you don't want to lose the attributes of the list items), you can do it this way:
function reverseDom(){
var list = document.getElementById('list');
var items = list.childNodes;
var length = items.length;
var item0 = items[0];
var i;
list.style.display = 'none';
for(i = 0; i < length - 1; i++)
{
list.insertBefore(list.lastChild, item0);
}
list.style.display = 'block';
}
In Google Chrome in Linux, the method above took about a second to reverse a 10,000 item list. But Pumbaa80's method is faster. You can compare the two methods side by side on the same list if you go to this link:
http://jsbin.com/ubugi3/6
Related
So I'm making a simulation of the lottery. I generate 6 numbers between 0 and 40 and show them in the html id 'generated'. My problem is that if I click a second time on 'generate' (in my html page), the previous generated numbers are still a part of the array and still show up. Does anybody know how to clear the array when pushed on the button multiple times?
This is my Javascript code:
'use strict';
function generatenumbers(){
let number = [];
let i;
i=0;
for(i= 0; i <= 5; i++){
number[i] = Math.floor(Math.random()*40);
}
i = 0;
for(i=0; i<= number.length - 1; i++){
let node = document.createElement("LI");
let textnode = document.createTextNode(number[i]);
node.appendChild(textnode);
document.getElementById("generated").appendChild(node);
}
}
You are supposed to remove the previously appended children then add new ones.
var list = document.getElementById("generated");
list.removeChild(list.childNodes[0]); // for removing first child only
//for removing all children
var list = document.getElementById("genrated");
while (list.firstChild) {
list.removeChild(list.firstChild);
}
you don't want to clear the array...you want to clear the document.getElementById("generated") element before you call the loop, that way, there will always be 6 LI elements at the end of the function.
document.getElementById("generated").innerHTML = ""; // clear the element
for(i=0; i<= number.length - 1; i++){
let node = document.createElement("LI");
let textnode = document.createTextNode(number[i]);
node.appendChild(textnode);
document.getElementById("generated").appendChild(node);
}
How do i access child nodes of a child node in javascript?
I need to target the img inside of each li and get its width and height.
I want to do this without using jquery. Just pure javascript.
<ul id="imagesUL">
<li>
<h3>title</h3>
<img src="someURL" />
</li>
</ul>
var lis = document.getElementById("imagesUL").childNodes;
The ideal way to do this is with the querySelectorAll function, if you can guarantee that it will be available (if you are designing a site for mobile browsers, for instance).
var imgs = document.querySelectorAll('#imagesUL li img');
If you can't guarantee that, however, you'll have to do the loop yourself:
var = lis = document.getElementById("imagesUL").childNodes,
imgs = [],
i, j;
for (i = 0; i < lis.length; i++) {
for (j = 0; j < lis[i].childNodes.length; j++) {
if (lis[i].childNodes[j].nodeName.toLowerCase() === 'img') {
imgs.push(lis[i].childNodes[j]);
}
}
}
The above snippet of code is an excellent argument for using a library like jQuery to preserve your sanity.
This works for me:
var children = document.getElementById('imagesUL').children;
var grandChildren = [];
for (var i = 0; i < children.length; i++)
{
var child = children[i];
for (var c = 0; c < child.children.length; c++)
grandChildren.push(child.children[c]);
}
grandChildren contains all childrens' children.
Libraries like jQuery and UnderscoreJS make this easier to write in a more declarative way.
You can provide a second parameter to the $() function to specify a scope in which to search for the images:
var lis = $('#imagesUL').children();
var images = $('img', lis)
images.each(function() {
var width = $(this).attr('width');
// ...
});
I've got a script that adds li elements to a ul and assigns some attributes to it based on a user's selection in a select element. However, I figured it would be necessary to make sure that the li element doesn't already exist in the ul. However, I'm not sure, but I think that either the array isn't assigning variables to it or there's something wrong with my if statement that compares the array. Of course, I may be totally off. I'm stumped.
function anotherCounty(){
var newCounty = document.forms['newForm'].county.value;
var ul = document.getElementById("counties");
var items = ul.getElementsByTagName("li");
var itemArray = new Array();
for (var i = 0; i < items.length; i++){
itemArray.push(items[i]);
}
if (itemArray.indexOf(newCounty)<'0'){//I've also tried ==-1 and <0
var new_item = document.createElement("li");
new_item.id = "addCounty[]";
new_item.innerHTML = newCounty;
ul.insertBefore(new_item, ul.firstChild);
}
}
You are comparing a string with DOM elements. I think this is what you actually need:
itemArray.push(items[i].value);
Instead of using indexOf, you'd be a lot better off to use this approach:
function anotherCounty() {
var newCounty = document.forms['newForm'].county.value;
var ul = document.getElementById("counties");
var items = ul.getElementsByTagName("li");
for (var i = 0; i < items.length; i++){
if(items[i].value == newCounty) return;
}
var new_item = document.createElement("li");
new_item.id = "addCounty[]";
new_item.innerHTML = newCounty;
ul.insertBefore(new_item, ul.firstChild);
}
I know this thread is old, but I just had the same issue and what worked for me was
itemArray.push(items[i].innerHTML);
How do I get access to live DOM collections from jQuery?
Take for example this HTML <div id='a'></div> and this JavaScript code:
var a = $('#a');
var divs = a[0].getElementsByTagName('DIV');
for(var i=0; divs.length < 20; ) {
a.append($('<div>'+(i++)+'</div>'));
}
It will append 20 div children to <div id='a'> because divs is a live collection.
Is there anything in jQuery that I could replace the second line with to get the same result?
var divs = $('#a div'); results in infinite loop.
JSFiddle is here.
In case #a already contains divs:
var $a = $('#a'),
toAdd = Math.max(20 - $a.find('div').length, 0);
for (var i = 0; i < toAdd; i++) {
$a.append('<div>' + i + '</div>');
}
That would be equivalent to the code above.
Live Collections - the true ones, are not something which can be returned by modern jquery.
Moreover, modern method which is intended to replace in nearest future getElementsByTagName, getQuerySelectorAll also return a static collection.
This is the answer to question you've stated.
As for the question you've really wanted to ask, other users already tried to provide you some help.
Select the element each time, this will create a new jQuery object. Which I think it the only way if the element is changing.
var a = $('#a');
for(var i=0; $('#a div').length < 20; ) {
a.append($('<div>'+(i++)+'</div>'));
if(i==50) break;
}
EDIT:
Or this:
for(var i=0, a=$('#a'); a.children('div').length < 20; ) {
a.append($('<div>'+(i++)+'</div>'));
if(i==50) break;
}
Or this, just one selector:
var a = $('#a');
var length = a.children('div').length;
while(length < 20) {
a.append($('<div>'+(length++)+'</div>'));
}
How to get DOM live collections with jQuery?
That’s not possible.
This has the same effect as your example code, though:
var $a = $('#a');
for (var i = 0; i < 20; i++) {
$a.append('<div>' + i + '</div>');
}
http://jsfiddle.net/mathias/Af538/
Update: If the code needs to be repeated periodically, you could use something like this:
var $a = $('#a'),
toAdd = Math.max(20 - $('div', $a).length, 0),
i;
for (i = 0; i < toAdd; i++) {
$a.append('<div>' + i + '</div>');
}
http://jsfiddle.net/mathias/S5C6n/
Is it always 20 div children ?
Isn't it better to use the following
var a = $('#a div');
for(var i=0; i < 20; i++) {
a.append($('<div>'+(i)+'</div>'));
}
The syntax you're looking for is:
var divs = $('div#a');
Since IDs are supposed to be unique, you could just do:
var divs = $('#a');
As you can see I am still a novice in javascript
Why is it so that you can append a Textnode only once? When you add it again somewhere else the first one disappears
I do not need a solution to a problem I was just curious what is causing this behavior.
Example where the textnode is only added to the last element of an array:
function hideAdd(){
var hide = document.createTextNode('Afbeelding verbergen');
var afb = collectionToArray(document.getElementsByTagName('img'));
afb.pop();
var divs = [];
for (i=0; i < afb.length; i++){
divs.push(afb[i].parentNode);
}
console.log(divs);
for ( i = 0; i < divs.length;i++){
divs[i].appendChild(hide);
}
}
This is where you use an unique textnode so it works:
function hideAdd(){
var hide = []
var afb = collectionToArray(document.getElementsByTagName('img'));
afb.pop();
var divs = [];
for (i=0; i < afb.length; i++){
divs.push(afb[i].parentNode);
hide[i] = document.createTextNode('Afbeelding verbergen');
}
console.log(divs);
for ( i = 0; i < divs.length;i++){
divs[i].appendChild(hide[i]);
}
}
Short answer is the DOM is a tree, not a network. Each node can have only one parent. If you could add a node in more than one location, it would have more than one parent.