How do I apply the same logic to multiple repeated elements?

Viewed 136

I am trying to add the same drop down functionality to an amount n of "cards". How could I do this in javascript without having to create new code for each card? This is my current code that I have tried, but even if I got this working it would only work for the first card: (working example below)

my html:

<div class="ideanode">
                        <div class="ideanodeheader">Need</div>
                        <div class="content">

                            <div class="title">
                                <h3 contenteditable="True" onclick='this.focus();'>Title</h3>
                            </div>
                            <i class="fas fa-sort-down title-arrow"></i>
                            <div class="maintext">
                                <textarea placeholder="Text" class="maintextinput"></textarea>
                            </div>
                            <i class="fas fa-sort-down maintxt-arrow"></i>
                            <div class="comments">
                                <textarea placeholder="Comments" class="commentsinput"></textarea>
                            </div>
                        </div>
                </div>

js:

var maintxt = document.querySelector(".maintext");
var title = document.querySelector(".title");
var titleArrow = document.querySelector(".title-arrow");
var mainArrow = document.querySelector(".maintxt-arrow");
var comments = document.querySelector(".comments");
var mainArrows = document.querySelectorAll(".maintxt-arrow")

titleArrow.addEventListener('click', function() {
    maintxt.classList.toggle("hidden");
    mainArrow.classList.toggle("hidden");
    if (comments.classList.contains("hidden")){
        ;
    } else {
    comments.classList.toggle("hidden");    
    };
});

mainArrow.addEventListener("click", function() {
    comments.classList.toggle("hidden");
});

Example: https://codepen.io/ricodon1000/pen/Baoxwed I would like for the dropdown arrows to trigger the boxes below them, the upper arrow should trigger only the box below it on, but if both boxes are open both should be closed. The arrow that is below should only trigger the box directly below it on and off (display: none on and off).

3 Answers

Do the same for each node, and limit your querySelector to the group

const ideanodes = [...document.querySelectorAll('.ideanode')];

ideanodes.forEach(ideanode => {

  const maintxt = ideanode.querySelector(".maintext");
  const title = ideanode.querySelector(".title");
  const titleArrow = ideanode.querySelector(".title-arrow");
  const mainArrow = ideanode.querySelector(".maintxt-arrow");
  const comments = ideanode.querySelector(".comments");

  titleArrow.addEventListener('click', function() {
    maintxt.classList.toggle("hidden");
    mainArrow.classList.toggle("hidden");
    if (comments.classList.contains("hidden")) {;
    } else {
      comments.classList.toggle("hidden");
    };
  });

  mainArrow.addEventListener("click", function() {
    comments.classList.toggle("hidden");
  });
});

You have a couple of options here, but the thing they have in common is that when you get a click on a .title-arrow element, you know which .title-arrow was clicked because it's the value of this (and event.currentTarget) in the event callback. From there, you can find the other elements it relates to using various DOM properties and methods, such as closest or the element version of querySelector.

So you could loop through all the .title-arrow elements and hook click on them, and then work it out from there. But I think you probably want to use event delegation instead: Hook click once, on the container for the various .ideanode elements, and then use event.target to figure out what to do.

I assume all those .ideanode elements are in a container, e.g. something like:

<div class="container">
    <div class="ideanode">
            <div class="ideanodeheader">Need</div>
            <div class="content">

                <div class="title">
                    <h3 contenteditable="True" onclick='this.focus();'>Title</h3>
                </div>
                <i class="fas fa-sort-down title-arrow">v</i>
                <div class="maintext">
                    <textarea placeholder="Text" class="maintextinput"></textarea>
                </div>
                <i class="fas fa-sort-down maintxt-arrow">v</i>
                <div class="comments">
                    <textarea placeholder="Comments" class="commentsinput"></textarea>
                </div>
            </div>
    </div>
    <div class="ideanode">
            <div class="ideanodeheader">Need</div>
            <div class="content">

                <div class="title">
                    <h3 contenteditable="True" onclick='this.focus();'>Title</h3>
                </div>
                <i class="fas fa-sort-down title-arrow">v</i>
                <div class="maintext">
                    <textarea placeholder="Text" class="maintextinput"></textarea>
                </div>
                <i class="fas fa-sort-down maintxt-arrow">v</i>
                <div class="comments">
                    <textarea placeholder="Comments" class="commentsinput"></textarea>
                </div>
            </div>
    </div>
    <!-- ...and so on... -->
</div>

So you can do this (see comments):

