How do I traverse ancestors with jQuery?
The current code is getting stuck in a recursive loop:
HTML:
<html>
<body>
<div>
<ul>
<li></li>
</ul>
</div>
</body>
</html>
JS:
function traverse($node){
while ( $node.get(0) !== $("html").get(0) ) {
console.log($node);
traverse($node.parent());
}
}
//traverse($("ul li"));
To observe the problem, un-comment the last line, but be warned that it may freeze your browser.
The same on JSFiddle: http://jsfiddle.net/WpvJN/1/
You can get the collection with .parents() and iterate with .each():
$('ul li').parents().each(function () {
console.log(this);
});
I expect you need to put a limit on the traverse in case the parent you seek is not an ancestor of where you started. Otherwise, you either end up in an endless loop or run out of parents.
For example, a generic function to go from an element up to a parent with a particular tag name, you might do (not jQuery but I'm sure you can convert it if necessary):
function upTo(tagName, el) {
tagName = tagName.toLowerCase();
// Make sure el is defined on each loop
while (el && el.tagName && el.tagName.toLowerCase() != tagName) {
el = el.parentNode;
}
return el;
}
An alternative is to check for el.parentNode and stop when there are no more.
There is always an HTML node in an HTML document, so as long as you don't start from the document node, you'll always get there.
Related
Scenario: I have an unordered list of list items's. In each list item is a span and in each span is an img tag. So my html structure looks like this.
<ul class="slider-controls">
<li data-preview1="./images/1.1.jpeg" data-preview2="./images/1.2.jpeg"><span><img src="./images/color1.jpeg"></img></span></li>
<li data-preview1="./images/2.1.jpeg" data-preview2="./images/2.2.jpeg"><span><img src="./images/color2.jpeg"></img></span></li>
<li data-preview1="./images/3.1.jpeg" data-preview2="./images/3.2.jpeg"><span><img src="./images/color3.jpeg"></img></span></li>
</ul>
The img tags are just tiny square colour swatches, and the spans are styled into circles so what you have is basically a colour picker of three coloured dots for the user to click on.
When a user clicks on the li, I'm using javascript to grab the data-previews so that I can use that information to change a picture. This works perfectly if the user clicks slightly outside the circle. However, if they click inside the circle, they end up click on the img tag. But the data I need is two parent nodes up in the li tag!
So I wrote a recursive function to address this.
function findUpTag(el, tag) {
console.log(el.tagName, tag);
if (el.tagName === tag) {
console.log("success!", el);
return el;
} else {
findUpTag(el.parentNode, tag);
}
}
I use it like so findUpTag(el, "LI");
I know my recursive search is working because I always get the console.log("success") output when the current element === "LI".
However, my return value when I click on the image tag is always undefined! Even when my method finds the LI!
What am I doing wrong?
It's because you're not returning the result of the recursive call. You also need to handle the case of el.parentNode being null:
function findUpTag(el, tag) {
console.log(el.tagName, tag);
if (el.tagName === tag) {
console.log("success!", el);
return el;
} else {
return el.parentNode ? findUpTag(el.parentNode, tag) : null; // <====
}
}
FWIW, while you can use recursion for this, there's no need to; there's only one path up the tree. So it could just be a simple loop:
function findUpTag(el, tag) {
while (el) {
if (el.tagName === tag) {
console.log("success!", el);
return el;
}
el = el.parentNode;
}
return null;
}
I'm using jQuery traversing to jump between DOM elements.
First of i have a onClick function:
$(document).on('keyup', '.size, .ant', function(){
Inside of this function I send data about what's clicked, to another function.
sulorTableRowWeight( $(this) );
function sulorTableRowWeight(thisobj){
Now, I'd like to traverse from the clicked element $(this) to its parent. I'd like to find the parent's siblings and then traverse down to a specific sibling.
var inputSize = $(thisobj).parent().siblings('.sizeTd').children('.size');
My problem is when I want to traverse back down to the element I came from, it is not listed as a sibling because it isn't a sibling...
var inputSize = $(thisobj).parent().siblings(); console.log(inputSize)
console will give me the siblings, but not the one U came from...
So, when a user clicks ".size" I'd like to traverse up to the parent and back to size.... When a user clicks ".ant" I'd like to traverse up to the parent and then down to ".size"...
I tried to hardcode the traversing:
var inputSize = $(thisobj).parent().siblings('.sizeTd').children('.size');
But it won't work because it is not actually a sibling.
So what is it? And how can I get back to it?
If it is not possible, I have to run some if/else statements, U guess...
UPDATE
$(document).on('keyup', '.size, .ant', function(){
//Find changed <select> .tbody
var tbodyId = $(this).parent().parent('tr').parent('tbody').attr('id');
//Check <tbody> #id
if(tbodyId === "cpTableBody"){
}
else if(tbodyId === "sulorTableBody"){
sulorTableRowWeight( $(this) );
}
else if(tbodyId === "konturTableBody"){
konturTableRowWeight( $(this) );
}
else if(tbodyId === "kantbalkTableBody"){
kantbalkTableRowWeight( $(this) );
}
})
//Function sulorTableRowWeight
function sulorTableRowWeight(thisobj){
//Find the selected data-weight
var selectedWeightmm3 = $(thisobj).parent().siblings('.selectTd').children('.select').find(':selected').data('weightmm3');
//Find input .size value
var inputSize = $(thisobj).parent().siblings('.sizeTd').children('.size'); console.log(inputSize)
PROBLEM
My var inputSize will return undefined when I click a ".size" element. That´m's because it is not listed as a sibling to itself.
I know it's keyup, not click...
e.target will select the current input
$(document).on('keyup', '.size, .ant', function(e) {
inputSize = $(e.target);
if($(e.target).is('.ant')) {//test if the element is .ant
inputSize = $(e.target).parent().find('.size');//get .size based on .ant
}
console.log(inputSize[0]);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div>
<input class="size x1" placeholder="x1">
<input class="ant x1" placeholder="x1 ant">
</div>
<div>
<input class="size x2" placeholder="x2">
<input class="ant x2" placeholder="x2 ant">
</div>
Hmm, if you're passing in $(this) as thisObj I don't think you need to be nesting thisObj in a $(). (See note below)
Anyway, you could try using .parents('<grandparent>').find('<child>') so basically you're traversing one higher level up the tree with <grandparent>, then getting all the descendants that match the child selector. That should include the branch of the three that $(this) represents. But it's hard to say for sure without seeing your HTML.
** A good practice when assigning jQuery objects to variables is to use $ syntax, ie var $this = $(this) so you know anything prepended with a $ is a jQuery object.
inside sulorTableRowWeight , you should have the reference to the clicked element in thisobj variable.
I have a pure Javascript script with an onclick event that checks the value of the next sibling before deciding what to do. This will cause an error if the element clicked is the last element in its container (because of accessing nextSibling). I need to first check that the element clicked is not the last element in the container, but can't seem to find out how.
Note: I don't think this is a duplicate. There are quite a few questions about checking if an element is the last child, but all accepted answers—all answers in general—use JQuery.
You can use the .nextSibling property on the element and see if it comes back as empty (undefined, etc).
You can use the node.lastChild Property
The Node.lastChild read-only property returns the last child of the node. > If its parent is an element, then the child is generally an element node, > a text node, or a comment node. It returns null if there are no child elements..
var tr = document.getElementById("row1");
var corner_td = tr.lastChild;
The error you get should be some kind of can't set property on undefined.
You have just to check whether the next element exists:
if (typeof element.nextSibling === "undefined")
return;
For some reason none of the answers worked for me, I always ended up seeing a #text node instead of undefined or null, so I ended up comparing my element with the last child of the parent of the element:
element === element.parentNode.children[element.parentNode.children.length-1]
or if you want it as a function
function isLastChild(el) {
return (el === el.parentNode.children[el.parentNode.children.length-1])
}
//Useage
if(isLastChild(el)) {
//Element is the last child of its parent.
}
Might be longer than other answers but surly won't fail you.
Accessing an element's nextSibling element you'll get null if the element has no next sibling, so you can just check before going on with your code, like this:
if (myElement.nextSibling) {
// the element has a next sibling
// go on...
} else {
// the element is the last child
}
Use the .lastChild property of the node.
Example:
Here, we are removing last 4 child nodes in the list.
function clearAll() {
var sidemenu = document.getElementById('side_menu');
console.log("sidemenu.childNodes.length = " + sidemenu.childNodes.length);
while (sidemenu.childNodes.length > 2) {
console.log(sidemenu.childNodes);
sidemenu.removeChild(sidemenu.lastChild);
console.log("removed");
console.log("sidemenu.childNodes.length = " + sidemenu.childNodes.length);
}
console.log("What we have left now:");
console.log(sidemenu.childNodes);
}
clearAll();
<ul id="side_menu">
<li>List Item 1</li>
<li>List Item 2</li>
<li>List Item 3</li>
<li>List Item 4</li>
<li>List Item 5</li>
</ul>
Learn more: Node.lastChild - Web API Interfaces | MDN
This is one way to check:
document.querySelector(":last-child");
Here's one more:
var isLastChild = (element === element.parentNode.lastChild);
If you're trying to make this compatible with older browsers, just use childNodes:
// Last element in the body
document.getElementsByTagName('body')[0].childNodes[document.getElementsByTagName('body')[0].childNodes.length-1]
so for your particular problem, use the event object in your onclick:
element.onclick = function(event) {
var parent = event.target.parentNode;
if(event.target === parent.childNodes[parent.childNodes.length-1])
// Code here
}
How about var isLastChild = element === element.parentNode.lastChild?
Ask if there is a next element or not to know if it is the last or not:
if (typeof element.nextElementSibling == null) {
//is last
return;
} else {
//is not last
}
you must use nextElementSibling, no nextSibling or typeof ...
function isLastElement(element) {
return element.nextElementSibling === null;
}
I'm in a situation where I want to check if two elements (one is clicked and another one a reference) are the same, what I'm trying to do is:
$("#process li").click(function() {
currentElement = $(this);
referenceElement = $("#process li:first-child");
if (currentElement === referenceElement) {
$(".mark").removeClass("mark");
$(this).addClass("mark");
}
});
So what I want is to check if the clicked <li> is the first child of the ul#process and if so first remove a .mark class from another element and then add it to the clicked one. I don't get any working result - ideas anyone?
UPDATE:
Thanks you very much! This is my solution:
$("#processlist li").click(function() {
currentElement = $(this);
if (currentElement.is('li:first-child')) {
$(this).addClass("mark");
}
});
Now if I click on a , if it is the first child of this list, the class .mark is added - sweet!
Comparing objects in JS is very troublesome. The simplest way is to just pick a few key properties and compare those, eg:
if (currentElement.prop('id') === referenceElement.prop('id') {
// rest of your code...
}
However, given your use case you could use is:
if (currentElement.is('#process li:first-child')) {
// rest of your code...
}
Example fiddle
You need to extract the DOM element from the jQuery object. You can use the get method of jQuery for this.
e.g. if( currentElement.get( 0 ) === referenceElement.get( 0 ) )
I have the following code.
<html>
<head>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
</head>
<div id="hello">Hello <div>Child-Of-Hello</div></div>
<br />
<div id="goodbye">Goodbye <div>Child-Of-Goodbye</div></div>
<script type="text/javascript">
<!--
function fun(evt) {
var target = $(evt.target);
if ($('div#hello').parents(target).length) {
alert('Your clicked element is having div#hello as parent');
}
}
$(document).bind('click', fun);
-->
</script>
</html>
I expect only when Child-Of-Hello being clicked, $('div#hello').parents(target).length will return >0.
However, it just happen whenever I click on anywhere.
Is there something wrong with my code?
If you are only interested in the direct parent, and not other ancestors, you can just use parent(), and give it the selector, as in target.parent('div#hello').
Example: http://jsfiddle.net/6BX9n/
function fun(evt) {
var target = $(evt.target);
if (target.parent('div#hello').length) {
alert('Your clicked element is having div#hello as parent');
}
}
Or if you want to check to see if there are any ancestors that match, then use .parents().
Example: http://jsfiddle.net/6BX9n/1/
function fun(evt) {
var target = $(evt.target);
if (target.parents('div#hello').length) {
alert('Your clicked element is having div#hello as parent');
}
}
.has() seems to be designed for this purpose. Since it returns a jQuery object, you have to test for .length as well:
if ($('div#hello').has(target).length) {
alert('Target is a child of #hello');
}
Vanilla 1-liner for IE8+:
parent !== child && parent.contains(child);
Here, how it works:
function contains(parent, child) {
return parent !== child && parent.contains(child);
}
var parentEl = document.querySelector('#parent'),
childEl = document.querySelector('#child')
if (contains(parentEl, childEl)) {
document.querySelector('#result').innerText = 'I confirm, that child is within parent el';
}
if (!contains(childEl, parentEl)) {
document.querySelector('#result').innerText += ' and parent is not within child';
}
<div id="parent">
<div>
<table>
<tr>
<td><span id="child"></span></td>
</tr>
</table>
</div>
</div>
<div id="result"></div>
If you have an element that does not have a specific selector and you still want to check if it is a descendant of another element, you can use jQuery.contains()
jQuery.contains( container, contained )
Description: Check to see if a DOM element is a descendant of another DOM element.
You can pass the parent element and the element that you want to check to that function and it returns if the latter is a descendant of the first.
Ended up using .closest() instead.
$(document).on("click", function (event) {
if($(event.target).closest(".CustomControllerMainDiv").length == 1)
alert('element is a child of the custom controller')
});
You can get your code to work by just swapping the two terms:
if ($(target).parents('div#hello').length) {
You had the child and parent round the wrong way.
Without jquery
target.matches() with :scope
If you want to see if the target element has a parent which matches some selector use the .matches() method on the target and pass the selector followed by the :scope pseudo class.
The :scope here refers to the target element so you can use the in a :where pseudo class to help you write out a clean selector.
In the following example we will match all target elements which are a decedent of an a, button, or summary element.
const app = document.getElementById("app");
app.addEventListener("click", (event) => {
if (
event.target.matches(
":where(a, button, summary) :scope"
)
) {
console.log("click", event.target.parentNode.tagName);
}
});
<div id="app">
<button>
<span>Click Me</span>
</button>
<a href="#">
<span>Click Me</span>
</a>
<details>
<summary>
<span>Click Me</span>
</summary>
</details>
<span>Click Me</span>
<div>
Note the selector :where(a, button, summary) :scope could also have been written as:
a :scope,
button :scope,
summary :scope
parent.contains()
If you are interested in seeing if the target element is a child of a specific element use .contains() on the potential parent element:
const app = document.getElementById("app");
const button = document.getElementById("button");
app.addEventListener("click", (event) => {
if (button.contains(event.target)) {
console.log("click");
}
});
<div id="app">
<button id="button">
<span>Click Me</span>
</button>
<span>Click Me</span>
<div>
In addition to the other answers, you can use this less-known method to grab elements of a certain parent like so,
$('child', 'parent');
In your case, that would be
if ($(event.target, 'div#hello')[0]) console.log(`${event.target.tagName} is an offspring of div#hello`);
Note the use of commas between the child and parent and their separate quotation marks. If they were surrounded by the same quotes
$('child, parent');
you'd have an object containing both objects, regardless of whether they exist in their document trees.
To know more background info on Aleksandr Makov's answer, checking the below page might be helpful.
https://developer.mozilla.org/en-US/docs/Web/API/Node/contains
Node.contains()
The contains() method of the Node interface returns a boolean value indicating whether a node is a descendant of a given node, that is the node itself, one of its direct children (childNodes), one of the children's direct children, and so on.
It means, the answer is not using a reclusive function.