Finding child element by class from parent with pure javascript cross browser - javascript

Following my code:
<div onclick="/*Here I would like to select the child element with the class 'vxf'*/">
<div class="abc"></div>
<div class="cir"></div>
<!--... other elements-->
<div class="vxf"></div>
<!--... other elements-->
</div>
<div onclick="/*Here I would like to select the child element with the class 'vxf'*/">
<div class="abc"></div>
<div class="cir"></div>
<!--... other elements-->
<div class="vxf"></div>
<!--... other elements-->
</div>
How to select the child element with the class "vxf" with pure javascript?

Pass this into your handler...
onclick="clickHandler(this)"
...and then for maximum browser compatibility, just look in the child nodes:
function clickHandler(element) {
var child;
for (child = element.firstNode; child; child = child.nextSibling) {
if (child.className && child.className.match(/\bvxf\b/)) {
break; // Found it
}
}
// ...
}
(Or keep looping and build up an array, if you want all matching children.)
On most modern browsers, another alternative is to use querySelector (to find the first) or querySelectorAll (to get a list) of matching child elements. Sadly, this requires a bit of a trick:
function clickHandler(element) {
var child, needsId;
needsId = !element.id;
if (needsId) {
element.id = "TEMPID____" + (new Date()).getTime();
}
child = document.querySelector("#" + element.id + " > .vxf");
if (needsId) {
element.id = "";
}
// ...
}
We have to play the id game because we only want direct children (not descendants), and unfortunately you can't use a child combinator without something on the left of it (so element.querySelector("> .vxf"); doesn't work).
If you didn't care whether it was a direct child or a descendant, then of course it's a lot easier:
function clickHandler(element) {
var child = element.querySelector(".vxf");
// ...
}

Just use this.getElementsByClassName('vxf')[0] in the div's onclick, and you have the element. See this fiddle.

in HTML5 you can use document.querySelector('.vxf')
As pointed out in other answers you can also use document.getElementsByClassName('vxf') for this specific requirement but the document.querySelector() and document.querySelectorAll() methods allow you to provide more complex selectors and therefore give you more power, so worth looking at for future.
See here for more information.

Related

Use .find() to get all descendants of an element but not their descendants

I have some html where elements are nested to create an accordion effect. Something similar to:
<span class="container">
<div class="item">1</div>
<span class="container">
<div class="item">1.a</div>
<span class="container">
<div class="item">1.a.1</div>
</span>
</span>
</span>
I need to use $('.container').find('.item').each() to create a hierarchy where 1.a becomes a child of 1 and 1.a.1 becomes a child of 1.a. The issue that I have is that 1.a.1 also becomes a child of 1 because 1.a.1 is a descendant of 1.
Theoretically there could also be 1.b or 1.c, etc. so I do need to find all descendants, not just the first one, but I don't want to find descendants of descendants.
Is there a simple jQuery way to do this?
.children() doesn't work for my specific case because there are more nested containers and whatnot in the actual HTML I'm in.
I've written the functionality here. The idea is that you're looking for all descendants with the item class. Then you remove all those elements from the list that have an ancestor with the item class. That ancestor still needs to be a descendant from the container (or your element starting point).
var container = $('.container');
var descendants = container.find('.item');
var res = [];
descendants.each(function() {
let parents = $(this).parents();
for (let i = 0; i < parents.length; i++) {
//console.log('p: ' + $(parents[i]).text());
if (container.html() == $(parents[i]).html()) {
//no ancestors with the `item` class found, this is a good one.
res.push($(this));
break;
}
if ($(parents[i]).hasClass('item')) {
//if an ancestor with the same class has been found
//it cannot be kept
break;
}
}
});
console.log(descendants);
console.log(res);
I created a fiddle to test

Find distance between elements in the DOM

