Home > Back-end >  How to iterate with click event through Array of divs
How to iterate with click event through Array of divs

Time:04-10

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 >
  <script src="main.js"></script>
  <img src="files/SomeLogo.jpg" alt="We are still building on our Website:-)">
</div>

<div id="verses">
  <div  style="display: none">Lorem Ipsum</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>

I am stuck, and clicked through similar questions for the last hours. Thanks in advance for any help

CodePudding user response:

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

CodePudding user response:

  1. You can remove the need for an array by giving all the verse elements the same class: verse. We can grab them with querySelectorAll.

  2. Add a data attribute to each verse to identify them.

  3. 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 >
  <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" >Lorem Ipsum 1</div>
  <div data-id="2" >Lorem Ipsum 2</div>
  <div data-id="3" >Lorem Ipsum 3</div>
  <div data-id="4" >Lorem Ipsum 4</div>
  <div data-id="5" >Lorem Ipsum 5</div>
</div>

Additional documentation

CodePudding user response:

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 >
      <img
        src="files/SomeLogo.jpg"
        alt="We are still building on our Website:-)"
      />
    </div>
    <div id="verses">
      <div >Lorem Ipsum</div>
      <div >Lorem Ipsum2</div>
      <div >Lorem Ipsum3</div>
      <div >Lorem Ipsum4</div>
      <div >Lorem Ipsum5</div>
    </div>
    <script src="main.js"></script>
  </body>
</html>

CodePudding user response:

Fixing the error

The "Cannot read properties of null" error occurs because you try access the properties of null. Your array holds nulls because you tried to get 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 the duration of their context. (Closures will cause their surrounding scope to persist for as long as they live!)

To have a variable persist across calls to a function, it has to be declared outside of this function:

let someVariable = 0;
function aFunc() {
  // `someVariable` will persist across calls!
}

This means, the variable to keep track of what verse to show 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 >
  <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)

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:

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.
  • Related