How to prevent parentNode madness? - javascript

I have a list of items which diferent event handlers on it.
...
<li>
<div class="item" data-id="1234">
<h3>Item</h3>
<div class="description">...</div>
<ul class="lists">
<li data-list-id="1">Add to list A<li>
<li data-list-id="2">Add to list B<li>
<li data-list-id="3">Add to list C<li>
</ul>
<button class="delete">delete</button>
</div>
</li>
...
Every li under .list has a click event registered which looks like this:
function addToList(event){
var id = event.target.getAttribute('data-id');
var listId = event.target.parentNode.parentNode.getAttribute('data-id');
// XHR stuff
}
There is no problem with this code but the parentNode.parentNode seems really fragile.
For the button would be only one parentNode and for deeper nested elements parentNode^n
I guess this is a common problem and there are more robust solutions?
With jQuery i would use $(target).parentNode('.item')
Whats the best way to do this without jQuery?

As part of my projects, I always write my own toolbox. Sure, I could use jQuery, but I'll stick to my precision toolkit over a sledgehammer, thanks!
With that in mind, have a look at this:
function findParent(source,filter,root) {
root = root || document.documentElement;
while(source != root) {
if( filter(source)) return source;
source = source.parentNode;
}
}
In your case, you can now call:
var listId = findParent(
event.target,
function(e) {return e.attributes && e.attributes['data-id'];}
).getAttribute("data-id");
// note that you should probably break that down, checking if an element
// is found before getting its attribute value... or let the error
// kill your script. Either works.
Now, here it does look a bit more messy than your simple .parentNode.parentNode, but it's much more robust because it doesn't rely on depths being a certain number, which I believe is what you were aiming for.

Related

How to dynamically get the dynamically created ID in a li

