How to get dynamic innerHTML values? - javascript

So I retrieve a list of data and each piece of data has an ID provided by SQL. The ID is stored in a <p> tag, and I extract the ID from the <p> tag using innerHTML.
I have an event handler that will delete the element on click based on its ID, But after I click delete on my first element, the innerHTML value remains the same. For example, say the ID of the first element was 10, once I click delete, it will delete it. But once I click delete on the element with the ID of 11, it still says the value is 10 and will NOT delete the element with the ID of 11.
document.addEventListener('click', async (e) => {
e.preventDefault();
if (e.target && e.target.id === 'delete-btn') {
// Reset the notification bar to be displayed again
resetNotification();
let movieToDeleteID = document.getElementById('primary-key').innerHTML;
console.log(movieToDeleteID)
await axios.post('http://localhost:5000/movies/delete', { movieToDeleteID })
.then(response => {
showNotification(response);
movieID.value = ''
})
.catch(err => console.error(err));
}
})
This is the code I'm using currently.
How can I make it so that the value of movieToDeleteID changes with each click of the delete button to reflect that element's ID?
Thank you!

In your code, the line
let movieToDeleteID = document.getElementById('primary-key').innerHTML;
will always get the inner HTML of the element with id='primary-key'-- and every id value on your page must be unique.
If this element's innerHTML content doesn't change between clicks, every click will have the same result as the first.
(The same goes for your id='delete-btn', by the way: there can be only one such element on your page. And your script only runs when you click that particular element, but I think this is probably what you intended.)
If you have one p element per movie, each holding a different movieID, you need to make sure they don't share id values, which may mean you want to use a different selector (besides .getElementById), such as .querySelector or .getElementsByClassName.
It's impossible to give you a 100% solution without seeing your HTML, but here's how it could work in a fabricated HTML structure:
// Selects the outermost div
const container = document.getElementById("container");
// Calls `deleteMovie` on click
container.addEventListener("click", deleteMovie);
// Defines `deleteMovie`
function deleteMovie(event){
// Gets the clicked element and quits early if it's not a button
const clickedEl = event.target;
if(!clickedEl.classList.contains("delete-btn")){ return; }
// Gets the text in the `primary-key` element (identifying its parent div as well)
const
movieDiv = clickedEl.parentElement,
idParagraph = movieDiv.querySelector(".primary-key"),
movieIdToDelete = idParagraph.textContent;
// Calls the DB-communication function (which, here, just logs success)
sendDeleteRequestToDatabase(movieIdToDelete);
// Calls a function to update the page
hideMovieFromList(movieIdToDelete);
}
// Defines `sendDeleteRequestToDatabase`
function sendDeleteRequestToDatabase(movieId){
console.log(`Deleted movie #${movieId}`);
}
//Defines `hideMovieFromList`
function hideMovieFromList(movieId){
// Selects all elements w/ `class='primary-key'`
let idPs = document.getElementsByClassName("primary-key");
// Loops through selected elements to find matching text
for(let p of idPs){
if(p.textContent === movieId){
// Removes movie div from top-level div
const movieDiv = p.parentElement;
container.removeChild(movieDiv);
break; // We already found a match, so might as well stop looking
}
}
}
h3, p{ margin: 0; padding: 0; }
.movie{ width: 15em; border: 1px solid grey; margin: 3px; padding: 3px; }
<div id="container">
<div class="movie">
<h3 class="title">Dracula</h3>
<p class="genre">Horror</p>
<p class="primary-key">1</p>
<button class="delete-btn">Delete</button>
</div>
<div class="movie">
<h3 class="title">Captain Marvel</h3>
<p class="genre">Action</p>
<p class="primary-key">2</p>
<button class="delete-btn">Delete</button>
</div>
<div class="movie">
<h3 class="title">When Harry Met Sally</h3>
<p class="genre">Comedy</p>
<p class="primary-key">3</p>
<button class="delete-btn">Delete</button>
</div>
</div>

Related

Why are div elements not re-indexed after removing one of them with JavaScript?

