How to target a specific element using Javascript in nested code? - javascript

I ran into a problem of targeting different nested levels.
I read that it is possible to use .children feature + for loop, but I failed to do that.
Let's say I want to have a function where you pass the nesting level and it will change some property of <li> element on that level only.
I wrote this function to add classes to all last <li>
function changeElement(){
var lastLi = document.querySelectorAll('li:last-child');
for(i = 0; i < lastLi.length; i++){
lastLi[i].classList.add('last');
lastLi[i].style.background = 'green';
}
}
But now I need to target <li> elements on specific nested level
function changeElementOnLevel(level) {
var lastLi = document.querySelectorAll('li:last-child');
for (i = 0; i < lastLi.length; i++) {
//if level is 1 than we should change ul.root > li:last-child
//if level is 3 than we should change ALL <li:last-child> inside ul.root
> li > ul >
}
}
changeElementOnLevel(3)
<ul class='root'>
<li>Cat
<ul>
<li>Eats1</li>
<li>Sleeps</li>
<li>Snuggles</li>
<li>Plays</li>
<li>Meows</li>
</ul>
</li>
<li>Dog
<ul>
<li>Eats2</li>
<li>Sleeps</li>
<li>Snuggles</li>
<li>Plays</li>
<li>Barks</li>
</ul>
</li>
<li>Fish
<ul>
<li>Eats3</li>
<li>Sleeps</li>
<li>Swims</li>
<li>Plays</li>
<li>Swims</li>
</ul>
</li>

I consider that you have some nested ul tags, so in your example, the ul.root is level 1 and the inner ul is level 2 and if instead of 'Eats1' you have a ul tag, it will be level 3 and ...
Use this function:
function changeElementOnLevel(level){
let query = 'ul.root>li';
for (let i = 1; i < level; i++) {
query += '>ul>li'
}
query += ':last-child'
var lastLi = document.querySelectorAll(query);
for(i = 0; i < lastLi.length; i++){
// your code
}
}

It's depends what exactly you want to do. Are you going to change inner HTML on click? What do you mean by saying third level? You have 3 levels, and 3 lists. More information needed in order to help you.
var myElement = document.getElementsByClassName('root');
var arrayOfPets = myElement[0].children; // we are getting first level, all animals
var secondLevelArr = [];
for(i = 0; i < arrayOfPets.length; i++){
arrayOfPets[i].classList.add('last');
arrayOfPets[i].style.background = 'green';
var secondLevel = arrayOfPets[i].children[0].children;
// Push results to array
secondLevelArr.push(secondLevel);
} // will add class and background to all of them
// To add styles only to last element, you do not need to loop through them
arrayOfPets[arrayOfPets.length - 1].style.background = 'red';
for(i = 0; i < secondLevelArr.length; i++){
secondLevelArr[i][0].style.color = "white";
for(j = 0; j < secondLevelArr[i].length; j++){
secondLevelArr[i][j].style.textDecoration = 'line-through';
}
secondLevelArr[i][secondLevelArr[i].length - 1].style.textDecoration = 'none';
}
<ul class='root'>
<li>Cat
<ul>
<li>Eats1</li>
<li>Sleeps</li>
<li>Snuggles</li>
<li>Plays</li>
<li>Meows</li>
</ul>
</li>
<li>Dog
<ul>
<li>Eats2</li>
<li>Sleeps</li>
<li>Snuggles</li>
<li>Plays</li>
<li>Barks</li>
</ul>
</li>
<li>Fish
<ul>
<li>Eats3</li>
<li>Sleeps</li>
<li>Swims</li>
<li>Plays</li>
<li>Swims</li>
</ul>
</li>
</ul>