// A single handler on the container
document.querySelector(".container").addEventListener("click", function(event) {
    // Find the arrow that was clicked and the .ideanode it's in, if any
    const arrow = event.target.closest(".title-arrow, .maintxt-arrow");
    const ideanode = arrow && arrow.closest(".ideanode");
    if (!ideanode || !this.contains(ideanode)) {
        // Click wasn't on a `.title-arrow` or a `.maintxt-arrow` within an `.ideanode`
        return;
    }
    if (arrow.matches(".title-arrow")) {
        // It was a .title-arrow
        titleArrowClick.call(arrow, ideanode, event);
    } else {
        // It was a .maintxt-arrow
        mainArrowClick.call(arrow, ideanode, event);
    }
});

function titleArrowClick(ideanode, event) {
    // Use `querySelector` to look for elements within `.ideanode`
    ideanode.querySelector(".maintext").classList.toggle("hidden");
    ideanode.querySelector(".maintxt-arrow").classList.toggle("hidden");
    const comments = ideanode.querySelector(".comments");
    // (Couldn't the following be replaced with `comments.classList.add("hidden");` ?)
    if (comments.classList.contains("hidden")){
        ;
    } else {
        comments.classList.toggle("hidden");    
    };
}

function mainArrowClick(ideanode, event) {
    ideanode.querySelector(".comments").classList.toggle("hidden");
}

Live Example:

// A single handler on the container
document.querySelector(".container").addEventListener("click", function(event) {
    // Find the arrow that was clicked and the .ideanode it's in, if any
    const arrow = event.target.closest(".title-arrow, .maintxt-arrow");
    const ideanode = arrow && arrow.closest(".ideanode");
    if (!ideanode || !this.contains(ideanode)) {
        // Click wasn't on a `.title-arrow` or a `.maintxt-arrow` within an `.ideanode`
        return;
    }
    if (arrow.matches(".title-arrow")) {
        // It was a .title-arrow
        titleArrowClick.call(arrow, ideanode, event);
    } else {
        // It was a .maintxt-arrow
        mainArrowClick.call(arrow, ideanode, event);
    }
});

function titleArrowClick(ideanode, event) {
    // Use `querySelector` to look for elements within `.ideanode`
    ideanode.querySelector(".maintext").classList.toggle("hidden");
    ideanode.querySelector(".maintxt-arrow").classList.toggle("hidden");
    const comments = ideanode.querySelector(".comments");
    // (Couldn't the following be replaced with `comments.classList.add("hidden");` ?)
    if (comments.classList.contains("hidden")){
        ;
    } else {
        comments.classList.toggle("hidden");    
    };
}

function mainArrowClick(ideanode, event) {
    ideanode.querySelector(".comments").classList.toggle("hidden");
}
.hidden {
    display: none;
}
<div class="container">
    <div class="ideanode">
            <div class="ideanodeheader">Need</div>
            <div class="content">

                <div class="title">
                    <h3 contenteditable="True" onclick='this.focus();'>Title</h3>
                </div>
                <i class="fas fa-sort-down title-arrow">v</i>
                <div class="maintext">
                    <textarea placeholder="Text" class="maintextinput"></textarea>
                </div>
                <i class="fas fa-sort-down maintxt-arrow">v</i>
                <div class="comments">
                    <textarea placeholder="Comments" class="commentsinput"></textarea>
                </div>
            </div>
    </div>
    <div class="ideanode">
            <div class="ideanodeheader">Need</div>
            <div class="content">

                <div class="title">
                    <h3 contenteditable="True" onclick='this.focus();'>Title</h3>
                </div>
                <i class="fas fa-sort-down title-arrow">v</i>
                <div class="maintext">
                    <textarea placeholder="Text" class="maintextinput"></textarea>
                </div>
                <i class="fas fa-sort-down maintxt-arrow">v</i>
                <div class="comments">
                    <textarea placeholder="Comments" class="commentsinput"></textarea>
                </div>
            </div>
    </div>
    <!-- ...and so on... -->
</div>

Here's a suggestion, out of the many possibilities to tackle such a thing:

You start with the most basic array of classnames references and build you way into an Object with keys & values which values represent DOM nodes.

For the events part, you should very much use event-delegation so you won't need to bind an event for each and every card. Just make sure they have the same parent node, and bind your events there.

// preparation
var elms = ['maintext', 'title-arrow', 'maintxt-arrow'].reduce((acc, name) => {
  acc[name] = document.querySelector(`.${name}`)
  return acc
  }, {}
)

// iterating each "variable"
for( let elm in elms )
  console.log(elm)
<div class='maintext'></div>
<div class='title-arrow'></div>
<div class='maintxt-arrow'></div>

Related