On this coding exercise, tt took me a while to figure out how to reference the class that gets hidden here, and now that it's working, it would be a shame to break it, however, it was my hope to refactor this thing so the reference to the class that needs to be hidden is cleaner.
As you can see, doubling up on nextElementSibling to traverse the tree costs in hard coding the solution for each adaptive reuse. My hope was to simply change the classes queried. To do this, at first, it looked like using this.querySelector('* +') might be the answer, but after many attempts (maybe not the right ones b/c it still might work unless you tell me otherwise) it failed.
The following does work:
let el = this.parentNode.firstChild
el = el.nextElementSibling
el = el.nextElementSibling
So, how can the class .acc-body be referenced in a simpler way given the HTML outline it acts on?
And, since the approach to event handing put into force here is decidedly non-standard, and probably not recommended, is that reason enough to avoid using this in production?
<div class="acc">
<div class="acc-item">
<!-- <div class="acc-title"> -->
<h1 class="acc-title" id="btn_hide">Click to hide the Body</h1>
<!-- </div> -->
<div class="acc-body hideable">
<p id="head1">Why innerHTML is so great...</p>
</div>
</div>
<div class="acc-item">
<h1 class="acc-title" id="other_btn_hide_xxxx">Click to hide the Body</h1>
<div class="acc-body hideable">
<p id="head2">Why innerHTML is so great...</p>
</div>
</div>
</div>
Implemented as an IIFE; recommend it be placed in its own .js file and referenced from a script tag in the HTML document.
(function() {
console.log('ready to assign event handler to chosen nodelist of elements')
// returned as a nodelist
all = document.querySelectorAll(".acc-item .acc-title")
console.log(all);
// document[selectionMethod(element)]
// element.selectionMethod.classList
console.log('setAttribute \'clickable\' received on: -----------');
all.forEach ( (j) => {
console.log(j);
// j.setAttribute('onclick', 'showhide(this)')
j.setAttribute('clickable', 'true')
j.classList.add("clickable")
})
console.log('listen for onclick on: ----------');
all.forEach( (j) => {
console.log(j);
j.onclick = function () {
// alert(this);
// let el = this.querySelector('* +')
let el = this.parentNode.firstChild
el = el.nextElementSibling
el = el.nextElementSibling
console.log(el);
if (el.hidden){
el.hidden = false
} else {
el.hidden = true
}
}
})
})()
Related
As a novice Javascript programmer, I'd like to create an html document presenting a feature very similar to the "reveal spoiler" used extensively in the Stack Exchange sites.
My document therefore has a few <div> elements, each of which has an onClick event listner which, when clicked, should reveal a hiddent text.
I already know that this can be accomplished, e.g., by
<div onclick="this.innerHTML='Revealed text'"> Click to reveal </div>
However, I would like the text to be revealed to be initially stored in a variable, say txt, which will be used when the element is clicked, as in:
<div onclick="this.innerHTML=txt"> Click to reveal </div>
Since there will be many such <div> elements, I certainly cannot store the text to be revealed in a global variable. My question is then:
Can I declare a variable that is local to a specific html element?
Yes you can. HTML elements are essentially just Javascript Objects with properties/keys and values. So you could add a key and a value to an HTML element object.
But you have to add it to the dataset object that sits inside the element, like this:
element.dataset.txt = 'This is a value' // Just like a JS object
A working example of what you want could look like this:
function addVariable() {
const myElement = document.querySelector('div')
myElement.dataset.txt = 'This is the extended data'
}
function showExtendedText(event) {
const currentElement = event.currentTarget
currentElement.innerHTML += currentElement.dataset.txt
}
addVariable() // Calling this one immediately to add variables on initial load
<div onclick="showExtendedText(event)">Click to see more </div>
Or you could do it by adding the variable as a data-txt attribute right onto the element itself, in which case you don't even need the addVariable() function:
function showExtendedText(event) {
const currentElement = event.currentTarget
currentElement.innerHTML += currentElement.dataset.txt
}
<div onclick="showExtendedText(event)" data-txt="This is the extended data">Click to see more </div>
To access the data/variable for the specific element that you clicked on, you have to pass the event object as a function paramater. This event object is given to you automatically by the click event (or any other event).
Elements have attributes, so you can put the information into an attribute. Custom attributes should usually be data attributes. On click, check if a parent element has one of the attributes you're interested in, and if so, toggle that parent.
document.addEventListener('click', (e) => {
const parent = e.target.closest('[data-spoiler]');
if (!parent) return;
const currentMarkup = parent.innerHTML;
parent.innerHTML = parent.dataset.spoiler;
parent.dataset.spoiler = currentMarkup;
});
<div data-spoiler="foo">text 1</div>
<div data-spoiler="bar">text 2</div>
That's the closest you'll get to "a variable that is local to a specific html element". To define the text completely in the JavaScript instead, one option is to use an array, then look up the clicked index of the spoiler element in the array.
const spoilerTexts = ['foo', 'bar'];
const spoilerTags = [...document.querySelectorAll('.spoiler')];
document.addEventListener('click', (e) => {
const parent = e.target.closest('.spoiler');
if (!parent) return;
const currentMarkup = parent.innerHTML;
const index = spoilerTags.indexOf(parent);
parent.innerHTML = spoilerTexts[index];
spoilerTexts[index] = currentMarkup;
});
<div class="spoiler">text 1</div>
<div class="spoiler">text 2</div>
There are also libraries that allow for that sort of thing, by associating each element with a component (a JavaScript function/object used by the library) and somehow sending a variable to that component.
// for example, with React
const SpoilerElement = ({ originalText, spoilerText }) => {
const [spoilerShown, setSpoilerShown] = React.useState(false);
return (
<div onClick={() => setSpoilerShown(!spoilerShown)}>
{ spoilerShown ? spoilerText : originalText }
</div>
);
};
const App = () => (
<div>
<SpoilerElement originalText="text 1" spoilerText="foo" />
<SpoilerElement originalText="text 2" spoilerText="bar" />
</div>
)
ReactDOM.createRoot(document.querySelector('.react')).render(<App />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div class='react'></div>
Thanks everybody for your answers, which helped immensely! However, as a minimalist, I took all that I learned from you and came up with what I believe is the simplest possible code achieving my goal:
<div spoiler = "foo" onclick="this.innerHTML=this.getAttribute('spoiler')">
Click for spoiler
</div>
<div spoiler = "bar" onclick="this.innerHTML=this.getAttribute('spoiler')">
Click for spoiler
</div>
I want text parts to appear, and disappear, on click.
Before the first click you can only see the banner, and no verses yet; on the first click the first verse appears, on second click the second verse appears in place of the first, and so on.
I am trying to achieve this with hiding the elements, placing them in an array, and let them display when the number of times the function gets called fits the index of the verse.
I am new to JavaScript, and don't understand the exceptions thrown. If i try to inspect my code online, I get this Exception when O try to call the function by clicking on the website:
Uncaught TypeError: Cannot read properties of null (reading 'style')
This is my code so far:
const text = document.querySelector(".banner")
document.addEventListener('click', myFunction);
const verse1 = document.querySelector(".verse1")
const verse2 = document.querySelector(".verse2")
const verse3 = document.querySelector(".verse3")
const verse4 = document.querySelector(".verse4")
const verse5 = document.querySelector(".verse5")
const verses = [verse1, verse2, verse3, verse4, verse5]
let versesLength = verses.length;
function myFunction() {
for (let i = 0; i < versesLength; i++) {
text.innerHTML = verses[i].style.display = 'block';
}
}
<div class="banner">
<script src="main.js"></script>
<img src="files/SomeLogo.jpg" alt="We are still building on our Website:-)">
</div>
<div id="verses">
<div class="verse1" style="display: none">Lorem Ipsum</div>
<div class="verse2" style="display: none">Lorem Ipsum2</div>
<div class="verse3" style="display: none">Lorem Ipsum3</div>
<div class="verse4" style="display: none">Lorem Ipsum4</div>
<div class="verse5" style="display: none">Lorem Ipsum5</div>
</div>
I am stuck, and clicked through similar questions for the last hours. Thanks in advance for any help
without changing anything in the HTML, you can do something like this in javascript
const text = document.querySelector(".banner")
document.addEventListener('click', myFunction);
let verses = document.querySelector("#verses").children
let count = 0
function myFunction() {
Array.from(verses).forEach(el=> el.style.display="none")
if(count < verses.length){
verses[count].style.display = 'block'
count ++
if(count===verses.length) count =0
}
}
You can remove the need for an array by giving all the verse elements the same class: verse. We can grab them with querySelectorAll.
Add a data attribute to each verse to identify them.
In order to limit the number of global variables we can use a closure - in the addEventListener we call the handleClick function which initialises the count, and then returns a function that will be assigned to the listener. This is a closure. It maintains a copy of its outer lexical environment (ie variables) that it can use when it's returned.
// Cache the elements with the verse class
const banner = document.querySelector('.banner');
const verses = document.querySelectorAll('.verse');
// Call `handleClick` and assign the function it
// returns to the listener
document.addEventListener('click', handleClick());
function handleClick() {
// Initialise `count`
let count = 1;
// Return a function that maintains a
// copy of `count`
return function () {
// If the count is 5 or less
if (count < verses.length + 1) {
// Remove the banner
if (count === 1) banner.remove();
// Remove the previous verse
if (count > 1) {
const selector = `[data-id="${count - 1}"]`;
const verse = document.querySelector(selector);
verse.classList.remove('show');
}
// Get the new verse
const selector = `[data-id="${count}"]`;
const verse = document.querySelector(selector);
// And show it
verse.classList.add('show');
// Increase the count
++count;
}
}
}
.verse { display: none; }
.show { display: block; margin-top: 1em; padding: 0.5em; border: 1px solid #787878; }
[data-id="1"] { background-color: #efefef; }
[data-id="2"] { background-color: #dfdfdf; }
[data-id="3"] { background-color: #cfcfcf; }
[data-id="4"] { background-color: #bfbfbf; }
[data-id="5"] { background-color: #afafaf; }
<div class="banner">
<img src="https://dummyimage.com/400x75/404082/ffffff&text=We+are+still+building+our+website" alt="We are still building on our Website:-)">
</div>
<div>
<div data-id="1" class="verse">Lorem Ipsum 1</div>
<div data-id="2" class="verse">Lorem Ipsum 2</div>
<div data-id="3" class="verse">Lorem Ipsum 3</div>
<div data-id="4" class="verse">Lorem Ipsum 4</div>
<div data-id="5" class="verse">Lorem Ipsum 5</div>
</div>
Additional documentation
Template/string literals
classList
This should make it:
const verses = document.querySelectorAll('.verse');
const banner = document.querySelector('.banner');
const length = verses.length;
let counter = 0;
document.onclick = () => {
if (counter === 0) banner.classList.add('hide');
if (counter >= length) return;
verses[counter].classList.add('show');
if (verses[counter - 1]) verses[counter - 1].classList.remove('show');
counter++;
};
body {
background: orange;
}
.hide {
display: none !important;
}
.show {
display: block !important;
}
<html>
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" type="text/css" href="styles.css" />
</head>
<body>
<div class="banner">
<img
src="files/SomeLogo.jpg"
alt="We are still building on our Website:-)"
/>
</div>
<div id="verses">
<div class="verse1 verse hide">Lorem Ipsum</div>
<div class="verse2 verse hide">Lorem Ipsum2</div>
<div class="verse3 verse hide">Lorem Ipsum3</div>
<div class="verse4 verse hide">Lorem Ipsum4</div>
<div class="verse5 verse hide">Lorem Ipsum5</div>
</div>
<script src="main.js"></script>
</body>
</html>
Fixing the error
The "Cannot read properties of null" error occurs because you try to access the properties of null. Your array holds nulls because you queried for the elements before the browser has inserted them into its DOM.
The browser parses the HTML the same way you would read it: From left to right, and top to bottom.
If the browser encounters a regular <script> element, it halts parsing and first executes the JavaScript. Naturally, some elements may not yet be available in the DOM.
There are multiple ways to defer script execution:
Add attribute defer to <script>: Will execute once the DOM is fully built.
Add attribute type="module" to <script>: Similar to defer, but will also make your code be treated as a JS module. This will also make your code run in strict mode.
Use JS event DOMContentLoaded: Similar to defer, but encapsulated in your JS-file.
Use JS event load: Similar to DOMContentLoaded, but will additionally wait until all resources (e.g. images, videos) have loaded. Prefer DOMContentLoaded if applicable.
Move <script> to the bottom of the HTML: Effectively like defer. Scripts with defer will still load after scripts at the bottom.
The simplest solution would be to use defer, as with it you wouldn't have to change your JS code:
<script src="main.js" defer></script>
By the way: Don't be fooled by the StackOverflow snippets; when using the on-site snippets, the <script> for the code is moved to the bottom of the HTML!
The feature!
Variable lifetimes
Variables in JS only persist for as long as they are used.
Using a variable that is declared outside a function will create a closure around your function and variable. That means, that variable will persist for as long as the function exists that uses it:
let someVariable = 0;
function someFunction() {
// `someVariable` is used, so it will persist across calls!
return someVariable;
}
This means, to have the variable for "keeping track of what verse to show" persist, it has to be declared outside your function.
Show and hide!
By calculating the previous verse's index with the index of the next-to-show verse, we only have to keep one counter. With two counters, they might get out of sync if we don't handle them correctly.
let nextToShow = 0;
function showNextVerse() {
const previousIndex = nextToShow - 1;
// ...
++nextToShow; // Increase counter for next call
}
In our case, the user (or rather, their clicks) will play the role of the loop. They will cause our click handler (the function) to run occasionally, at which point we have to swap the verses.
Swapping the verses can be done in many ways, but we'll stick to your "inline style" way: (Final code)
document.addEventListener("click", showNextVerse);
const banner = document.querySelector(".banner");
const verses = document.getElementById("verses").children; // More on this later
let nextToShow = 0;
function showNextVerse() {
const previousIndex = nextToShow - 1;
// On every call, hide the banner
banner.style.display = "none"; // Use `.style` instead of `.innerHTML` to preserve its HTML!
// Hide previous if valid index
if (previousIndex >= 0 && previousIndex < verses.length) {
verses[previousIndex].style.display = "none";
}
// Show next if valid index
if (nextToShow >= 0 && nextToShow < verses.length) {
verses[nextToShow].style.display = "block";
}
++nextToShow;
}
<div class="banner">
<script src="main.js" defer></script>
<img alt="We are still building on our Website:-)">
</div>
<div id="verses">
<div style="display:none">Lorem Ipsum1</div>
<div style="display:none">Lorem Ipsum2</div>
<div style="display:none">Lorem Ipsum3</div>
<div style="display:none">Lorem Ipsum4</div>
<div style="display:none">Lorem Ipsum5</div>
</div>
Improvements?!
There is no need for the variable versesLength; you can directly replace each of its occurences with verses.length. It doesn't improve on the original name, and is one more potential source for bugs if not synchronized with the original variable.
Correctly use class and id
Currently, your verses use class as if it was id; they each use a different class. This is not wrong, but semantically I would use id for this purpose.
To use the class attribute effectively, you should give each verse the class verse. This way, you can select them more easily via JS (see next section).
Easier getting of elements
As with everything in the coding world, there are many solutions to a problem. You solved getting the elements in a rather tedious way, but there are alternatives: (Non-exhaustive list)
Use document.querySelectorAll().
Rename verses to use same class, and use document.getElementsByClassName().
Use Element.children.
You may have already noticed how I get all the verses. In fact, verses (in the final code) doesn't even reference an array, but an HTMLCollection. It is very similar to an array, with the exception of it updating live to changes:
const elementsWrapper = document.getElementById("elements");
const collection = elementsWrapper.children;
const array = Array.from(elementsWrapper.children);
document.getElementById("bt-add").addEventListener("click", function() {
const newElement = document.createElement("div");
newElement.textContent = "Added later";
elementsWrapper.appendChild(newElement);
});
document.getElementById("bt-log").addEventListener("click", function() {
console.log("collection.length:", collection.length);
console.log("array.length:", array.length);
});
<button id="bt-add">Add new element</button>
<button id="bt-log">Log <code>.length</code></button>
<p>
Try logging first! Then try to add elements, and log again! See the difference?
</p>
<div>Elements:</div>
<div id="elements">
<div>Initially existent</div>
<div>Initially existent</div>
</div>
Alternative way of hiding
Here are ways of hiding the elements:
Use inline styling (this is what you did!).
Use CSS classes.
Use the HTML attribute hidden.
For small style changes I too would use inline styling. But for only hiding elements I would use the hidden attribute.
Also, there are multiple CSS ways of hiding elements:
Using display: none: Will hide the element as if it doesn't exist.
Using opacity: 0: Will hide the element by making it invisible; it still takes up space, and should still be part of the accessibility tree (opinionated).
Moving it off-site with position: fixed and top, left, etc. properties: (Please don't.)
Will move the element off-site, visually. It will still be part of the accessibility tree, and will only work for languages with the intended writing direction (e.g. it won't work for right-to-left languages).
Setting width, height, margin, padding and border to 0: Will hide the element only visually; it will still be part of the accessibility tree, and will stop margin collapse. Screen-reader only classes use this for non-visual elements, very useful.
currently I am making my own project and I got stuck in :/
I wanted to add an event and give some function on div which is made with a button("create"). However, the console returned "Uncaught TypeError: Cannot read property 'addEventListener' of null" this. I think it is because DIV(.c) is made after JS run.
But I need to add an eventlistener and function on DIV(.c) to accomplish what I want.
So, is there any way to bind a div which is created later and add an
const createButton = document.querySelector(".create");
const paperBook = document.querySelector(".b");
createButton.addEventListener("click", createWriting);
function createWriting(e) {
e.preventDefault();
const writing = document.createElement("div");
writing.classList.add("c");
writing.innerHTML = `All work No rest make jack a dull boy`;
paperBook.appendChild(writing);
}
const myProblem = document.querySelector(".c");
myProblem.addEventListener("click", randomFunction);
function randomFunction(e) {
e.preventDefault();
console.log(e)
}
<div class="a">
<button class="create">create</button>
<div class="b"></div>
</div>
event on it?
Below is my code. But I summarized it to simplify for you if you need a whole code just ask me, please :)
thx!
I think the problem is that this part of code
const myProblem = document.querySelector(".c");
It is executed before you create your .c element, so the new elements would not be included on it. Try to add the event everytime a new element is created in the createWriting function
const createButton = document.querySelector(".create");
const paperBook = document.querySelector(".b");
createButton.addEventListener("click", createWriting);
function createWriting(e) {
e.preventDefault();
const writing = document.createElement("div");
writing.classList.add("c");
writing.innerHTML = `All work No rest make jack a dull boy`;
paperBook.appendChild(writing);
writing.addEventListener("click", randomFunction);
}
function randomFunction(e) {
e.preventDefault();
console.log(e)
}
<div class="a">
<button class="create">create</button>
<div class="b"></div>
</div>
I have DOM elements as shown below. I want to sort it on the basis of href attribute.
This is what I have tried in JS but more need to be done.
document.addEventListener("DOMContentLoaded", function () {
let elems = Array.from(document.querySelectorAll(".house-senate a"));
elems.sort((a, b) => a.textContent.localeCompare(b.textContent));
});
Problem Statement:
I am wondering what JS code I need to add so that it sorts everything on the basis of href attributes.
You're close, but:
You need to actually move them in the DOM.
You're potentially sorting ones that aren't in the same parent (though they all are in your example HTML).
blex pointed out to me that you want to sort by the category in the href, not by the href itself. In your example, it comes to the same thing because the text prior to the category in all the hrefs is the same, but still, perhaps better to extract it.
This is blex's function for extracting it:
function getLinkCategory(a) {
const matches = a.href.match(/category=([a-z]+)/i);
return matches ? matches[1] : '';
}
Or if you want to be more rigorous about extracting that parameter from the query string, this collaborative answer originally by Code Spy shows how to do that.
See comments for more:
document.addEventListener("DOMContentLoaded", function () {
// Get the container
const container = document.querySelector(".house-senate");
// Get its immediate child `a` elements
const elems = [...container.children].filter(child => child.tagName === "A");
// Sort them
elems.sort((a, b) => getLinkCategory(a).localeCompare(getLinkCategory(b)));
// Add them back, which moves them
for (const el of elems) {
container.appendChild(el);
}
});
Live Example:
function getLinkCategory(a) {
const matches = a.href.match(/category=([a-z]+)/i);
return matches ? matches[1] : '';
}
document.addEventListener("DOMContentLoaded", function () {
// Get the container
const container = document.querySelector(".house-senate");
// Get its immediate child `a` elements
const elems = [...container.children].filter(child => child.tagName === "A");
// Sort them
elems.sort((a, b) => getLinkCategory(a).localeCompare(getLinkCategory(b)));
// Add them back, which moves them
for (const el of elems) {
container.appendChild(el);
}
});
<div class="house-senate widget widget-cpac-depth -horizontal">
<h1 class="widget__title">Committees</h1>
<a href="/en/?s=&category=BOIE">
<div class="committee">
<div class="color-green">BOIE</div>
<p>Board of Internal Economy</p>
</div>
</a>
<a href="/en/?s=&category=CACN">
<div class="committee">
<div class="color-green">CACN</div>
<p>Canada-China Relations</p>
</div>
</a>
<a href="/en/?s=&category=CHPC">
<div class="committee">
<div class="color-green">CHPC</div>
<p>Canadian Heritage</p>
</div>
</a>
<a href="/en/?s=&category=CIIT">
<div class="committee">
<div class="color-green">CIIT</div>
<p>International Trade</p>
</div>
</a>
</div>
If you need to add more sorting criteria (per your comment under the question), just add them in the sort callback; this question has answers showing how to sort an array of objects on multiple criteria.
I've assumed above that there aren't hundreds of these links. If there are, and if you see a performance problem with the above, you can remove the container from the DOM before moving the links around within it, then put it back:
Live Example:
function getLinkCategory(a) {
const matches = a.href.match(/category=([a-z]+)/i);
return matches ? matches[1] : '';
}
document.addEventListener("DOMContentLoaded", function () {
// Get the container
const container = document.querySelector(".house-senate");
// Remember its parent and following sibling and remove it
const parent = container.parentNode;
const sibling = container.nextSibling;
parent.removeChild(container);
// Get its immediate child `a` elements
const elems = [...container.children].filter(child => child.tagName === "A");
// Sort them
elems.sort((a, b) => getLinkCategory(a).localeCompare(getLinkCategory(b)));
// Add them back, which moves them
for (const el of elems) {
container.appendChild(el);
}
// Put the container back -- note this works even if the
// container was the last child in the parent
// and `sibling` is `null`.
parent.insertBefore(container, sibling);
});
<div>This is before the <code>div</code> with the links in it.</div>
<div class="house-senate widget widget-cpac-depth -horizontal">
<h1 class="widget__title">Committees</h1>
<a href="/en/?s=&category=BOIE">
<div class="committee">
<div class="color-green">BOIE</div>
<p>Board of Internal Economy</p>
</div>
</a>
<a href="/en/?s=&category=CACN">
<div class="committee">
<div class="color-green">CACN</div>
<p>Canada-China Relations</p>
</div>
</a>
<a href="/en/?s=&category=CHPC">
<div class="committee">
<div class="color-green">CHPC</div>
<p>Canadian Heritage</p>
</div>
</a>
<a href="/en/?s=&category=CIIT">
<div class="committee">
<div class="color-green">CIIT</div>
<p>International Trade</p>
</div>
</a>
</div>
<div>This is after the <code>div</code> with the links in it.</div>
Note: You're using modern language features, but the above relies on a modern browser feature (NodeList being iterable). If you're transpiling, it may not be that all the browsers you're targeting have the necessary feature, but for anything even vaguely modern, you can polyfill it; see my answer here for details.
I'm trying to add an event listener on some repeating innerHTML. I.E for every lot of HTML added by innerHTML, I'll also need to add a click event onto it.
To complicate things I'm also importing a data set from another JS file imported under the name data. As you can see in the code I need the data inside the event listener to be specific to the for loop iteration of the innerHTML so that when I fire the event listener I can see the correct, corresponding data.
This is my attempt:
JS:
import data from './data.js';
import img from './images.js';
export const lists = () => {
const main = document.getElementById('main');
main.innerHTML = `
<div class="main-container">
<div class="flex-between row border-bottom">
<div class="flex new-list">
<img class="create-img img-radius" src="${img.symbols[0]}" alt="Delete Bin">
<h3>New List</h3>
</div>
<div class="flex-between sections">
<h3 class="text-width flex-c">Items:</h3>
<h3 class="text-width flex-c">Reminders:</h3>
<h3 class="text-width flex-end">Created:</h3>
</div>
</div>
<div id="lists"></div>
</div>
`;
const lists = document.getElementById('lists');
for (let i = 0; i < data.lists.length; i++) {
let obj = eval(data.lists[i]);
let totalReminders = getTotalReminders(obj);
lists.innerHTML += `
<div class="flex-between row list">
<h4>${obj.name}</h4>
<div class="flex-between sections">
<h4 class="number-width flex-c">${obj.items.length}</h4>
<h4 class="number-width flex-c">${totalReminders}</h4>
<div class="text-width flex-end">
<h4 class="date">${obj.created}</h4>
<img class="img-radius" src="${img.symbols[3]}" alt="Delete Bin">
</div>
</div>
</div>
`;
const list = document.querySelector('.list');
list.addEventListener('click', () => { // click event
listNav.listNav(obj.name);
listSidebarL.listSidebarL();
listSidebarR.listSidebarR();
listMain.listMain(obj.items);
});
};
};
const getTotalReminders = passed => { // find total reminders
let total = 0;
for (let i = 0; i < passed.items.length; i++) {
total += passed.items[i].reminders;
};
return total;
};
At the moment ONLY the first iteration of innerHTML += has an event listener attached and when I click on it I see the data that should be corresponding the last iteration.
What am I doing wrong here?
You need to move the code that sets up the event handlers so that it is outside of your for loop and runs after that loop is finished. Then, instead of .querySelector(), which only returns the first matching element, you need .querySelectorAll() to return all matching elements. After that, you'll loop through all those elements and set up the handler.
You'll also need to change how your obj variable is declared so that it will be in scope outside of the for loop. Do this by declaring it just before the loop, but assigning it inside the loop:
let obj = null; // Now, obj is scoped so it can be accessed outside of the loop
for (let i = 0; i < data.lists.length; i++) {
obj = eval(data.lists[i]);
And, put the following just after the for loop finishes:
// Get all the .list elements into an Array
const list = Array.prototype.slice.call(document.querySelectorAll('.list'));
// Loop over the array and assign an event handler to each array item:
list.forEach(function(item){
item.addEventListener('click', () => {
listNav.listNav(obj.name);
listSidebarL.listSidebarL();
listSidebarR.listSidebarR();
listMain.listMain(obj.items);
});
});
With all this said, your approach here is really not very good. There is almost always another option than to use eval() for anything and using .innerHTML is usually something to avoid due to its security and performance implications. Using it in a loop is almost always a bad idea. You really should be using the DOM API to create new elements, configure them and inject them into the DOM. If you must use .innerHTML, then build up a string in your loop and after the loop, inject the string into the DOM via .innerHTML, just once.
One options is to look at event delegation/bubbling. The basic principle here is you add the event handler to a parent object, in this case <div id="lists"></div>. Then when the event is fired you query the target of that event to see if it matches your element.
Using this technique you don't have to re-bind event handlers when new items are added, particularly useful if the items are added by user interaction.
In your case it would look something like:
export const lists = () => {
const main = document.getElementById('main');
main.innerHTML = `
<div class="main-container">
<div class="flex-between row border-bottom">
<div class="flex new-list">
<img class="create-img img-radius" src="${img.symbols[0]}" alt="Delete Bin">
<h3>New List</h3>
</div>
<div class="flex-between sections">
<h3 class="text-width flex-c">Items:</h3>
<h3 class="text-width flex-c">Reminders:</h3>
<h3 class="text-width flex-end">Created:</h3>
</div>
</div>
<div id="lists"></div>
</div>
`;
const lists = document.getElementById('lists');
//Now that the parent element is added to the DOM
//Add the event handler
lists.addEventListener("click",function(e) {
// e.target was the clicked element
if (e.target && e.target.matches(".list")) {
listNav.listNav(obj.name);
listSidebarL.listSidebarL();
listSidebarR.listSidebarR();
listMain.listMain(obj.items);
}
//Add Items etc
});
NOTE Scots comments re eval and innerHTML apply equally to this answer.