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
Related
In Google Tag Manager it's quite easy to use the element, element classes, element parent classes to fire tags. Unfortunately in this case I want to scrape an element which is only available in the same .
So, when people click on the link "delete" (class=deleteItem) I want to scrape the url_product_id (in bold).
I've tried so much but can't figure out how to achieve this. I hope somebody can help me out.
<td align="center">
<span class="selquantity menosdis"></span>
<span class="selquantity plus"></span>
<input class="urlProductId" type="hidden" name="url_product_id" value="113293">
<input class="urlQuantity" type="text" name="url_quantity" value="1" readonly="readonly">
<br>
<a style="cursor: pointer" class="deleteItem">delete</a>
</td>
Assuming you are wanting to accomplish this within an event handler, you could use parentNode and querySelector to accomplish your goal:
var myClickHandler = function(event) {
var productId = event.target.parentNode.querySelector('. urlProductId').value;
// do stuff with productId
}
Or with jQuery:
$('.deleteItem').on('click', function() {
var productId = $(this).prev('.urlProductId').val();
});
This is similar to Rob's answer, but also takes into account if code is moved around into additional divs, etc.. Since people update their UI all the time. This should be more sustainable as the product matures. Of course you'll want to watch the .closest() call if you ever change to something like BootStrap data tables.
$(".deleteItem").on("click", function() {
var getClosestProdID = $(event.target).closest('td').find('.urlProductId');
// Just for display of the value
alert("Our closest selected value is: " + getClosestProdID.val());
});
JS Fiddle
Here is a jQuery method (modify and test on your own), and you could do this in a GTM Custom Javascript variable:
function(){
var ce = {{Click Element}}; // get the clicked element
if ($(ce).length > 0){
return $(ce).closest('td').find('.urlProductId').attr('name');
}
return undefined;
}
The premise here being that your elements are all nested under the <td> element, and all you're doing is taking the clicked element and scraping for the element you're interested in.
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.
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.
For some practice this week, I tried creating the front end of a blog page. I added a few "fake" Like buttons (they don't attach to facebook, just raise a counter that's placed next to them.)
Everything works, though I think there's a more direct and re-usable way to write the jQuery/JavaScript code I used to build those counters.
Here's the JavaScript code:
<script>
var whichButton = "";
var counter = 0; // Adds counters next to each
var counter2 = 0; // "Like" button, and raises
$("button").on("click", function(){ // their values separately.
whichButton = this.id;
if (whichButton === "button1") {
counter++;
$("#span1").html(counter);
} else {
counter2++
$("#span2").html(counter2);
}
});
</script>
...and here's the html it affects:
<button id="button1">Like</button>
<span id="span1"></span>
<button id="button2">Like</button>
<span id="span2"></span>
Is there a less hands-on way to write this code? Something that would allow me to
add new buttons alongside new spans, both with complementary ids, and, without updating my JavaScript code, have my site allow each button to function automatically?
I'm trying to write this in the most efficient way I can.
Thanks!
To make this a more reusable component, take advantage of classes instead of unique IDs.
$(".like_button button").on("click", function() {
var $count = $(this).parent().find('.count');
$count.html($count.html() * 1 + 1);
});
In your markup, create as many like_button instances as you want, and use the HTML to set the default value of 0.
<div class="like_button">
<button>Like</button>
<span class="count">0</span>
</div>
Note: $(this).parent().find('.count'); is a very literal traversing example. You could use $(this).next(); instead to find the button's next sibling, which would remove the need for the "count" class. Check out the jQuery Docs on Traversal for many other wonderful methods.
Here's a fiddle showing that in action: http://jsfiddle.net/bw5LU/
Sure, mark all the like buttons with a class or other attribute so we can select like:
$(".like-button").on("click", function(e){
var $counter = $(this).find(".count");
var count = $counter.text() | 0; //corose current count to an int
$counter.text(count + 1);//set new count
});
Now to create new like buttons add the following snippet anywhere in your html document:
<div class="like-button">
<button>Like</button>
<span class="count"></span>
</div>
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)