2

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).

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • 1
    Add each item in array and iterate over it –  May 10 '20 at 08:38
  • Related: https://stackoverflow.com/questions/14442896/dynamic-variable-name-in-loop – T.J. Crowder May 10 '20 at 08:39
  • yes but how could I iterate over multiple items at once, because if I add the titleArrows in an array each one only has to work on its respective maintxt and comments boxes. The first title arrow on the first maintxt item the second one on the second and so on –  May 10 '20 at 08:40
  • 1
    I added the html! –  May 10 '20 at 08:48

3 Answers3

1

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");
  });
});
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
Gabriele Petrioli
  • 191,379
  • 34
  • 261
  • 317
  • 1
    FWIW, the `NodeList` from `querySelectorAll` has its own `forEach` now, which is at least as well supported as it being iterable (which `..` relies on -- if you wanted to avoid requiring iterability, use `Array.from` instead), possibly *slightly* more so. So probably best just to use it directly. (And if you need to polyfill it, I explain how to do that [here](https://stackoverflow.com/a/46057817/157247).) – T.J. Crowder May 10 '20 at 09:16
  • I find it interesting nobody comments on the empty `if` branch till now... – Icepickle May 10 '20 at 09:21
  • 1
    @Icepickle i assume it is irrelevant to the current problem and that is why it is not included – Gabriele Petrioli May 10 '20 at 11:04
1

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>
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • @Rico - No worries! I'd missed out a couple of `.classList`s in the code, fixed that doing the live example. One advantage to event delegation like this is that when you add further `.ideanode` elements, you don't have to worry about hooking them up, they just work. – T.J. Crowder May 10 '20 at 09:11
-1

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>
vsync
  • 118,978
  • 58
  • 307
  • 400