I've been trying to learn js (and a tad of jquery) and I have run into two difficulties when trying to find a way to combine solutions that I find.
Just a little warning that this code is a mix of a few tutorials that I have recently done. I am very new to js.
So I start with a basic html with a few li.
<body>
<ol id="liste">
<li class="active">
</li>
<li>
</li>
<li>
</li>
</ol>
<div id="main_ima">
</div>
<script src="js/main.js"></script>
</body>
I want to create ids for each "li" so in my main.js I add this:
var idVar = $("#liste").find("li").each(function(index){
$(this).attr("id","num-li-"+index);
});
This works great so far. Everytime I add a new li, it gets a new id. I also put it into a var because I will need to use it later.
In th console, If I type idVar, it gives me the whole list of li. If I type idVar[3]. it only gives me the li associated to the [3]. Perfect.
Now I want to get something to appear when one of the li is clicked. For example, I will use the [3]. So I add this to my main.js
var imaContainer = document.getElementById('main_ima')
var listed = document.getElementById('liste');
idVar[3].addEventListener("click", appar);
function appar(){
$(idVar[3]).addClass("active").siblings().removeClass("active");
var imaSel = new XMLHttpRequest();
imaSel.open('GET', 'https://domain.link.to.file.json');
imaSel.onload = function() {
var imaLo = JSON.parse(imaSel.responseText);
renderHTML(imaLo);
};
imaSel.send();
};
function renderHTML(data) {
var htmlS = "";
for (i = 0; i < data.length; i++) {
htmlS += "<p>" + data[i].name + " is a " + data[i].species + ".</p>";
}
imaContainer.insertAdjacentHTML('beforeend', htmlS);
}
Just a side note, I added the add/remove "active" class for CSS.
So when I click the li[3], it works almost as expected. The only thing is when I reclick [3] it produces the result a 2nd time. And again, if I click it a 3rd time, it produces the result a 3rd time, without remove the past results. (which is not totally what I want. Just the 1st result would be better.)
But that is not the main problem I am facing.
I would like the [number] to be dynamically detected, based on the id of the clicked li. I could, in a very ugly way, copy and past this code for every [number] I have. and it would work. But then, what if I want to add more li elements, I would need to add more copy and paste of the above code, giving me possibly huge files for nothing. This is surely not the best way, although it would work.
I'm sure this can be done dynamically.. but that is mostly why I am here. :)
Afterwards, once the dynamic has been added to the clicked li, I would also like the link to be changed dynamically based on the li id. For example, instead of :
imaSel.open('GET', 'https://domain.link.to.file.json');
something like:
imaSel.open('GET', "https://domain.link.to.file" + var +".json");
the var being equal to the [3] number of the clicked li.
In this case, when I try to add a var with a for loop, I always get the "var = max.length" instead of the "var = [id of clicked item]".
So there you have it. Do you need more details?
This is my first JS and/or Jquery try. I've been playing with it for a few days but when I search for answers, when I implement the "solutions" it alwas gives me some new problem. So I am showing you the code that is the closest, IMO, to what I am looking for.
Hopefully, I am not too far away of somehting that works and is not as big as my solutions. :)
Thanks for your time and all help is appreciated.
Here are some suggestions:
You don't need to assign id attributes to your li. You actually never need that id. This will work just as well (note also the > in the selector which makes the find call unnecessary):
var $li = $("#liste > li");
Already now you can address each of the li as $li[3], although that is not the "best practise". Better is $li.get(3). I also like the convention to start the variable with $ when it is the result of a jQuery selection. It gives a clue that you can apply jQuery methods to it.
You don't need to assign a click handler to each li separately. With jQuery on (instead of the native addEventListener) you can assign one event handler for all of them at once.
$li.on('click', apar)
The callback you define for on will have this set to the particular li element that has been clicked, so you can do:
$(this).addClass("active").siblings().removeClass("active");
... without any array lookup.
jQuery offers easy functions for several types of HTTP requests, so you don't need to use XMLHttpRequest. In fact, there is one specifically for getting JSON, so you don't even have to parse the response:
$.getJSON('https://domain.link.to.file.json', renderHTML);
The jQuery index() method can give you the sequence number of that li:
$.getJSON('https://domain.link.to.file' + $(this).index() + '.json', renderHTML);
To replace the inner HTML of a certain element, the jQuery html method can be used:
$('#main_ima').html(htmlS);
Note also how you don't need the DOM native getElementById method, jQuery can look that up for you with the short $('#main_ima').
Example
Here is a working example with a fake JSON serving server:
$("#liste > li").on('click', apar);
function apar() {
$(this).addClass("active").siblings().removeClass("active");
$.getJSON('https://jsonplaceholder.typicode.com/posts/'
+ (1+$(this).index()), renderHTML);
}
function renderHTML(data) {
// This particular JSON request returns an object with body property
var htmlS = data.body;
$('#main_ima').html(htmlS);
}
// On page load, click on the first `li` to automatically load the data for it
$('#liste > li:first').click();
#liste { width: 40px }
.active { background: yellow }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ol id="liste">
<li class="active">load 1</li>
<li>load 2</li>
<li>load 3</li>
</ol>
<div id="main_ima"></div>
The following answers your main concern, how to dynamically get the ID with jquery:
$('.listen-to-me').click(function() { //Add event listener to class
var elementId = $(this).attr('id'); //Get the 'id' attribute of the element clicked
var idNumber = elementId.substring(elementId.indexOf("-") +1); //Get the index of the "-" in the string, and then cut everything prior
alert(idNumber); //The final result
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ul>
<li id="test-1" class="listen-to-me">1</li>
<li id="test-2" class="listen-to-me">2</li>
<li id="test-3" class="listen-to-me">3</li>
<li id="test-4" class="listen-to-me">4</li>
<li id="test-5" class="listen-to-me">5</li>
</ul>

How to optimize the code like "this.parentNode.parentNode.parentNode..."?

var topClick = function() {
child = this.parentNode.parentNode.parentNode.parentNode;
child.parentNode.removeChild(child);
var theFirstChild = document.querySelector(".m-ctt .slt");
data[child.getAttribute("data-id")].rank = 5;
insertBlogs();
};
As you see, there is a part of my code like this:
this.parentNode.parentNode.parentNode.parentNode;
Is there another way to optimize the code (without using jQuery)?
You can use a non recursive helper function, for example:
function nthParent(element, n) {
while(n-- && element)
element = element.parentNode;
return element;
}
You can use recursive helper function, for example:
function getNthParent(elem, n) {
return n === 0 ? elem : getNthParent(elem.parentNode, n - 1);
}
var child = getNthParent(someElement, 4);
An alternative approach
Your goal
According to your comments on the original question, your overall goal is this:
There is a list of blogs.Each blog has a button like "edit" and "delete". When I click such buttons I want to find it's blog element.
I believe the best approach to solve the problem you're facing (as opposed to answering the question you asked - sorry!) is to approach the problem in a different manner.
From your comments, you said you have something like this:
<ul id="blog-list">
<li class="blog-item">
<a href="blog1.com">
Boats galore!
</a>
<span class="blog-description">
This is a blog about some of the best boats from Instagram.
</span>
<span class="blog-button delete-button">
Delete
</span>
<span class="blog-button edit-button">
Edit
</span>
</li>
<li class="blog-item">
<a href="blog2.com">
Goats galore!
</a>
<span class="blog-description">
A blog about the everyday adventures of goats in South Africa.
</span>
<span class="blog-button delete-button">
Delete
</span>
<span class="blog-button edit-button">
Edit
</span>
</li>
<li class="blog-item">
<a class="blog-link" href="blog3.com">
Totes galore!
</a>
<span class="blog-description">
A blog about containers and bags, and the owners who love them.
</span>
<span class="blog-button delete-button">
Delete
</span>
<span class="blog-button edit-button">
Edit
</span>
</li>
</ul>
And your goal is to add click event handlers to the button elements for each blog-link item.
So let's just translate that goal from plain English into pseudo-code:
for each `blog-link` `b`
delete_button.onclick = delete_handler(`b`);
edit_button.onclick = edit_handler(`b`);
Example script
Working example at: http://jsfiddle.net/vbt6bjwy/10/
<script>
function deleteClickHandler(event, parent) {
event.stopPropagation();
parent.remove();
}
function editClickHandler(event, parent) {
event.stopPropagation();
var description = parent.getElementsByClassName("blog-description")[0];
description.innerHTML = prompt("Enter a new description:");
}
function addClickHandlers() {
var bloglistElement = document.getElementById("blog-list");
var blogitems = bloglistElement.getElementsByClassName("blog-item");
blogitems.forEach(function(blogitem){
var deleteButtons = blogitem.getElementsByClassName("delete-button");
deleteButtons.forEach(function(deleteButton){
deleteButton.onclick = function(event) {
return deleteClickHandler(event, blogitem);
}
});
var editButtons = blogitem.getElementsByClassName("edit-button");
editButtons.forEach(function(editButton){
editButton.onclick = function(event) {
return editClickHandler(event, blogitem);
}
});
});
}
HTMLCollection.prototype.forEach = Array.prototype.forEach;
addClickHandlers();
</script>
Explanation
The way you've chosen to implement a solution is valid, but I thought I'd give you a different way to look at the same problem.
In my opinion, the inner tags of the blog entity should not have to have knowledge of the structure or properties of the surrounding HTML for your edit and delete buttons to work.
Your original solution has to work backwards from each button up the chain of parents until it finds what it assumes is the correct parent element, based on a brittle method like hard-coding moving up N times in the chain of parent elements. Wouldn't it be nicer if we could use normal JavaScript element selection to be absolutely sure of what we're selecting? That way, no matter how the HTML structure might change, our JavaScript isn't going to break as long as the classes and IDs remain consistent.
This solution iterates over every blog-item in the #blog-list element:
blogitems.forEach(function(blogitem){ ... });
Within the forEach loop, we grab arrays containing .delete-button and .edit-button elements. On each of those elements, we add the appropriate event handler to the onclick property:
deleteButtons.forEach(function(deleteButton){
deleteButton.onclick = function(event) {
return deleteClickHandler(event, blogitem);
}
});
For each deleteButton element in the array, we assign an anonymous function to the event handler onclick. Creating this anonymous function allows us to create a closure.
This means each deleteButton.onclick function will individually "remember" which blogitem it belongs to.
Check out this SO question/answer about closures for more info: How do JavaScript closures work?
And we throw in the HTMLCollection.prototype.forEach... line to provide forEach functionality to all HTMLCollection objects. Functions like getElementsByClassName return an HTMLCollection object. It acts exactly like an array for the most part, but it doesn't let us use forEach by default. A note about compatibility: you can certainly use a standard for loop, since forEach isn't available in all browsers (mostly old IE browsers are to blame). I prefer the forEach idiom.
End result
The end result is a little bit longer code, since the scope of the problem is a little wider than the question you actually asked. The advantage is that this code is much more flexible and resistant to being broken by changes to the HTML structure.
var topClick = function(event){
console.log(event);
child = this.parentNode.parentNode.parentNode.parentNode;
child.parentNode.removeChild(child);
var theFirstChild = document.querySelector(".m-ctt .slt");
data[child.getAttribute("data-id")].rank = 5;
insertBlogs();
};
Surprise! I print the event through console.log.
And find a array like this inside the event:
And I find the element that I want :
function(event){
console.log(event.path[4]);
child = event.path[4];
}
and it work! How magic! Maybe event agent is another way!
Thank you all the same for giving answers!
Next time before asking questions I'll think over first! :D

Replacing an Element with it's Contents IE8

I am trying to replace some "nested" classes from some HTML with javascript/jquery. I need to get rid of the spans with class="foo" and leave behind only "stuff".
<ul>
<li>
<span class="foo">stuff
<ul>
<li><span class="foo">stuff</span>
</li>
</ul>
</span>
</li>
</ul>
This works in everything but IE8 (which I must unfortunately support):
$(".foo").each(function () {
$(this).replaceWith(this.innerHTML);
});
Before someone points out that there are other similar questions, please note that I've tried several methods outlined in those questions and I think my use case differs because of the "nesting". I've tried the solutions in topics on this site and others. Some of these completely crash IE8, others just don't work.
I am aware of trying to use .empty() before replaceWith, but this doesn't help... my problem isn't performance (yet) it is getting it to even work at all.
I've also tried this:
$(".foo").each(function () {
var HTMLshunt = this.innerHTML;
$(this).after(HTMLshunt).remove();
});
Why the "HTMLshunt" var? I was working on the premise that maybe it wasn't working in IE8 because the "after" wasn't really going "after" .foo, but inside it... because in IE8 something happens with this one: it eliminates every .foo but leaves no contents of foo behind.
This "nesting" isn't helping. I think something else cobbled together along the way would have worked if it weren't for the nesting, but it doesn't matter because there IS this nesting. If anyone can help, please respond.
You could try this, should be IE8 friendly (though I do believe jQuery 1.11.0 should also support IE8). If reflow is any worry then you could cloneNode first at the expense of some memory instead.
HTML
<ul>
<li>
<span class="foo">stuff
<ul>
<li>
<span class="foo">stuff</span>
</li>
</ul>
</span>
</li>
</ul>
Javascript
var fooClasses = document.querySelectorAll('.foo'),
fooIndex,
foo,
fragment;
for (fooIndex = fooClasses.length - 1; fooIndex >= 0; fooIndex -= 1) {
fragment = document.createDocumentFragment();
foo = fooClasses[fooIndex];
while (foo.firstChild) {
fragment.appendChild(foo.firstChild);
}
foo.parentNode.replaceChild(fragment, foo);
}
On jsFiddle
With a cloned node
var fooClasses = document.querySelectorAll('.foo'),
fooIndex,
foo,
fragment,
clone;
for (fooIndex = fooClasses.length - 1; fooIndex >= 0; fooIndex -= 1) {
fragment = document.createDocumentFragment();
foo = fooClasses[fooIndex];
clone = foo.cloneNode(true);
while (clone.firstChild) {
fragment.appendChild(clone.firstChild);
}
foo.parentNode.replaceChild(fragment, foo);
}
On jsFiddle
It seems as though you are using jQuery. In that case, you should just use the unwrap() method:
Description: Remove the parents of the set of matched elements from the DOM, leaving the matched elements in their place.
As an example:
$(".foo").each(function () {
$(this).children().unwrap();
});
jsFiddle demonstration
This fiddle uses jQuery 1.9.1, so it should function in IE8.
EDIT
Okay, so the problem is that jQuery.unwrap doesn't work when a node only contains text content.
In order to work with text content, you'll have to use a slightly different approach:
$(".foo").each(function() {
$(this).replaceWith(this.childNodes);
});
See this fiddle
Full disclosure: I used this answer for the technique.

Fastest way to traverse DOM (Breadth First)

Sorry if this will sound like a strange need.
Users can create a HTML page with any number of elements in any order, Div's will be assigned a class. I have no control over which order or where they are placed.
I need to traverse the DOM and call an init function on each div with the class names specific to my framework. So I do not know the order they will be placed.
Here are my challenges:
- I am using a recursive .children() Jquery call then iterating through each child of the current element... I think I need to find a faster way of doing this, it is a performance hit.
- When iterating, $.each() is faster than a standard for loop. I have no idea why. Does $.each() perform faster for smaller sets? (i.e. size < 20 )
My other alternative is to do a selector on each class but I need to do several iterations as this can't guarantee the ordering so the iterations cancel out any performance advantage.
So My needs:
- A better performing way to traverse the DOM breadth first. Plain ol' Javascript is fine if I can find better performance than JQuery functions.
Sorry, I know this sounds a bit of a strange need. Any advice is greatly welcome.
First of all, you shouldn't be having a huge performance hit. The issue could somewhere else in your code, but you haven't shown us what you currently have, so I have no way of saying. That said, here's an idea that should be pretty speedy:
var classes = ["class1", "class2", "class3"];
i = classes.length, divs, j, curDiv;
while(i--) {
divs = document.getElementsByClassName(classes[i]);
j = divs.length;
while(j--) {
var curDiv = divs[j];
// call some init function on the current div
}
}
If this isn't fast, you must have an issue somewhere else (also, don't forget to make sure this is called after the document has loaded).
As for whether $.each() is faster than a standard for loop, this is pretty much a rule of thumb: native code is faster than library code (but library code is usually more convenient). That holds true in this case; your average for loop will be much faster than $.each().
Here's something a little bit more concise, with some current syntax.
function bfs(root) {
const queue = [];
while (root) {
console.log(root);
[...root.children].forEach(child => queue.push(child));
root = queue.shift();
}
}
bfs(document.body);
<html>
<head>
<title>Breadth First</title>
</head>
<body>
<div class="a">
<div class="aa">
<span class="aaa"></span>
<span class="aab"></span>
</div>
</div>
<div class="ab">
<span class="aba"></span>
<span class="abb"></span>
</div>
<script src="main.js"></script>
</body>
</html>
For breadth first tree traversal...
const html = `<div class="a">
<div class="aa">
<span class="aaa">
</span>
<span class="aab">
</span>
</div>
<div class="ab">
<span class="aba">
</span>
<span class="abb">
</span>
</div
</div>`;
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
function BFT_dom(root) {
const queue = [];
let currentEl = root;
while(currentEl) {
console.log(currentEl) // Do something to element here
for(let i = 0; i < currentEl.children.length; i++) {
queue.push(currentEl.children[i]);
}
currentEl = queue.shift();
}
}
BFT_dom(doc)

jQuery - different behaviour of before() and after()

I have a sortable list (not jQuery UI sortable, just by a simple click on buttons).
Sorting to the right via after() works just fine, but sorting to the left with before() does not work.
First of all the code:
Html
PhotoList:
<ul class="PhotoList">
<li>
<div class="ThumbWrapper">
<img src="images/img_testphoto.jpg" />
</div>
</li>
[...more Elements...]
</ul>
Selectable List:
(positioned absolutely over the photos)
<ul class="SelectablePolaroids">
<li>
<div class="Selectable">
<div class="Tools">
<div class="ArrowLeft Tool">
</div>
<div class="Delete Tool">
</div>
<div class="ArrowRight Tool">
</div>
</div>
</div>
</li>
[...more Elements...]
</ul>
JS
Bind functions
jQuery('.SelectablePolaroids .Selectable .ArrowLeft').live('click', function(){sortPolaroid(jQuery(this),0)});
jQuery('.SelectablePolaroids .Selectable .ArrowRight').live('click', function(){sortPolaroid(jQuery(this),1)});
Function
function sortPolaroid(elm, right)
{
var selectitem = elm.parents('li');
var index = jQuery('.SelectablePolaroids').children().index(selectitem);
var photo = jQuery('.PhotoList').children().eq(index);
selectitem.remove();
photo.remove();
if(right)
{
jQuery('.SelectablePolaroids').children().eq(index).after(selectitem);
jQuery('.PhotoList').children().eq(index).after(photo);
}
else
{
console.log(index);
jQuery('.SelectablePolaroids').children().eq(index).before(selectitem);
jQuery('.PhotoList').children().eq(index).before(photo);
}
}
If right is true or .ArrowRight was clicked, it works just as expected. The item is removed and inserted one position further to the right.
As you can see I logged index to check if the else statement is executed at all and whether the index is correct. And yeah the else statement is executed and the index is correct as well. Is before() working in another way than after() or am I just blind to another mistake?
EDIT:
I have also logged the index after before().
//...
else {
console.log(index);
jQuery('.SelectablePolaroids').children().eq(index).before(selectitem);
console.log(jQuery('.SelectablePolaroids').children().index(selectitem));
jQuery('.PhotoList').children().eq(index).before(photo);
}
And it stays the same, nothing changes at all... but it was remove, which means it is inserted at exactly the same position, therfore it works if I am doing the following - my Solution:
//...
else {
jQuery('.SelectablePolaroids').children().eq(index-1).before(selectitem);
jQuery('.PhotoList').children().eq(index-1).before(photo);
}
But I don't really know why... the new element should be at the index, and before should insert it before index...
If someone knows please answer I will gladly accept an answer :-)
...
Okay, figured it out by myself and answered, sorry about the ruckus, but maybe some other people don't know as well and are thinking in the same way as me :-)
photo.remove(); actually removes the dom element from the tree so I think there is mismatching of indexes. Try using before/after without removing, that just moves the element.
By removing the element the next and not the previous element gets the index, so it will be inserted into the same position.
It works with after() because the element getting the new index is in the "right direction", therefore it works with the following:
//...
else
{
jQuery('.SelectablePolaroids').children().eq(index-1).before(selectitem);
jQuery('.PhotoList').children().eq(index-1).before(photo);
}
Just a logical mistake... solved after appropiate thinking :-)

Categories