Recursively searching up a node tree - javascript

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;
}

Related

Click to change image across multiple items

I'm trying to set up a javascript function that will take a down arrow image and swap it with an up arrow image when clicked. It worked when the image had an id, but because I have a couple menu items that uses the arrow image, I wanted to try to get it to work as a class name or just regular name, but I can't get the function to change the source of the image when it's not an id. Can anyone help?
Here is my current HTML markup:
<div class="header__links hide-for-mobile" >
<a id="subMenu" href="#">Features <img class="downArrow" src="/assets/images/icon-arrow-down.svg"></a>
Company
Careers
About
</div>
And here is my JS
const subMenu = document.querySelector('#subMenu');
subMenu.addEventListener('click', function() {
if(subMenu.classList.contains('subOpen')) {
subMenu.classList.remove('subOpen');
document.getElementsByClassName('downArrow').src="/assets/images/icon-arrow-down.svg";
}
else {
subMenu.classList.add('subOpen');
document.getElementsByClassName('downArrow').src="/assets/images/icon-arrow-up.svg";
}
})
Right now since your markup only contains a single element with the downArrow class you could update your code to:
const subMenu = document.querySelector('#subMenu');
subMenu.addEventListener('click', function() {
if(subMenu.classList.contains('subOpen')) {
subMenu.classList.remove('subOpen');
document.getElementsByClassName('downArrow')[0].src="/assets/images/icon-arrow-down.svg";
}
else {
subMenu.classList.add('subOpen');
document.getElementsByClassName('downArrow')[0].src="/assets/images/icon-arrow-up.svg";
}
})
But a better way to do this would be to loop through the results of the getELementsByClassName method. You could do that with something like:
const subMenu = document.querySelector('#subMenu');
subMenu.addEventListener('click', function() {
if(subMenu.classList.contains('subOpen')) {
subMenu.classList.remove('subOpen');
Array.from(document.getElementsByClassName("downArrow")).forEach(
function(element, index, array) {
element.src="/assets/images/icon-arrow-down.svg";
}
);
}
else {
subMenu.classList.add('subOpen');
Array.from(document.getElementsByClassName("downArrow")).forEach(
function(element, index, array) {
element.src="/assets/images/icon-arrow-up.svg";
}
);
}
})

Insert text into HTML element with Javascript function return

This is a piece of my HTML code:
<label for="input_train"><script>mountainTrains("all");</script></label>
mountainTrains() is a javascript function, which returns string. I want this string to be passed as a text of the label element on the website. This piece of code however only results in an empty string, even if mountainTrains() returns non-empty string. Is there anything wrong about the syntax I'm trying to access javascript function from HTML?
Function definition:
function mountainTrains(par) {
if (par !== 'all') {
return 'One train';
} else {
return 'All trains';
}
}
To edit HTML with JS, don't put the script tag inside the element you're trying to edit. Instead do something like this:
function mountainTrains(par) {
if (par !== 'all') {
return 'One train';
} else {
return 'All trains';
}
}
document.getElementById("my_label").innerHTML = mountainTrains("all");
<label for="input_train" id="my_label"></label>
I will suggest you to modify the text content of the label element like the following way:
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelector('label[for=input_train]').textContent = mountainTrains("all");
function mountainTrains(par) {
if (par !== 'all') {
return 'One train';
} else {
return 'All trains';
}
}
});
</script>
<label for="input_train"></label>
The only way to make this work by simply placing the function call inside the element and not passing in any element identifiers is to use document.currentScript and then setting the text of the parentNode.
Note this is not commonly done. I'm just pointing out how it can be done.
I would not pursue this very far as there more robust approaches depending on your use case that don't require individual script tags for each call
label{ padding: 1em; border:1px solid grey}
<script>
function mountainTrains(par) {
let txt = par === 'all' ? 'All trains' : 'One train';
document.currentScript.parentNode.textContent = txt
}
</script>
<label for="input_train"><script>mountainTrains("all");</script></label>
<label for="another_train"><script>mountainTrains("one");</script></label>
You are trying to use JavaScript for DOM manipulation
I would not recommend to insert the script tag in the label itself. The best practice is to put it right before the closing tag of the body element, more here
also the function to add this text insde the lable should look something like this
document.getElementsByClassName("random_class_name")[0].innerHTML = 'the text for the label'
<label class="random_class_name" for="input_train"></label>
<input name="input_train" type="text">
Note that we are referring to document.getElementsByClassName("random_class_name")[0] it is because getElementsByClassName function returns all elements that have such a class name so if there are 10 of them you can refference them like you would an array. This particular one is called a NodeList
<label for="input_train" onclick="event.target.innerText = mountainTrains('all');">text</label>
<script>
function mountainTrains(par) {
if (par !== 'all') {
return 'One train';
} else {
return 'All trains';
}
}
</script>

empty span elements within div block