There is a root element in the DOM tree and there is another element inside this root element nested somewhere. How do I calculate how nested is this another element inside the root element?
What I would like to know is essentially how many times I have to get the parent element of the nested element until I get to the root element. So I can solve this problem by iterating on the parents until I get to the root element, like in this fiddle.
const first = document.getElementById('search-target-1');
let parent = first.parentElement;
let level = 0;
do {
parent = parent.parentElement;
level++;
}
while (!parent.classList.contains('root'))
console.log(`The first element is ${level} levels deep inside the root div.`);
const second = document.getElementById('search-target-2');
parent = second.parentElement;
level = 0;
do {
parent = parent.parentElement;
level++;
}
while (!parent.classList.contains('root'));
console.log(`The second element is ${level} level deep inside the root div.`);
<div class="root">
<div class="first-level">
<div class="second-level" id="search-target-1">
<!-- How deep is this element? -->
</div>
</div>
<div class="first-level"></div>
<div class="first-level">
<div class="second-level">
<div class="third-level" id="search-target-2">
<!-- And this one? -->
</div>
</div>
</div>
</div>
Is there a better way of achieving this? I am looking for a javascript api to get the same result.
The element.matchesSelector does not solve my problem, as I know the target element is inside the root element, but I don't know how deep it is.
You could use jQuery's .parentsUntil() function to accomplish this:
var searchDistance_1 = $("#search-target-1").parentsUntil(".root").length; // 1
var searchDistance_2 = $("#search-target-2").parentsUntil(".root").length; // 2
That gives you the number of parents in between the child and root you are looking for. If you're looking for the number of jumps up the hierarchy needed to get to the parent, you can just add 1.
If you need to do this in vanilla JS, you could look through the source code for this function on GitHub.
Your code works, but as Niet the Dark Absol said you need to take care of cases where the descendent isn't a descendent, e.g.
function getLevel(parent, child) {
let level = 0;
while (child && parent != child) {
level++;
child = child.parentNode;
}
return child? level : null;
}
var parent = document.getElementById('parent');
var child = document.getElementById('child');
var notChild = document.getElementById('notChild');
console.log(getLevel(parent, child)); // 3
console.log(getLevel(parent, notChild)); // null
<div id="parent">
<div>
<div>
<div id="child"></div>
</div>
</div>
</div>
<div id="notChild"></div>
Without the guarding condition to stop when the loop runs out of parnetNodes, it will throw an error if child isn't a descendant of parent.

How to select next sibling of a certain type with JS?

I've got some html
<h4 id="start-here">title</h4>
<p>paragraph</p>
<p>paragraph</p>
...some number of paragraphs...
link
And I've got the <h4> with the id selected in JavaScript. How do I get from that selection in JS to the first <a> which is of the class link, or just the next sibling anchor tag?
Using document.querySelector() and a CSS selector, here with the general sibling combinator ~, you can achieve that like this:
A side note, in below samples I target inline style, though it is in general better to toggle a class.
Stack snippet
(function(){
document.querySelector('#start-here ~ a.link').style.color = 'red';
})();
<h4 id="start-here">title</h4>
<p>paragraph</p>
link
<p>paragraph</p>
link
Updated based on another question/comment, how to get more than one element in return.
With document.querySelectorAll() one can do similar, and target multiple elements like this.
Stack snippet
(function(){
var elements = document.querySelectorAll('#div2, #div3');
for (var i = 0; i < elements.length; i++) {
elements[i].style.color = 'red';
}
})();
<h4 id="start-here1">title</h4>
<div id="div1">some text</div>
<h4 id="start-here2">title</h4>
<div id="div2">some text</div>
<h4 id="start-here3">title</h4>
<div id="div3">some text</div>
The "start-here" ID on your element makes this easy. But let's imagine you have a reference to a DOM element without such a convenient selector, and you don't want to add a temporary ID to it.
In that case, you could use XPath with document.evaluate and your DOM reference as the second argument. Let's say you have that reference in yourElement and you want the next <section> sibling
const nextSibling = document.evaluate("following-sibling::section", yourElement, null,
XPathResult.FIRST_ORDERED_NODE_TYPE).singleNodeValue
I think to start with the first sibling, then i put all the siblings inside an array. Hence I extract what you want.
var x = document.getElementById("stat-here");
console.log(x)
var result = [],
node = x.nextSibling;
while ( node ) {
if (node.nodeType === Node.ELEMENT_NODE ) {
result.push( node );
}
node = node.nextElementSibling || node.nextSibling;
}
console.log(result, '\n Result: ',result[result.length-2])
<h4 id="stat-here">title</h4>
<p>paragraph</p>
<p>paragraph</p>
link

CSS - Parent child selector not working