I want to remove a div with the index == 0 each time I click on one of the elements with the class .slide.
It works the first time, but when I try again, the code tries to remove the element that has been previously removed, but I can still log the element to the console.
I thought that index "0" will be assigned to the next div sharing the same class, so the next time I click, this next div would be deleted.
What am I missing here?
Here is my code:
let slides = document.querySelectorAll(".slide")
slides.forEach((el) => {
el.addEventListener("click", () => {
// remove the first div
slides[0].remove()
// the element above has been removed, but I still can log it out (?)
// and it seems to keep the index [0]
console.log(slides[0])
})
})
This will do exactly what you expect - and will only get from the live HTMLCollection that getElementsByClassName returns:
let slides = document.getElementsByClassName("slide")
for (const slide of slides) {
slide.addEventListener("click", () => {
// remove the first div
slides[0].remove()
console.log(slides[0])
})
}
<div class="slide">0</div>
<div class="slide">1</div>
<div class="slide">2</div>
<div class="slide">3</div>
<div class="slide">4</div>
<div class="slide">5</div>
I believe you have deleted the element from the DOM, but have not removed it from the array. Try using the shift array method to remove the first element that you are deleting.
slides.forEach((el)=>{
el.addEventListener("click",()=>{
slides.shift().remove();
})
}
Based on your feedback, and the research I did ( on live and static node lists ) I came up with the following solution to my question. As you can see I am not using a querySelection nodelist anymore as this type is static. Instead, I went for a getElementsByClassName HTML collection. What do you think of this? Thanks for your precious support!
let sliderCollection = document.getElementsByClassName("slider") // HTML COLLECTION
let arrayCollection =[]
arrayCollection = Array.from(sliderCollection) // HTML COLLECTION turned into an array
console.log("initial number of divs:",arrayCollection.length) // initial number of divs
arrayCollection.forEach( (el)=>{
el.addEventListener('click',()=>{
sliderCollection[0].remove() // remove always the first element
arrayCollection = Array.from(sliderCollection) // refill the array with the updated number of divs
console.log(arrayCollection.length) // keep track of the current number of divs
})
})
.container {
display: flex;
flex-direction: row;
}
.slider {
height: 100px;
width: 100px;
background-color: red;
border: solid 2px black;
font-size: 3em;
}
<div class="container">
<div class="slider">0</div>
<div class="slider">1</div>
<div class="slider">2</div>
<div class="slider">3</div>
<div class="slider">4</div>
<div class="slider">5</div>
</div>

Why innerText does work for invisible elements? It should not

textContent gets the content of all elements, including
<script> and <style> elements. In contrast, innerText only
shows "human-readable" elements.
textContent returns every element in the node. In contrast,
innerText is aware of styling and won't return the text of
"hidden" elements.
- https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent#differences_from_innertext
So it seems that innerText should ignore invisible elements, but acutally it doesn't. Why? It seems I misunderstood something.
Example 1
const style = document.querySelector('style');
style.textContent = 'p { color: red; }'; // works
style.innerText = 'p { color: red; }'; // works. But why?
console.log(style.textContent); // works
console.log(style.innerText); // works. But why?
html { font-family: sans-serif; }
<p>foo</p>
Example 2
const invisibleDiv = document.querySelector('div');
console.log(invisibleDiv.innerText); // works. But why?
<div style="display: none;">
invisible div
</div>
The difference only applies to elements that are nested within the element, not the element's text itself.
console.log("outer.textContent:", outer.textContent);
console.log("outer.innerText:", outer.innerText);
console.log("inner.textContent:", inner.textContent);
console.log("inner.innerText:", inner.innerText);
<div id="outer">This is visible <span id="inner" style="display: none;">This is invisible</span></div>
Because the element is still in the DOM at the point in time JavaScript references it.
To force the alert to fail, you need to remove the element with something like .remove(), and then (attempt to) reference it.
As you can see in the following, even a Promise's resolution still retains the existing .innerText, as the reference was made before the element was updated. You explicitly need to update the reference:
let invisibleDiv = document.getElementById('alert');
const resolvedProm = Promise.resolve(invisibleDiv.remove());
let thenProm = resolvedProm.then(value => {
console.log(invisibleDiv.innerText); // Still exists
invisibleDiv = document.getElementById('alert');
console.log(invisibleDiv); // No longer exists
return value;
});
<div id="alert">invisible div</div>

How can I access the inner HTML of an element while looping over it with a class selector?

I am working on a project that has several paragraphs of text with keywords (set to overflow so you can scroll them in a text box). On hover, these keywords should populate another part of the page with images related to the keyword itself. My plan has been to put these keywords in spans with the same class name and then to select them all, using forEach to make each of them hoverable, and on hover to have them execute the popImage function.
This is where I'm stuck: I need popImage to be able to access the inner HTML of the specific keyword when it runs, so that it can compare that to the JSON list in which I'm storing the specific images that correspond to those keywords. Right now on my site, it is logging only the second keyword.
const popImage = () => {
//this is where I'm hoping to make images load in to another div.
console.log(name) //for testing to see if I can compare the value of name (stringified) to a list of object names and grab the images from the matching one.
}
//make keywords hoverable and run popImage
list = document.querySelectorAll(".keyword");
list.forEach((item)=>{
name = item.innerHTML; //I think this is the problem but couldn't figure out how else to do it.
item.addEventListener("mouseover", popImage);});
<div id="text">
Hovering over the word <span class="keyword">horse</span> should populate a separate div with images of horses, while hovering over <span class="keyword">dog</span> should populate it with images of dogs.
</div>
Change popImage() to take the name as an argument, and then use a closure in addEventListener().
const popImage = (name) => {
console.log(name)
}
//make keywords hoverable and run popImage
list = document.querySelectorAll(".keyword");
list.forEach((item) => {
let name = item.innerHTML; //I think this is the problem but couldn't figure out how else to do it.
item.addEventListener("mouseover", () => popImage(name));
});
.keyword {
text-decoration: underline;
}
<div id="text">
Hovering over the word <span class="keyword">horse</span> should populate a separate div with images of horses, while hovering over <span class="keyword">dog</span> should populate it with images of dogs.
</div>
you can do that, by using event in popImage
const popImage = (e) => {
console.log(e.target.innerHTML);
}
const list = document.querySelectorAll(".keyword");
list.forEach((item) => {
item.addEventListener("mouseover", popImage);
});
.keyword {
font-weight: bold;
}
<div id="text">
Hovering over the word <span class="keyword">horse</span> should populate a separate div with images of horses, while hovering over <span class="keyword">dog</span> should populate it with images of dogs.
</div>

remove styles from all nodelist element and add only to clicked element Vanilla JS

I have multiple divs that when clicked adds a border and scales them up a little. I am looping through all elements using foreach and on click i remove every element's border and scale property except the clicked element, to which i add a border and scale.
My code is completely logical and is supposed to work but for some reason i cant seem to grasp, it only applies the styles to clicked elements but not removing from the rest of the elements (like my code says it should).
JS
document.querySelectorAll('.projcolorpick div').forEach(el => {
el.onclick = (e) => {
el.style.border = "none"
el.style.transform = "scale(1)"
e.target.style.border = "2px solid #fff"
e.target.style.transform = "scale(1.2)"
projcolor = e.target.style.background
}
})
}
give something like this a try... each element needs an id attribute for this to work (the filter part - if there is a unique attribute...)
const list = Array.from(document.querySelectorAll('.projcolorpick div'));
list.forEach(el => {
el.addEventListener('click', (e) => {
//code that affects the element you click on
el.style.border = "2px solid #fff"
el.style.transform = "scale(1.2)"
projcolor = e.target.style.background;
list.filter(x=>x.id!=el.id).forEach(otherEl=>{
//code that affects the other elements you didn't click on
otherEl.style.border = "none"
otherEl.style.transform = "scale(1)"
});
});
});
```
edit:
fixed some typos.
forEach only applies to Arrays unless you configure it otherwise.
querySelectorAll does not return arrays, but array-like objects (NodeLists)
To allow looping over NodeLists, add the following code:
if (window.NodeList && !NodeList.prototype.forEach) {
NodeList.prototype.forEach = Array.prototype.forEach;
}
var nL = document.querySelectorAll('*');
console.log(nL instanceof NodeList); // true
You don't really need an id attribute on each div and I would advocate using class-assignments instead of changing their individual attributes. You can compare the actual DOM elements with each other like c==ev.target, as you can see in my code below:
// === populate the page first ... ============================= START =
const cont=document.getElementById('container');
cont.innerHTML=
[...Array(3)].map(cp=>'<div class="projcolorpick">'+
[...Array(8)].map(d=>{
let hsl= "hsl("+Math.floor(Math.random()*360)+",100%,80%)";
return ' <div style="background-color:'+hsl+'">'+hsl+'</div>'}).join('\n')
+'</div>').join('\n');
// === populate the page first ... =============================== END =
// now, do the action:
cont.onclick=ev=>{
if ( ev.target.parentNode.classList.contains('projcolorpick')
&& ev.target.tagName=='DIV'){
[...ev.target.parentNode.children].forEach(c=>c.classList.toggle('selected',c==ev.target));
ev.target.parentNode.style.backgroundColor=ev.target.textContent;
}
}
.projcolorpick {border: 2px solid #888}
.selected {border: 2px solid #fff; transform:scale(1.2);}
div {margin:6px; padding:4px}
.projcolorpick div {width:200px; height:20px}
<div id="container"></div>
The action happens here:
cont.onclick=ev=>{
if ( ev.target.parentNode.classList.contains('projcolorpick')
&& ev.target.tagName=='DIV'){
[...ev.target.parentNode.children].forEach(c=>c.classList.toggle('selected',c==ev.target));
ev.target.parentNode.style.backgroundColor=ev.target.textContent;
}
}
I use a delegated event-attachment to the parent .container div. The first if statements makes sure that only clicks on .projcolorpick>div elements are processed.
If you want to include more than one generation between them you need to use something like ev.target.closest('.projcolorpick') instead ...
Now, inside the if block two things happen:
Using toggle() on all DOM elements in ev.target.parentNode.children the class "selected" is either
assigned or
removed.
The text found in the clicked div is applied as background-color to the parent .projcolorpick container.

Add id to children and store them as array to list

I have different buttons without id like this:
<div id="buttons">
<button data-note="n1">One</button>
<button data-note="n2">Two</button>
</div>
I cannot access them by .button since I have many, like 20 or so. Can I access them by their data-note attribute? querySelector selects the first one, second or all of them. Can I specifically select them, maybe put them an id?
What I actually want is to store the data-note as an Array, so have to convert that data into arrays as list to add to the HTML.
I supposed it is also possible without the id?
I might have to create for each button a function that by clicking them it stores that information as array into a list that I can access later if called, but I don't know how to select these children. If possible not by nodelist numbers, but by data, or if by nodelist then.
It might be a problem depending on the browser as they count 0,1,2 or 1,3,5... Is it possible to use getElementById and select a specific number of child?
To addEventListener I need to select each one of the buttons.
If you just want the notes data in a unordered list, then you can do the following:
// Get all the buttons
let buttons = document.getElementsByTagName("button");
// Get the "clear" button as it has a special function
let clear = buttons[buttons.length - 1];
// Create an unordered list
let ul = document.createElement("ul");
// Iterate through the buttons
for (let button of buttons) {
// Add the event listener to each button
button.addEventListener("click", (e) => {
// Clear the unordered list if the clear button was pressed
if (e.target === clear) {
ul.innerHTML = "";
return;
}
// For each button create a list item
let li = document.createElement("li");
// Set it's innerText attribute to be the data-note
li.innerText = `${button.dataset.note} - ${e.target.style.backgroundColor}`;
// Append it to the unordered list
ul.append(li);
});
}
// Append the list to the body
document.body.append(ul);
button {
border: none;
color: white;
padding: 10px 20px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 14px;
margin: 4px 2px;
cursor: pointer;
}
<h2>Buttons Game</h2>
<p>Click the buttons to add them to the list, press clear to reset</p>
<button data-note="n1" style="background-color: red;">red</button>
<button data-note="n2" style="background-color: green;">green</button>
<button data-note="n3" style="background-color: orange;">orange</button>
<button data-note="n4" style="background-color: blue;">blue</button>
<button id="clear" style="color: black; border: 1px solid black;">clear</button>
Sample Output:
Steps:
1) Use getElementsByTagName to store all the buttons into an array
2) Use a for loop to loop through the array
3) Use setAttribute to set the ids for all the buttons
4) Use the allButtons array as you wish since it contains all the buttons already, with their ids set.
Code:
var allButtons = document.getElementsByTagName("button");
for (var i = 0; i < allButtons.length; i++) {
allButtons[i].setAttribute("id", "button-" + [i])
}
console.log(allButtons);
<div id="buttons">
<button data-note="n1">One</button>
<button data-note="n2">Two</button>
</div>
This can be done easily with
var myNodes = document.querySelectorAll("button[data-note]");
This will assign to myNodes all buttons that contain data-note.
One of the properties of the array created's elements is "attributes" so you can search them easily with .find
Note: I see that you are actually trying to record the clicks, in order, like a Simon game. In that case you should have a hidden div on the page to store the data in, have every click call your event and that event gets the relevant control's data-note from the passed in e object to the function. You can assign as many click events as necessary to the control and they will execute in reverse order, last one added runs first.

Categories

Resources