For a comment list I use the event delegation pattern after a recommendation from Stackoverflow colleagues (mplungjan, Michel). It works well and I am very enthusiastic about this pattern. But as I already suspected, there will be problems if the bound element (button) contains two child elements (span, span).
Since I want to get the CommentID from the target in the parent element of the child element, it only works in the cases when you click exactly between the spans inside the button. Actually a case for currentTarget but that doesn't work in this case because the tapped element is the whole comment list.
Question: What do I have to do to fix it?
const commentList = document.querySelector('.comment-list');
commentList.addEventListener('click', (ev) => {
console.log('1. clicked');
const getObjectId = () => {
return ev.target.parentNode.parentNode.getAttribute('data-comment-id');
}
if (! getObjectId()) return false;
if (ev.target.classList.contains('delete')) {
console.log('2. Delete action');
console.log('3. for relatedID', getObjectId());
}
if (ev.target.classList.contains('edit')) {
console.log('2. Edit action');
console.log('3. for relatedID', getObjectId());
}
if (ev.target.classList.contains('flag')) {
console.log('2. Flag action');
console.log('3. for relatedID', getObjectId());
}
});
.controller {
display: flex;
gap:20px;
}
.comment {
margin-bottom: 20px;
background: gray;
}
.controller button > span {
background: orange;
}
.controller button span:first-child {
margin-right: 10px;
}
<div class="comment-list">
<div class="comment">
<div class="content">lorem 1. Dont work! Nested button.</div>
<div class="controller" data-comment-id="1">
<div class="delete">
<button class="delete"><span>delete</span><span>ICON</span></button>
</div>
<div class="edit">
<button class="edit"><span>edit</span><span>ICON</span></button>
</div>
<div class="flag">
<button class="flag"><span>flag</span><span>ICON</span></button>
</div>
</div>
</div>
<div class="comment">
<div class="content">lorem 2. Work! </div>
<div class="controller" data-comment-id="2">
<div class="delete"><button class="delete">delete</button></div>
<div class="edit"><button class="edit">edit</button></div>
<div class="flag"><button class="flag">flag</button></div>
</div>
</div>
<div class="comment">
<div class="content">lorem 3. Work! </div>
<div class="controller" data-comment-id="3">
<div class="delete"><button class="delete">delete</button></div>
<div class="edit"><button class="edit">edit</button></div>
<div class="flag"><button class="flag">flag</button></div>
</div>
</div>
</div>
The problem is that you're using .parentNode.parentNode to get to the element with data-comment-id, but the number of parents changes when the target is nested inside additional <span> elements.
Don't hard-code the nesting levels, use .closest() to find the containing controller node.
const getObjectId = () => {
return ev.target.closest('.controller').getAttribute('data-comment-id');
}
Building on my last comment in the other question
const tgtButtonWhenSpansInsideButton = e.target.closest("button")
Cache the objects
the closest method will get the button itself even if no children
Make sure you get the class from the containing element of what you want to call a button
const commentList = document.querySelector('.comment-list');
const getObjectId = (tgt) => tgt.closest('.controller').dataset.commentId;
commentList.addEventListener('click', (ev) => {
const tgt = ev.target.closest("button")
const objectId = getObjectId(tgt);
if (!objectId) return;
console.log(objectId,"clicked")
if (tgt.classList.contains('delete')) {
console.log('2. Delete action');
console.log('3. for relatedID', objectId);
}
if (tgt.classList.contains('edit')) {
console.log('2. Edit action');
console.log('3. for relatedID', objectId);
}
if (tgt.classList.contains('flag')) {
console.log('2. Flag action');
console.log('3. for relatedID', objectId);
}
});
.controller {
display: flex;
gap: 20px;
}
.comment {
margin-bottom: 20px;
background: gray;
}
.controller button>span {
background: orange;
}
.controller button span:first-child {
margin-right: 10px;
}
<div class="comment-list">
<div class="comment">
<div class="content">lorem 1. Dont work! Nested button.</div>
<div class="controller" data-comment-id="1">
<div class="delete">
<button class="delete"><span>delete</span><span>ICON</span></button>
</div>
<div class="edit">
<button class="edit"><span>edit</span><span>ICON</span></button>
</div>
<div class="flag">
<button class="flag"><span>flag</span><span>ICON</span></button>
</div>
</div>
</div>
<div class="comment">
<div class="content">lorem 2. Work! </div>
<div class="controller" data-comment-id="2">
<div class="delete"><button class="delete">delete</button></div>
<div class="edit"><button class="edit">edit</button></div>
<div class="flag"><button class="flag">flag</button></div>
</div>
</div>
<div class="comment">
<div class="content">lorem 3. Work! </div>
<div class="controller" data-comment-id="3">
<div class="delete"><button class="delete">delete</button></div>
<div class="edit"><button class="edit">edit</button></div>
<div class="flag"><button class="flag">flag</button></div>
</div>
</div>
</div>
In this case I would "traverse" the DOM up if it wasn't a button that was clicked, something like this
const commentList = document.querySelector('.comment-list');
commentList.addEventListener('click', (ev) => {
console.log('1. clicked', ev.target.tagName);
let target = ev.target
if (target.tagName === "SPAN") {
target = target.parentElement
}
const commentId = target.parentElement.parentElement.getAttribute('data-comment-id');
if (!commentId) return false;
if (target.classList.contains('delete')) {
console.log('2. Delete action');
console.log('3. for relatedID', commentId);
}
if (target.classList.contains('edit')) {
console.log('2. Edit action');
console.log('3. for relatedID', commentId);
}
if (target.classList.contains('flag')) {
console.log('2. Flag action');
console.log('3. for relatedID', commentId);
}
});
.controller {
display: flex;
gap:20px;
}
.comment {
margin-bottom: 20px;
background: gray;
}
.controller button > span {
background: orange;
}
.controller button span:first-child {
margin-right: 10px;
}
<div class="comment-list">
<div class="comment">
<div class="content">lorem 1. Dont work! Nested button.</div>
<div class="controller" data-comment-id="1">
<div class="delete">
<button class="delete"><span>delete</span><span>ICON</span></button>
</div>
<div class="edit">
<button class="edit"><span>edit</span><span>ICON</span></button>
</div>
<div class="flag">
<button class="flag"><span>flag</span><span>ICON</span></button>
</div>
</div>
</div>
<div class="comment">
<div class="content">lorem 2. Work! </div>
<div class="controller" data-comment-id="2">
<div class="delete"><button class="delete">delete</button></div>
<div class="edit"><button class="edit">edit</button></div>
<div class="flag"><button class="flag">flag</button></div>
</div>
</div>
<div class="comment">
<div class="content">lorem 3. Work! </div>
<div class="controller" data-comment-id="3">
<div class="delete"><button class="delete">delete</button></div>
<div class="edit"><button class="edit">edit</button></div>
<div class="flag"><button class="flag">flag</button></div>
</div>
</div>
</div>
Related
I want to change the layout of a page that has 3 columns:
<div>
<div class="container">
<div class="row" >
<div class="col-md-4"></div>
<div class="col-md-4"></div>
<div class="col-md-4"></div>
</div>
</div>
</div>
... to 4 columns when a button is clicked:
<div>
<div class="container">
<div class="row" >
<div class="col-md-3"></div>
<div class="col-md-3"></div>
<div class="col-md-3"></div>
<div class="col-md-3"></div>
</div>
</div>
</div>
I have no clue on how to do this.
There are many ways you can add another div. Here is my approach :
function appendDiv(){
let row = document.getElementsByClassName('row');
// change className for all the col-md-4 div
document.querySelectorAll('.col-md-4').forEach(function(item) {
item.className = 'col-md-3';
})
//create new div;
let col = document.createElement('div');
// add classname to div
col.className = "col-md-3"
row[0].appendChild(col)
}
.col-md-4{
border : 1px solid blue;
height : 20px;
}
.col-md-3{
border : 1px solid green;
height : 20px;
}
<div>
<div class="container">
<div class="row" >
<div class="col-md-4"></div>
<div class="col-md-4"></div>
<div class="col-md-4"></div>
</div>
<button onClick='appendDiv()'>click</button>
</div>
</div>
There's a few ways this could be done depending on your data, however, here's one angle.
If you have both your 4 column & 3 column versions of the data loaded on the page (but one hidden with css). You could run something like this.
HTML
<div id="colsThree" class="displayArea show">
<div class="container">
<div class="row" >
<div class="col-md-4"></div>
<div class="col-md-4"></div>
<div class="col-md-4"></div>
</div>
</div>
</div>
<div id="colsFour" class="displayArea">
<div class="container">
<div class="row" >
<div class="col-md-4"></div>
<div class="col-md-4"></div>
<div class="col-md-4"></div>
</div>
</div>
</div>
<button id="changeColumns">Click Me To Change Columns</button>
Javascript
const buttonEl = document.querySelector("#changeColumns");
buttonEl.addEventListener('click', () => {
const outputEls = document.querySelectorAll('.displayArea')
outputEls.forEach((outputEl) => {
outputEl.toggle("show")
})
});
CSS
.displayArea {
display: none;
}
.displayArea.show {
display: block;
}
Use forEach and appendChild method.
const btn = document.querySelector('#btn')
btn.onclick = function() {
const targetClasses = document.querySelectorAll('.col-md-4')
targetClasses.forEach((tag, idx) => {
tag.className = 'col-md-3'
const lastIdx = targetClasses.length - 1
if (idx === lastIdx) {
const tag = document.createElement('div')
, row = document.querySelector('.row')
tag.className = 'col-md-3'
tag.innerText = '4'
row.appendChild(tag)
}
})
console.log(targetClasses)
return
}
<div>
<button id="btn">Click me</button>
<div class="container">
<div class="row" >
<div class="col-md-4">1</div>
<div class="col-md-4">2</div>
<div class="col-md-4">3</div>
</div>
</div>
</div>
If you're only using vanilla HTML, CSS, and JavaScript, then one of the ways to achieve this is by adding a click listener to the button beforehand. FYI: for brevity's sake, I'll call the div element with row class as parent. When user clicks the button, then it should
remove col-md-4 class and add col-md-3 class to all the children elements of parent.
add a new div element with col-md-3 class into parent.
Here's a link to the codepen for your reference.
const button = document.querySelector('button');
const rowDiv = document.querySelector('.row');
button.addEventListener('click', (e) => {
Array.from(rowDiv.children).forEach(childDiv => {
childDiv.classList.remove('col-md-4');
childDiv.classList.add('col-md-3');
});
const newDiv = document.createElement('div');
newDiv.classList.add('col-md-3');
rowDiv.appendChild(newDiv);
// I disabled the button to prevent the user
// from clicking it the second time.
e.target.disabled = true;
});
.button-parent {
margin: 15px 0;
}
.row {
height: 100vh;
}
.row > div:nth-child(1) {
background: red;
}
.row > div:nth-child(2) {
background: blue;
}
.row > div:nth-child(3) {
background: yellow;
}
.row > div:nth-child(4) {
background: green;
}
<head>
<link href="https://cdn.jsdelivr.net/npm/bootstrap#5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
</head>
<body>
<div>
<div class="container">
<div class="button-parent">
<button class="btn btn-primary">Add div</button>
</div>
<div class="row">
<div class="col-md-4"></div>
<div class="col-md-4"></div>
<div class="col-md-4"></div>
</div>
</div>
</div>
</body>
I am trying to do a question page where when you click on the plus at the end of a question the answer appears. I have the answer hidden and try adding class show-text with display of block instead of none and switching the plus button to a minus, however on click it does nothing! any help would be greatly appreciated!
const questions2 = document.querySelectorAll(".question")
questions2.forEach(function(info) {
const btn = info.querySelector(".question-btn")
btn.addEventListener("click", function() {
questions2.forEach(function(item) {
if (item !== info) {
item.classList.remove("show-text")
}
})
info.classList.toggle("show-text")
})
})
.question-text {
display: none
}
.show-text .question-text {
display: block
}
.minus-icon {
display: none
}
.show-text .minus-icon {
display: inline
}
.show-text .plus-icon {
display: none
}
<section class="questions">
<!-- Title -->
<div class="title">
<h2 class="heading">Questions</h2>
<div class="underline"></div>
</div>
<!-- Questions -->
<div class="section-center">
<article class="question">
<div class="question-title">
<p>Question here</p>
<button type="button" class="question-btn">
<span class="plus-icon"><i class="far fa-plus-square"></i></span>
<span class="minus-icon"><i class="far fa-minus-square"></i></span>
</button>
</div>
<div class="question-text">
<p>Answer here</p>
</div>
</article>
</div>
</section>
You could try letting your button keeps track of which answer it controls with the aria-controls attribute and use the aria-hidden attribute to toggle the display property of the answer.
const questions = document.querySelectorAll(".question");
questions.forEach(question => {
const questionBtn = question.querySelector("button");
const btnControls = questionBtn.getAttribute("aria-controls");
const answer = document.getElementById(btnControls);
questionBtn.addEventListener("click", () => {
if(answer.getAttribute("aria-hidden") == "true") {
answer.setAttribute("aria-hidden", "false")
} else {
answer.setAttribute("aria-hidden", "true")
}
})
})
div[aria-hidden="true"] {
display: none;
}
<section class="questions">
<!-- Title -->
<div class="title">
<h2 class="heading">Questions</h2>
<div class="underline"></div>
</div>
<!-- Questions -->
<div class="section-center">
<article class="question">
<div class="question-title">
<p>Question here</p>
<button type="button" class="question-btn" aria-controls="answer1">
<span class="plus-icon"><i class="far fa-plus-square"></i></span>
<span class="minus-icon"><i class="far fa-minus-square"></i></span>
</button>
</div>
<div id="answer1" class="question-text" aria-hidden="true">
<p>Answer here</p>
</div>
</article>
</div>
</section>
I am working with HTML and JS and I am trying to add a click event listener so that the index = 0 value updates automatically on click.
Depending on which .tab element I click, the content inside of the div.tab should change to display either Hello1, Hello2 or Hello3.
The first tab should display the tab content hello1 accordingly.
I have provided both with data attributes in the HTML. I want the corresponding div.tab content to be output if both have the same data attribute with vanilla JS.
JS
const tabbarContent = document.querySelectorAll(".tab-content");
const tabbarButtons = document.querySelectorAll(".tab");
const index = 0;
tabbarContent[index].classList.add('active');
tabbarButtons[index].classList.add('active');
const dataAttribute1 = tabbarButtons[index].getAttribute("data-tab");
const dataAttribute2 = tabbarContent[index].getAttribute("data-tab");
function clickTab() {
for (let index = 0; index.length; index++) {
if (dataAttribute1[index] === dataAttribute2[index]) {
dataAttribute2.classList.add("active");
}
}
}
tabbarButtons[index].addEventListener("click", clickTab);
HTML
<div class="container">
<div class="inner-container">
<div class="tab tab1" data-tab="1">
1
</div>
<div class="tab tab2" data-tab="2">
2
</div>
<div class="tab tab3" data-tab="3">
3
</div>
</div>
<div class="inner-container">
<div class="tab-content content1" data-tab="1">
<h1>Hello1</h1>
</div>
<div class="tab-content content2" data-tab="2">
<h1>Hello2</h1>
</div>
<div class="tab-content content3" data-tab="3">
<h1>Hello3</h1>
</div>
</div>
</div>
Thanks
Jessy
If I understand correctly what you want to achieve, you could do something like:
const tabbarButtons = document.querySelectorAll(".tab");
const tabbarContents = document.querySelectorAll(".tab-content");
tabbarButtons.forEach((tabBtn) => {
tabBtn.addEventListener('click', () => {
tabbarContents.forEach(tabCnt => {
if (tabCnt.dataset.tab === tabBtn.dataset.tab) {
tabCnt.classList.add('active');
} else {
tabCnt.classList.remove('active');
}
})
});
})
.tab {
display: inline-block;
padding: 5px;
border: 1px solid #ccc;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
<div class="container">
<div class="inner-container">
<div class="tab tab1" data-tab="1">
1
</div>
<div class="tab tab2" data-tab="2">
2
</div>
<div class="tab tab3" data-tab="3">
3
</div>
</div>
<div class="inner-container">
<div class="tab-content content1" data-tab="1">
<h1>Hello1</h1>
</div>
<div class="tab-content content2" data-tab="2">
<h1>Hello2</h1>
</div>
<div class="tab-content content3" data-tab="3">
<h1>Hello3</h1>
</div>
</div>
</div>
I don't want the value of button but instead I want the value of a span inside a div which corresponds to that button. I have a model in which buttons correspond to a div.
For example here you can see there are 3 buttons that correspond to each assignment.
Whenever a user clicks Assign button I need to return the value of the title span for example if someone clicks assign on first, it should print "English UOI 1" in the console.
The html code wraps up like this -
<div class="assignment-wrapper">
<div class="assignment-title"><span class="assignment-name">English UOI 1</span><span>15-06-2021</span></div>
<div class="assignment-functions">
<div class="assign-task assignment-function-btn">Assign</div>
<div class="update-task assignment-function-btn">Update</div>
<div class="view-results assignment-function-btn">Results</div>
</div>
</div>
Now this is the example for structure but I would be doing this using javascript, I have retrieved data for database an I need to present each assignment in a table form.
If you have a better model you can suggest changes.
Any help is appreciated.
You can target up the element tree 2 parents and then back down two children and get the spans textContent.
const btn = document.querySelectorAll('.assignment-function-btn')
function getValue(e){
let target = e.target.parentNode.parentNode.children[0].children[0]
console.log(target.textContent)
}
btn.forEach(button => {
button.addEventListener('click', getValue)
})
.row {
display: flex;
justify-content: space-around;
}
.assignment-function-btn {
background: #ddd;
border: 1px solid black;
border-radius: 5px;
margin-left: 5px;
padding: 3px;
cursor: pointer;
}
<div class="assignment-wrapper row">
<div class="assignment-title">
<span class="assignment-name">English UOI 1</span>
<span>15-06-2021</span>
</div>
<div class="assignment-functions row">
<div class="assign-task assignment-function-btn">Assign</div>
<div class="update-task assignment-function-btn">Update</div>
<div class="view-results assignment-function-btn">Results</div>
</div>
</div>
or...
If each of these classes per say, has its own line with the three buttons for each title/class, you could code a dataset attribute into the buttons parent element that represents the title/class that button is being used for. See my second snippit for that example...
Using dataset attribute:
In your HTML you add a data attribute like data-class-name="English UOI 1" to the direct parent element of your buttons. Then use the event target + parentNode to get the e.target.parentNode.dataset.className.
className is javascript camel-case for class-name.
<div class="assignment-title">
<span class="assignment-name">English UOI 1</span>
<span>15-06-2021</span>
</div>
<!--/ Here we add the data attribute data-class-name="English UOI 1" /-->
<div data-class-name="English UOI 1" class="assignment-functions row">
<div class="assign-task assignment-function-btn">Assign</div>
<div class="update-task assignment-function-btn">Update</div>
<div class="view-results assignment-function-btn">Results</div>
</div>
const btn = document.querySelectorAll('.assignment-function-btn')
function getValue(e) {
let target = e.target.parentNode
console.log(target.dataset.className)
}
btn.forEach(button => {
button.addEventListener('click', getValue)
})
.row {
display: flex;
justify-content: space-around;
}
.assignment-title {
flex: auto;
}
.assignment-function-btn {
background: #ddd;
border: 1px solid black;
border-radius: 5px;
margin-left: 5px;
padding: 3px;
cursor: pointer;
}
<div class="assignment-wrapper">
<div class="row">
<div class="assignment-title">
<span class="assignment-name">English UOI 1</span>
<span>15-06-2021</span>
</div>
<div data-class-name="English UOI 1" class="assignment-functions row">
<div class="assign-task assignment-function-btn">Assign</div>
<div class="update-task assignment-function-btn">Update</div>
<div class="view-results assignment-function-btn">Results</div>
</div>
</div>
<div class="row">
<div class="assignment-title">
<span class="assignment-name">Social Sciences</span>
<span>23-06-2021</span>
</div>
<div data-class-name="Social Sciences" class="assignment-functions row">
<div class="assign-task assignment-function-btn">Assign</div>
<div class="update-task assignment-function-btn">Update</div>
<div class="view-results assignment-function-btn">Results</div>
</div>
</div>
<div class="row">
<div class="assignment-title">
<span class="assignment-name">Concepts of Algebra</span>
<span>26-06-2021</span>
</div>
<div data-class-name="Concepts of Algebra" class="assignment-functions row">
<div class="assign-task assignment-function-btn">Assign</div>
<div class="update-task assignment-function-btn">Update</div>
<div class="view-results assignment-function-btn">Results</div>
</div>
</div>
</div>
You could use .closest to grab the .assignment-wrapper up the DOM tree and then, within the wrapper, find the .assignment-name element. Then grab its innerText.
Edit: Also, I strongly recommend using button elements for button behavior for accessibility purposes.
const btn = document.querySelector(".assign-task");
btn.addEventListener("click", (e) => {
const wrapper = e.target.closest(".assignment-wrapper");
const assignment = wrapper.querySelector(".assignment-name");
console.log(assignment.innerText);
})
<div class="assignment-wrapper">
<div class="assignment-title"><span class="assignment-name">English UOI 1</span><span>15-06-2021</span></div>
<div class="assignment-functions">
<button class="assign-task assignment-function-btn">Assign</button>
<button class="update-task assignment-function-btn">Update</button>
<button class="view-results assignment-function-btn">Results</button>
</div>
</div>
what i understand of you that you wants to get the text of .assignment-title span blongs to the parent of this button
Here's Example
var assignTask = document.getElementsByClassName('assign-task');
for(var i = 0; i < assignTask.length; i++) {
assignTask[i].addEventListener('click',function() {
var text = this.parentNode.parentNode.getElementsByClassName('assignment-name')[0].innerText;
console.log(text)
})
}
You can try the below
<div class="assignment-wrapper">
<div class="assignment-title">
<span class="assignment-name">English UOI 1</span
><span>15-06-2021</span>
</div>
<div class="assignment-functions">
<div class="assign-task assignment-function-btn">Assign</div>
<div class="update-task assignment-function-btn">Update</div>
<div class="view-results assignment-function-btn">Results</div>
</div>
</div>
<script>
function handleButtonClick(event) {
const buttonType = event.target.innerText;
const assignmentName = event.target
.closest(".assignment-wrapper")
.querySelector(".assignment-name").innerText;
console.log(`${assignmentName} -> ${buttonType}`);
}
document
.querySelectorAll(".assignment-function-btn")
.forEach((ele) => ele.addEventListener("click", handleButtonClick));
</script>
With the press of a button, I want to toggle the class .active on the next div.bottom. These are basically accordions, but with a different structure.
Using nextElementSibling I guess won't work here to select the target element. How would one select such an element, that's neither a child nor a sibling (in plain JS)?
<div class="wrapper">
<div class="top">
<div class="inner">
<div><button></button></div>
</div>
</div>
<div class="bottom"></div>
</div>
<div class="wrapper">
<div class="top">
<div class="inner">
<div><button></button></div>
</div>
</div>
<div class="bottom"></div>
</div>
I'd do it by using closest to go up to the container .wrapper element, then querySelector to find the bottom element:
function onClick(event) {
const wrapper = event.target.closest(".wrapper");
const bottom = wrapper && wrapper.querySelector(".bottom");
if (bottom) {
bottom.classList.toggle("active");
}
}
Live Example:
// I've added event delegation here
document.body.addEventListener("click", function onClick(event) {
const button = event.target.closest(".inner button");
const wrapper = button && button.closest(".wrapper");
const bottom = wrapper && wrapper.querySelector(".bottom");
if (bottom) {
bottom.classList.toggle("active");
}
});
.active {
color: blue;
border: 1px solid black;
}
<div class="wrapper">
<div class="top">
<div class="inner">
<div><button>Button A</button></div>
</div>
</div>
<div class="bottom">Bottom A</div>
</div>
<div class="wrapper">
<div class="top">
<div class="inner">
<div><button>Button B</button></div>
</div>
</div>
<div class="bottom">Bottom B</div>
</div>
Or the same thing using optional chaining (relatively new):
function onClick(event) {
const wrapper = event.target.closest(".wrapper");
const bottom = wrapper?.querySelector(".bottom");
bottom?.classList.toggle("active");
}
Live Example:
// I've added event delegation here
document.body.addEventListener("click", function onClick(event) {
const button = event.target.closest(".inner button");
const wrapper = button?.closest(".wrapper");
const bottom = wrapper?.querySelector(".bottom");
bottom?.classList.toggle("active");
});
.active {
color: blue;
border: 1px solid black;
}
<div class="wrapper">
<div class="top">
<div class="inner">
<div><button>Button A</button></div>
</div>
</div>
<div class="bottom">Bottom A</div>
</div>
<div class="wrapper">
<div class="top">
<div class="inner">
<div><button>Button B</button></div>
</div>
</div>
<div class="bottom">Bottom B</div>
</div>
By using closest() you can traverse the DOM upwards. With this it's easy to just get the relevant .bottom and toggle the active class on this element.
document.querySelectorAll('button').forEach(button => {
button.addEventListener('click', (e) => {
e.currentTarget.closest('.wrapper').querySelector('.bottom').classList.toggle('active');
});
});
.bottom {
display: none
}
.bottom.active {
display: block
}
<div class="wrapper">
<div class="top">
<div class="inner">
<button type="button">Toggle</button>
</div>
</div>
<div class="bottom">Hidden content</div>
</div>
<div class="wrapper">
<div class="top">
<div class="inner">
<button type="button">Toggle 2</button>
</div>
</div>
<div class="bottom">Hidden content 2</div>
</div>