I have the following code:
.recipe
.ingredients
= f.simple_fields_for :ingredients do |ingredient|
= render 'ingredient_fields', f: ingredient
.row#links
.col-xs-12
= link_to_add_association "", f, :ingredients
%hr
I need to select the ingredients div using jquery in the format of $("#links")["closest"](".recipe > .ingredients") but this doesn't select anything.
It's frustrating though as $("#links")["closest"](".recipe > .row") will return the correct div.
Fiddle of what works and what I want: https://jsfiddle.net/yL6dr4s1/
According to jQuery documentation, closest method tries to find element matching the selector by testing the element itself and
traversing up through DOM.
It does not go through siblings of the element.
Based on your requirements, it seems like you want to traverse the tree for getting match in siblings. jQuery has siblings method to do that. So one solution would be to use siblings method like:
$("#links")["siblings"](".recipe > .ingredients")
Another soultion would be to get closest parent and then use children as answered by #mhodges
As for the query $("#links")["closest"](".recipe > .row"):
It works fine because closest method finds the match in the element itself.
Here is the example to showcase that:
$(document).ready(function() {
// Match found because it is parent
console.log($("#links")["closest"](".wrapper").length);
// No match found because element is sibling
console.log($("#links")["closest"](".row1").length);
// No match found because element is sibling
console.log($("#links")["closest"](".row3").length);
// Match found because it is element itself
console.log($("#links")["closest"](".row2").length);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="wrapper">
<div class="row1">
<span>Content1</span>
</div>
<div class="row2" id="links">
<span>Content2</span>
</div>
<div class="row3">
<span>Content3</span>
</div>
</div>
I am not sure of your requirements on using the exact selector/syntax you provided, but this selector works exactly how you want it to.
$(this).closest(".recipe").children(".ingredients").append('<br/><input type="text" value="Flour">');
Edit
This is the closest I could get:
$(this)["closest"](".recipe").children(".ingredients").append('<br/><input type="text" value="Flour">');
I don't think you can use the selectors in the way you propose.
As far as the DOM is concerned (and jQuery), the element defined by ingredient and the element defined by row are not related. You have to traverse up to the parent element, then back down to get to the child.
Here is a fiddle that hopefully demonstrates the issue.
If you can change it so that ingredient and row are both within the same parent div, you might have more luck with your test selector syntax.
When jQuery gets to buggy, doesn't have a certain option or just becomes to messy to use for a certain operation, it is good we also have access to good old plain javascript.
document.querySelector('#addToIngredients').addEventListener('click' , function(e) {
var recipe = getClosest(e.target,'recipe');
if (recipe) {
var ingred = recipe.querySelector('.ingredients');
ingred.innerHTML += '<br/><input type="text" value="Flour">';
}
});
function getClosest(elem,cls) {
var el = elem.parentNode;
while (el){
if (el.className.indexOf(cls) > -1) {
return el;
}
el = el.parentNode;
}
return false;
}
<div class="recipe">
<div class="ingredients">
<input type="text" value="Eggs"><br/>
<input type="text" value="Flour">
</div>
<div class="row">
<div class="col-xs-12">
Add to .ingredients
</div>
</div>
<hr/>
</div>
Of course they can be combined
$(function() {
$("#addToIngredients").on('click', function(e) {
var recipe = getClosest(e.target,'recipe');
if (recipe) {
var ingred = recipe.querySelector('.ingredients');
ingred.innerHTML += '<br/><input type="text" value="Flour">';
}
});
})

jQuery function similar to closest that will return elements outside of the parent chain

Is there any jQuery function similar to closest() that will return elements outside of the parent chain, traversing sideways? For example, I want to call a function foo() on the div source that would return the div target. I know I could navigate using parent() and siblings(), but I need something generic that would go as many levels as needed, up, sideways and down?
var allsources = $('.source');
allsources.click(function()){
$(this).closest('.target').hide();
});
<div class="row">
<div>
<div class="target" ></div>
</div>
<div>
<div>
<div class="source"></div>
</div>
</div>
</div>
<div class="row">
<div>
<div class="target" ></div>
</div>
<div>
<div>
<div class="source"></div>
</div>
</div>
</div>
EDIT:
My definition of closest: you have an element source. Try to find it down. If find more than one, return one that is less node hoops down/next/prev. If not found, go one level up, and try to find again. Repeat until no parent.
If, by closest, you mean "travel up as little as possible, then anywhere downwards", then you can do
$("#source")
.closest(":has(.target)")
.find(".target:first") //make sure we only select one element in case of a tie
In your case, it would be better to specify the common parent directly:
$(this)
.closest(".row")
.find(".target") //there's no tie here, no need to arbitrate
This is a tricky one. As has been commented, how do you define closest in this context? Assuming you can decide on some rules; for example:
Traverse up: 3pt
Traverse down: 2pts
Move sideways: 1pts
And then consider the item with the lowest points to be "closest" then it would be easy enough to author a plugin, named something such as closestAll, which would do the recursive traversal of the whole dom tree to determine the closest item.
However, looking at your recent edit, one (of many!) right solutions to the problem stated is:
var allsources = $('.source');
allsources.click(function(){
$(this).parents('.row').find('.target').hide();
});
Live example: http://jsfiddle.net/zCvJM/ (Source A only hides Target A, Same for B)
If you know exactly the structure of the dom and level of nesting, have you consider to use the eq() method
$(this).parents().eq(1).prev().children(".target")
I don't think there is a way to do this other than basically querying the whole DOM:
$('#target')
Because if you want to go up and across (never mind down as well) then the target element isn't related to the child element. If you also want to check for the presence of the child element you will have to do that separately.
-Edit:
After reading your comment on wanting to find the closest element regardless of whether it is a parent, I think you will have to write a custom function to crawl back up the dom one node at a time. I have tested the following and it works:
Markup
<div id="parent">
<div id="child1">
<div id="source"></div>
</div>
<div id="child2">
<div class="target" rel="right"></div>
</div>
<div id="child3">
<div>
<div class="target" rel="wrong"></div>
</div>
</div>
</div>
Script
$(document).ready(function () {
var tgt = findClosest($('#source'), '.target');
if (tgt != undefined) {
alert(tgt.attr('rel'));
}
});
function findClosest(source, targetSel) {
var crawledNodes = $();
var target = null;
// Go up
source.parents().each(function () {
console.log(crawledNodes.index($(this)));
if (crawledNodes.index($(this)) == -1 && target == null) {
crawledNodes.add($(this));
target = findTarget($(this), targetSel);
// Go across
$(this).siblings().each(function () {
console.log("Sibling");
if (crawledNodes.index($(this)) == -1 && target == null) {
crawledNodes.add($(this));
target = findTarget($(this), targetSel);
}
});
}
});
return target;
}
function findTarget(el, targetSel) {
console.log(targetSel);
var target = el.find(targetSel);
if (target.size() > 0) {
return target.eq(0);
}
else
{
return null;
}
}
If I understood the specification correctly you mean something like the function closest defined below:
var allsources = $(".source");
function closest($source,selector) {
if($source == null) return $([]);
var $matchingChildren = $source.find(selector);
if($matchingChildren.length != 0) return $($matchingChildren.get(0));
else return closest($source.parent(), selector)
}
allsources.click(closest($(this),'.target').hide();});
You can see it working at http://jsfiddle.net/y2wJV/1/
Your definition requires that when choosing among matching children the function must return one that is less node hoops down/next/prev. This requirement has not been met, but this function is quite flexible and seems to do what you want to do in the case of the example you provided.
I found this code that is simple but does not solve the tie issue (returns the first)...
(function ($) {
$.fn.findClosest = function (filter) {
var $found = $(),
$currentSet = this; // Current place
while ($currentSet.length) {
$found = $currentSet.find(filter);
if ($found.length) break; // At least one match: break loop
// Get all children of the current set
$currentSet = $currentSet.parent();
}
return $found.first(); // Return first match of the collection
};
})(jQuery);
I encountered a similar problem, i had a table i needed to find the next element which may be outside the current td, so i made a jquery function:
$.fn.nextAllLevels = function(sel) {
if ($(this).nextAll(sel).length != 0) {
return $(this).nextAll(sel).eq(0);
} else if ($(this).nextAll(':has(' + sel + ')').length != 0) {
return $(this).nextAll(':has(' + sel + ')').find(sel).eq(0);
} else {
return $(this).parent().nextAllLevels(sel);
}
So to use this you simply call
$('#current').nextAllLevels('.target');
To give you the element closest in the foward direction, regardsless of whether in is in the current parent or not.

Categories