I'd like to append updated javascript data to 2 span tags, rescued and rescued2, when it's available. Since I'm appending new data, I first need to empty out the tags first so they don't keep reappending data...
function updateHUD() {
$('#bottomDisplay').empty();
$('#rescued').append("Total Animals Rescued: " + rescuedTotal_Array.length);
$('#rescued2').append("Total Animals Rescued lol: " + rescuedTotal_Array.length);
}
<div id="bottomDisplay">
<ul>
<li>Total Animals Rescued: <span id="rescued"></span></li>
<li>Total Animals Rescued2: <span id="rescued2"></span></li>
</ul>
</div>
I'd like to do the clear that way so I don't need to individually clear each span... but instead clear the entire div block then start over. Instead, when I clear the div block, it just erases all spans inside.
Why is this not acceptable?
On another note, I suppose I can just skip the clearing and appending, and just set the text...
$('#rescued').text(rescuedTotal_Array.length);
$('#rescued2').text(rescuedTotal_Array.length);
.empty gets rid of everything inside the selected element. If you want to empty both spans in one go, you'd need to do:
$('#bottomDisplay').find('span').empty();
For reference, here's what .empty looks like:
empty: function() {
var elem,
i = 0;
for ( ; (elem = this[i]) != null; i++ ) {
if ( elem.nodeType === 1 ) {
// Prevent memory leaks
jQuery.cleanData( getAll( elem, false ) );
// Remove any remaining nodes
elem.textContent = "";
}
}
return this;
}
elem.textContent = ""; is what is getting rid of everything inside.
I'm a little confused by the question but I think I understand. Correct me if I'm not understanding you fully.
You are looking for a single line to empty both spans. However, you want to leave the rest of the HTML inside the div intact and you don't want to have to target each individual span. The following line would work:
$('#bottomDisplay span').empty();

Checking if Element hasClass then prepend and Element

What I am trying to achieve here is when a user clicks an element it becomes hidden, once this happens I want to prepend inside the containing element another Element to make all these items visible again.
var checkIfleft = $('#left .module'),checkIfright = $('#right .module');
if(checkIfleft.hasClass('hidden')) {
$('#left').prepend('<span class="resetLeft">Reset Left</span>');
} else if(checkIfright.hasClass('hidden')) {
right.prepend('<span class="resetRight">Reset Right</span>');
}
I tried multiple ways, and honestly I believe .length ==1 would be my best bet, because I only want one element to be prepended. I believe the above JS I have will prepend a new element each time a new item is hidden if it worked.
Other Try:
var checkIfleft = $('#left .module').hasClass('hidden'),
checkIfright = $('#right .module').hasClass('hidden');
if(checkIfleft.length== 1) {
$('#left').prepend('<span class="resetLeft">Reset Left</span>');
} else if(checkIfright.length== 1) {
right.prepend('<span class="resetRight">Reset Right</span>');
}
else if(checkIfleft.length==0){
$('.resetLeft').remove()
} else if (checkIfright.length==0){
$('.resetRight').remove()
}
Basically if one element inside the container is hidden I want a reset button to appear, if not remove that reset button...
hasClass() only works on the first item in the collection so it isn't doing what you want. It won't tell you if any item has that class.
You can do something like this instead where you count how many hidden items there are and if there are 1 or more and there isn't already a reset button, then you add the reset button. If there are no hidden items and there is a reset button, you remove it:
function checkResetButtons() {
var resetLeft = $('#left .resetLeft').length === 0;
var resetRight = $('#left .resetRight').length === 0;
var leftHidden = $('#left .module .hidden').length !== 0;
var rightHidden = $('#right .module .hidden').length !== 0;
if (leftHidden && !resetLeft) {
// make sure a button is added if needed and not already present
$('#left').prepend('<span class="resetLeft">Reset Left</span>');
} else if (!leftHidden) {
// make sure button is removed if no hidden items
// if no button exists, this just does nothing
$('#left .resetLeft').remove();
}
if (rightHidden && !resetRight) {
$('#right').prepend('<span class="resetRight">Reset Right</span>');
} else if (!rightHidden) {
$('#right .resetRight').remove();
}
}
// event handlers for the reset buttons
// uses delegated event handling so it will work even though the reset buttons
// are deleted and recreated
$("#left").on("click", ".resetLeft", function() {
$("#left .hidden").removeClass("hidden");
$("#left .resetLeft").remove();
});
$("#right").on("click", ".resetRight", function() {
$("#right .hidden").removeClass("hidden");
$("#right .resetRight").remove();
});
FYI, if we could change the HTML to use more common classes, the separate code for left and right could be combined into one piece of common code.
Add the reset button when hiding the .module, if it's not already there :
$('#left .module').on('click', function() {
$(this).addClass('hidden');
var parent = $(this).closest('#left');
if ( ! parent.find('.resetLeft') ) {
var res = $('<span />', {'class': 'resetLeft', text : 'Reset Left'});
parent.append(res);
res.one('click', function() {
$(this).closest('#left').find('.module').show();
$(this).remove();
});
}
});
repeat for right side !
I've recently experimented with using CSS to do some of this stuff and I feel that it works quite well if you're not trying to animate it. Here is a jsfiddle where I can hide a module and show the reset button in one go by adding/removing a 'hideLeft' or 'hideRight' class to the common parent of the two modules.
It works by hiding both reset button divs at first. Then it uses .hideLeft #left { display:none;} and .hideLeft #right .resetLeft { display: block; } to hide the left module and display the reset button when .hideLeft has been added to whichever element both elements descend from. I was inspired by modernizr a while back and thought it was a neat alternative way to do things. Let me know what you think, if you find it helpful, and if you have any questions :)

how do i traverse ancestors with jQuery?

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.

Categories

Resources