Here's what I came up with on the fly:
First, I'd make a small tweak to your HTML (I'm not sure what you have is actually valid...it might display how you like, but structurally it's going to cause problems). It is going to be difficult to set the value of an "li", if it has the value AND a nested list. If you reset the innerText or the innerHtml, you're going to completely overwrite the rest of the HTML in that tag, meaning you'll lose the nested list. You could work around this, but why bother, just close those tags predictably.
(Note I don't think any of the other answers address this issue).
So I'd first do this, notice how I close the "li" for Cat, Dog, and Fish:
<ul class='root'>
<li>Cat</li>
<ul>
<li>Eats1</li>
<li>Sleeps</li>
<li>Snuggles</li>
<li>Plays</li>
<li>Meows</li>
</ul>
<li>Dog</li>
<ul>
<li>Eats2</li>
<li>Sleeps</li>
<li>Snuggles</li>
<li>Plays</li>
<li>Barks</li>
</ul>
<li>Fish</li>
<ul>
<li>Eats3</li>
<li>Sleeps</li>
<li>Swims</li>
<li>Plays</li>
<li>Swims</li>
</ul>
</ul>
Now you can select elements and set values very straightforwardly (and the HTML is sound); the selectors here basically say "give me the li and ul children that are ONLY direct descendants of whatever element I'm currently working with" (otherwise you will get all of them no matter how deeply nested, which you don't want).
This code gets you the desired result by working recursively on nested "li" and "ul" collections, note that it also works on the top level "li" collection:
const top_layer = document.querySelectorAll ( '.root' );
const the_new_val = 'THE NEW VAL';
function setProps ( elems, level ) {
Array.from ( elems ).forEach ( x => {
const li = x.querySelectorAll ( ':scope > li' );
const ul = x.querySelectorAll ( ':scope > ul' );
if ( Array.from ( li ).length >= level ) {
li [ level ].innerText = the_new_val;
setProps ( li [ level ].children, level );
}
if ( Array.from ( ul ).length ) {
setProps ( ul, level );
}
});
}
setProps ( top_layer, 2 );
Yes, you could work with "children", but since we are directly interested in setting "li" values, which always appear in "ul" tags, the explicit selectors make it more obvious what's going on and would ignore any other children that may be around, feel free to make that change if you like.
The displayed result:

It is not very clear what you are trying to achieve :) But you can try :nth-child() - CSS pseudo-class selector that allows you to select elements based on their index (source order) inside their container.
This is just an example:
function find(n) {
// returns NodeList
var liNodeList = document.querySelectorAll('li:nth-child(' + n + ')');
console.log(li);
// if you need to do something with those elements, you can iterate
for (var i = 0; i < liNodeList.length; ++i) {
var item = liNodeList[i];
// do what you need with particular item
}
}
Also, the right method is querySelectorAll(...). What you are using querySelectAll does not exist.

Try like below,
I had used mix of query selectors and traversal to achieve this,
function changeElementOnLevel(level) {
var rootElement = document.querySelector(".root");
let targetLi;
if (level === 1) {
targetLi = rootElement.children[rootElement.children.length - 1];
let ul = targetLi.querySelector("ul"); // Since a ul is also there in level 1, I am expecting we need to change only li value not the ul
targetLi.textContent = "changed Value";
targetLi.append(ul);
} else if (level === 3) {
targetLi = rootElement.querySelector(
"li:last-child ul li:last-child"
);
targetLi.textContent = "changed Value";
}
}
changeElementOnLevel(3);
<ul class="root">
<li>
Cat
<ul>
<li>Eats1</li>
<li>Sleeps</li>
<li>Snuggles</li>
<li>Plays</li>
<li>Meows</li>
</ul>
</li>
<li>
Dog
<ul>
<li>Eats2</li>
<li>Sleeps</li>
<li>Snuggles</li>
<li>Plays</li>
<li>Barks</li>
</ul>
</li>
<li>
Fish
<ul>
<li>Eats3</li>
<li>Sleeps</li>
<li>Swims</li>
<li>Plays</li>
<li>Swims</li>
</ul>
</li>
</ul>

This code resolved my issue:
var parent = document.querySelector('.root');
function setFirstItemClass(element, level){
level = +level;
if(level == 1){
console.dir(element);
if(element.children.length > 0){
element.children[0].style.backgroundColor = 'red';
}
} else {
if(element.children.length > 0){
level--;
for(child of element.children){
setFirstItemClass(child, level);
}
}
}
}
setFirstItemClass(parent, 3);

Related

How do you toggle check marks on an HTML unordered list?

I have to create a list of items out of which user can choose any one item.
I have this code till now -
var state = 0;
function selectLI(element) {
if (state == 0) {
element.innerHTML = element.innerHTML + "<span class='liTick'>✔</span>";
state = 1;
} else {
var ele = document.getElementsByClassName('checklistLI');
for (var i = 0; i < ele.length; i++) {
var els = ele[i].getElementsByClassName(".liTick");
els.item(0).style.display = "none";
}
}
}
<ul class="checklist">
<li class="checklistLI" onclick="selectLI(this)">item 1</li>
<li class="checklistLI" onclick="selectLI(this)">item 2</li>
</ul>
What the code is supposed to do is first generate a tick and remove it if user chooses another time and generate a tick on that item instead. The first tick generates fine but it keeps giving me an error -
Uncaught TypeError: Cannot read properties of null (reading 'style')
when I try to remove the first tick and generate second one.
I could simply just use radio buttons but I don’t want that kind of UI.
There are two issues making it so your code doesn't hide the tick:
You've included a . in the class name when calling getElementsByClassName, but the class name doesn't actually have a dot in it.
You're looping through all the li elements, but only some of them have a liTick inside them; you're not allowing for the possibility you didn't find one.
The minimal fix is:
var els = ele[i].getElementsByClassName("liTick");
// No `.` here −−−−−−−−−−−−−−−−−−−−−−−−−−^
if (els[0]) { // <== Make sure there is one
els[0].style.display = "none";
}
Or in really up-to-date environments, you could use optional chaining:
var els = ele[i].getElementsByClassName("liTick");
els[0]?.style.display = "none";
But, I wouldn't do that, because it's just hiding the tick. The be consistent, if you're adding a tick, you should remove (not just hide) it.
Separately, you have only one state flag, but there are multiple elements. I'm going to assume they should be independently "tick"-able. For that, we want to base our decision on whether the element already has a tick in it.
Also, avoid using innerHTML (and in particular avoid .innerHTML = .innerHTML + "x"). Instead, just insert what you need.
Here's an example:
function selectLI(element) {
const tick = element.querySelector(".liTick");
if (tick) {
// Remove the tick
tick.remove(); // In modern environments
// Or: tick.parentNode.removeChild(tick); // In older environments
} else {
// Add the tick
element.insertAdjacentHTML(
"beforeend",
"<span class='liTick'>✔</span>"
);
}
}
<ul class="checklist">
<li class="checklistLI" onclick="selectLI(this)">item 1</li>
<li class="checklistLI" onclick="selectLI(this)">item 2</li>
</ul>
Or if you wanted only one item to be allowed to have a tick:
function selectLI(element) {
const tick = element.querySelector(".liTick");
if (tick) {
// Remove the tick
tick.remove(); // In modern environments
// Or: tick.parentNode.removeChild(tick); // In older environments
} else {
// Remove any other tick
const other = document.querySelector(".checklistLI .liTick");
if (other) {
other.remove(); // Or, again, the longer form in old environments
}
// Add the tick to this element
element.insertAdjacentHTML(
"beforeend",
"<span class='liTick'>✔</span>"
);
}
}
<ul class="checklist">
<li class="checklistLI" onclick="selectLI(this)">item 1</li>
<li class="checklistLI" onclick="selectLI(this)">item 2</li>
</ul>
Note that this is not accessible to people using assistive technologies. Consider using a <input type="checkbox"> instead.
As #Andy Holmes suggested in a comment:
This seems a lot more complicated for what it's actually doing. Why
don't you just put the ticks there by default, and when clicking on
the relevant li you just toggle a hidden class (which has display: none;) on the tick rather than this seemingly excessive code?
HTML
<ul class="checklist">
<li class="checklistLI" onclick="selectLI(this)">item 1 <span class='liTick hidden'>✔</span></li>
<li class="checklistLI" onclick="selectLI(this)">item 2 <span class='liTick hidden'>✔</span></li>
<li class="checklistLI" onclick="selectLI(this)">item 3 <span class='liTick hidden'>✔</span></li>
</ul>
CSS
.hidden {
display: none;
}
JS
function selectLI(element) {
document.querySelectorAll(".checklistLI").forEach(function (el) {
el.getElementsByClassName("liTick").item(0).classList.add("hidden");
});
element.getElementsByClassName("liTick").item(0).classList.remove("hidden");
}
Demo
checklist
You can use this solution, it's working!
<ul class="checklist">
<li class="checklistLI" onclick="selectLI(this, 1)">item 1</li>
<li class="checklistLI" onclick="selectLI(this, 2)">item 2</li>
</ul>
<script>
var state1 = 0;
var state2 = 0;
function selectLI(element, flag) {
if (flag === 1) {
if (state1 === 0) {
element.innerHTML = element.innerHTML + "<span class='liTick'>✔</span>";
state1 = 1;
} else {
element.innerHTML = 'item 1';
state1 = 0;
}
} else {
if (state2 === 0) {
element.innerHTML = element.innerHTML + "<span class='liTick'>✔</span>";
state2 = 1;
} else {
element.innerHTML = 'item 2';
state2 = 0;
}
}
}
</script>

Is there any programmtical way where I can remove class of list item

I am doing one loop where I am matching with some value and assigning class="hidden" to list. But When I run again the loop I want all my list should be without class, so I can assign for other value.
Here is my code.
for (i = list.children.length; i--;) {
li = list.children[i];
match = li.textContent.toLowerCase().indexOf(value.toLowerCase()) > -1;
li.classList.toggle('hidden', !match)
}
But before I run this loop I want all the list without any class so hete in the list I want to remove Class="hidden"
<li class="hidden">
Albania
</li>
Can anyone help me to achieve this
You want to do this before your existing loop? Try this:
var list = document.getElementById("list");
for (i = list.children.length; i--;) {
li = list.children[i];
li.classList.remove("hidden");
}
<ul id="list">
<li class="hidden">foo</li>
<li>bar</li>
<li class="hidden">baz</li>
</ul>
Though it does look like you could do this in the beginning of your existing loop. No need for another loop before that one.
Why not use a combo of Element.classList.contains(), ..remove(), ..add() etc. Lots of info on the MDN page.
For example:
for(i=list.children.length; i--;) {
li = list.children[i];
if (li.textContent.toLowerCase().indexOf(value.toLowerCase()) > -1) {
li.classList.add('hidden');
// or is it li.classList.remove('hidden'); here?
}
}
It is safe to ..add() even if the element has the class already. The class will not be added twice.

include variable with addEventListener

I´ve just started to learn JavaScript and made a click function to change value of i depending on which li element is being clicked. Instead of having 3x of basically same function is it possible to send variable with addEventLisener to check with a if statement. Because if I add 1 or 2 more li it would add a lot of unnecessary code.
HTML:
<div><img src="image1" />
<ul>
<li id="list1">1</li>
<li id="list2">2</li>
<li id="list3">3</li>
</ul>
</div>
JavaScript:
var list1 = getElementById('list1');
var list2 = getElementById('list2');
var list3 = getElementById('list3');
list1.addEventListener("click", clicked1);
list2.addEventListener("click", clicked2);
list3.addEventListener("click", clicked3);
var i = 0;
function clicked1(){
i= 0;
loop();
}
function clicked2(){
i = 1;
loop();
}
function clicked3(){
i = 2;
loop();
}
function loop(){
if (i > 2){
i=0;
}
var imageloop = getElementById('slideshow').getElementsByTagName('img');
imageloop[0].src = 'image' + i;
i++
setTimeout(loop,3000);
}
So when one of the li element is being clicked it will change the img currently displaying.
Bind to the list, not the list item - <ul id="ul1">... :
document.getElementById('ul1').addEventListener('click', function (e) {
var li = e.target;
alert(li.id); // list1, list2, respectively, etc.
});
http://jsfiddle.net/seancannon/D6HX4/
As Teemu said, bind to the list and use e.target. Then, change i according to e.target.innerHTML (because for your purposes, that's easier since the innerHTML is similar to the id, but simpler).
Assuming the <ul> element now has an id of "list":
var list = document.getElementById("list");
var i = null;
list.addEventListener("click", function(e) {
window.i = parseInt(e.target.innerHTML, 10);
console.log(window.i);
});
You don't need Separate code for each <li>,
Here is How to do it in single function:
HTML:
<div>
<ul id="All_List">
<li id="list1">1</li>
<li id="list2">2</li>
<li id="list3">3</li>
</ul>
</div>
JavaScript:
var clicked_li=0; /* It'll store the number of <li> being clicked */
var ul=document.getElementById("All_List"); /* <ul> Element with all <li> */
window.onload=function(){
var lis=ul.getElementsByTagName("li"); /* All <li>'s from <ul> */
for(var i=0;i<lis.length;i++){
var li=lis[i]; /* Specific <li> from <ul> ie, lis[0] means first <li> of <ul> */
li.addEventListener("click", function(){ /* Listen and bind click event to that <li> */
clicked_li=getIndex(this); /* Store Which <li> Was clicked in clicked_li */
alert("'list"+clicked_li+"' was clicked!");
/* if you just want to get the id you can use event.target.id; instead of getIndex(); */
}, false);
}
};
function getIndex(node) {
var childs = node.parentNode.getElementsByTagName("li"); /* Get parent <ul> and all child <li> of that parent */
for (var i = 0; i < childs.length; i++) {
if (node == childs[i]) break; /* Find which <li> from current <ul> was clicked */
}
return (i+1); /* Return No.(index) of that <li> starting with 1 */
}
and here is the:
fiddle for the same.
Hope it'll help you. Cheers!
This is an "improve/clearer" of Vedant Terkar answer. Which may give a more clear aspect on
the answer he gave.
if we give ul id group. We dont need id for each of the list-item inside the ul.
<ul id="group">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
Javascript
var i=0;
we set i to be a global variable and equal to 0 assuming it will display image(0) if nothing else is called.
window.onload=function(){
var lis=document.getElementById("group").getElementsByTagName('li');
We are now creating a Nodelist which is called lis. Then we loop through the NodeList to se which list-element is being clicked at.
for(i=0;i<lis.length;i++){
var li=lis[i];
li.addEventListener("click",index, false);
}
}
function index(){
var lis = document.getElementById("group").getElementsByTagName('li');
i = getIndex(this);
for(var a=0; a < lis.length; a++){
if(a ==i){
lis[i].innerHTML = 'Choosen';
}else{
lis[a].innerHTML = 'list'+a;
}
}
}
function getIndex(node) {
var childs = node.parentNode.getElementsByTagName("li");
for (var i = 0; i < childs.length; i++) {
if (node == childs[i]) break;
}
return (i);
}

Sort through multiple list using jQuery

I have multiple list on a page. An example of a list looks like this:
<ul class="effects-list">
<li data-sorte="2">creative</li>
<li data-sorte="1">euphoric</li>
<li data-sorte="2">uplifted</li>
<li data-sorte="1">energetic</li>
<li data-sorte="0">lazy</li>
<li data-sorte="1">focused</li>
<li data-sorte="2">happy</li>
<li data-sorte="0">talkative</li>
<li data-sorte="0">giggly</li>
<li data-sorte="0">tingly</li>
<li data-sorte="0">hungry</li>
<li data-sorte="0">sleepy</li>
<li data-sorte="0">aroused</li>
</ul>
I have a script that will remove all data-sorte that equals 0. After that is done, it sorts the list from highest to lowest (again by the numbers stored in data-sorte). It then takes the top three options and removes the rest.
Here is the script that does this:
$('*[data-sorte="0"]').remove();
$(".effects-list li").sort(sort_li_effects).appendTo('.effects-list');
function sort_li_effects(a, b){
return ($(a).data('sorte')) < ($(b).data('sorte')) ? 1 : -1;
}
$(".effects-list li").filter( function(k, v) {
if( k < 3 ) {
min = parseInt($(v).data('sorte'));
return false;
} else
return min > parseInt($(v).data('sorte'));
}).remove();
The problem I have is it sorts all of the list based on the first list. My question is how do I modify the script so it sorts all of the list on the page correctly?
Here is a jsFiddle with working code that shows the problem.
EDIT
To clarify a point. Lets say I have the following list:
<ul class="effects-list">
<li data-sorte="2">creative</li>
<li data-sorte="1">euphoric</li>
<li data-sorte="2">uplifted</li>
<li data-sorte="1">energetic</li>
<li data-sorte="0">lazy</li>
<li data-sorte="1">focused</li>
<li data-sorte="1">happy</li>
<li data-sorte="0">talkative</li>
<li data-sorte="0">giggly</li>
<li data-sorte="0">tingly</li>
<li data-sorte="0">hungry</li>
<li data-sorte="0">sleepy</li>
<li data-sorte="0">aroused</li>
</ul>
I would want it to show creative, uplifted, euphoric, energetic, focused and happy as those are the top options by the numbers. euphoric, energetic, focused and happy are all tied at 1 thus I want to show them all. The original script does this.
I slightly modified your script.
$('*[data-sorte="0"]').remove();
$(".effects-list").each(function() {
var $list = $(this),
$items = $list.find('li'),
sortes = [];
$items.detach().sort(sort_li_effects).filter(function(i) {
var sorte = $(this).data('sorte');
if (i < 3) {
sortes.push(sorte);
return true;
}
return sortes.indexOf(sorte) >= 0;
}).appendTo($list);
});
function sort_li_effects(a, b) {
return ($(a).data('sorte')) < ($(b).data('sorte')) ? 1 : -1;
}
http://jsfiddle.net/KK2bV/5/ or http://jsfiddle.net/KK2bV/6/ or http://jsfiddle.net/KK2bV/3/
Major differences:
It goes through li in every list and sorts correctly
It removes all unnecessary items at once using li:gt(2)
You can do this in the following three steps:
$('.effects-list').replaceWith(function() {
// step 1: grab list of items, remove all zero values and sort descending
var $items = $(this).children()
.filter(function() {
return $(this).data('sorte') !== 0;
}).sort(function(a, b) {
var a_value = $(a).data('sorte'),
b_value = $(b).data('sorte');
if (a_value != b_value) {
return a_value > b_value ? -1 : 1;
}
return 0;
}),
current,
levels = 0,
index = 0;
// step 2: work out the top three
while (index < $items.length && levels < 3) {
var value = $items.eq(index).data('sorte');
if (current === null || current != value) {
current = value;
++levels;
}
++index;
}
// step 3: replace the contents
return $items.slice(0, index);
});
See also: .replaceWith()
Demo
I don't like jQuery, however I can provide a javascript solution. I can recommend that you structure your HTML so that each UL has an ID that is enumerable. So:
<ul class="effects-list" id="list1"> ... </ul>
<ul class="...-list" id="list2">...<ul>
And so on. Then use a for loop to go through all the lists like this. Assume that there are 10 lists:
for (var i = 0; i < 10; i++) {
var list = document.getElementById("list"+i);
// Then run your jQuery code on the list 10 times.
}
Does that help?

Javascript Adding

Ok so I have columns I want added together if there is any information in them. So say I have
Accounts
1
2
3
.
There are 4 account spaces but only 3 accounts.
How do I create java script to add this up.
Live Example
HTML:
<ul>
<li id="accounts">
<p> Accounts </p>
<ul>
<li> 1 </li>
<li> 2 </li>
<li> 3 </li>
<li> . </li>
</ul>
</li>
</ul>
JavaScript:
// Get accounts, ul and set sum to 0
var acc = document.getElementById("accounts"),
ul = acc.getElementsByTagName("ul")[0],
sum = 0;
// Filter out text nodes. Returning an array of <LI> elements
var lis = Array.prototype.filter.call(ul.childNodes, function(li) {
if (li.tagName === "LI") {
return true;
}
});
// Loop through LIs adding up the sum
for (var i = 0, ii = lis.length; i < ii; i++) {
// If it's the last LI element then set the textContent.
if (i === ii - 1) {
lis[i].textContent = sum;
} else {
sum += +lis[i].textContent;
}
}
Disclaimer: Requires Modern Browser or Modernizr.
If your real markup is a list like that, you could do something like this:
// using jquery syntax for brevity; consider it pseudocode
var jList = $('#accounts');
function addColumns() {
var iSum = 0;
jList.find('li').each(function() {
var jLI = $(this);
if(parseFloat(jLI.text()) != 'NaN')
iSum += parseFloat(jLI.text());
});
return iSum;
}
This isn't super-great code. If you give us a little more info about what you're working with, something a little more robust should suggest itself. But the basic idea is that you check a set of elements-of-interest to see if they have summable content (i.e. if their text content can be interpreted as a number); then you add the summable items together.
The requirements of an algorithm like that will impose constraints on the way your "columns" can be marked-up. But there's a near-infinite set of possibilities.

Categories

Resources