Javascript on click event for multiple buttons with same class - javascript

I have a few buttons across a site I am building, certain buttons have one class while others have another. What I am trying to do is find the best way to find the clicked button without having an event listener for each individual button. I have come up with the below 2 for loops to find all the buttons with class button-1 and class button-2. Being fairly new to javascript i just don't want to get into bad habits so would appreciate any advice on the best way to achieve this.
<section>
<div class="button--1"></div>
<div class="button--1"></div>
<div class="button--2"></div>
</section>
<section>
<div class="button--2"></div>
<div class="button--1"></div>
<div class="button--2"></div>
</section>
var button1 = document.querySelectorAll('.button--1');
var button2 = document.querySelectorAll('.button--2');
for (var a = 0; a < button1.length; a++) {
button1[a].addEventListener('click',function(){
//do something
});
}
for (var b = 0; b < button2.length; b++) {
button1[b].addEventListener('click',function(){
//do something
});
}

If you plan to have multiple other classes like button--3, …4 … …15,
You must want to target all div elements which class starts (^=) with "button":
(Note that you can do it in the CSS too!)
var allButtons = document.querySelectorAll('div[class^=button]');
console.log("Found", allButtons.length, "div which class starts with “button”.");
for (var i = 0; i < allButtons.length; i++) {
allButtons[i].addEventListener('click', function() {
console.clear();
console.log("You clicked:", this.innerHTML);
});
}
/* Some styling */
section {
margin: 8px 0;
border: 1px solid gray;
}
section div {
border: 1px solid lightgray;
display: inline-block;
margin-left: 8px;
padding: 4px 8px;
width: 30px;
}
section div[class^=button] {
background: lightgray;
cursor: pointer;
}
<span>You can click on the buttons:</span>
<section>
<div class="button--1">s1-1</div>
<div class="button--2">s1-2</div>
<div class="button--3">s1-3</div>
<div class="button--4">s1-4</div>
</section>
<section>
<div class="button--1">s2-1</div>
<div class="button--2">s2-2</div>
<div class="button--3">s2-3</div>
<div class="button--4">s2-4</div>
</section>
<section>
<div class="not-a-button">not1</div>
<div class="not-a-button">not2</div>
<div class="not-a-button">not3</div>
<div class="not-a-button">not4</div>
</section>
Hope it helps.

Try using event delegation
(function() {
document.body.addEventListener("click", clickButtons);
// ^ one handler for all clicks
function clickButtons(evt) {
const from = evt.target;
console.clear();
if (!from.className || !/button--\d/i.test(from.className)) { return; }
// ^check if the element clicked is one of the elements you want to handle
// if it's not one of the 'buttons', do nothing
console.log("you clicked " + from.classList);
}
}())
.button--1:before,
.button--2:before {
content: 'BTTN['attr(class)']';
}
.button--1,
.button--2 {
border: 1px solid #999;
background: #eee;
width: 220px;
padding: 3px;
text-align: center;
}
<section>
<div class="b1 button--1 section1"></div>
<div class="b2 button--1 section1"></div>
<div class="b3 button--2 section1"></div>
</section>
<section>
<div class="b4 button--2 section2"></div>
<div class="b5 button--1 section2"></div>
<div class="b6 button--2 section2"></div>
</section>

You can use multiple selectors in the string of querySelctorAll() by separating them with a ,
var button1 = document.querySelectorAll('.button--1');
var button2 = document.querySelectorAll('.button--2');
var allButtons = document.querySelectorAll('.button--1, .button--2');
console.log(button1.length);
console.log(button2.length);
console.log(allButtons.length);
<section>
<div class="button--1"></div>
<div class="button--1"></div>
<div class="button--2"></div>
</section>
<section>
<div class="button--2"></div>
<div class="button--1"></div>
<div class="button--2"></div>
</section>

My suggestion is to use jQuery so that you can do it something like this:
$(document).on('click', '.button--1', function() {
// Do something
});
$(document).on('click', '.button--1', function() {
// Do something
})
But a clean approach for pure Javascript is to create a function that binds a callback for the event.
function bindEvent(callback, eventType, targets) {
targets.forEach(function(target) {
target.addEventListener(eventType, callback);
});
};
var button1 = document.querySelectorAll('.button--1');
var button2 = document.querySelectorAll('.button--2');
bindEvent(function() {
// do something
}, 'click', button1);
bindEvent(function() {
// do something
}, 'click', button2);

The click event is fired when a pointing device button (usually a mouse's primary button) is pressed and released on a single element.
This documentation will help you to understand how it works MDN - Click event

Related

How do I check whether an element is already bound to an event?

Goal
Avoid unnecessary event bindings.
Sample code
Comment box with a reply button for each individual comment
const btns = document.getElementsByClassName('reply-btn');
for (let i = 0; i < btns.length; i++) {
btns[i].addEventListener('click', showCommentContentAsPreview);
}
function showCommentContentAsPreview(e) {
console.log('showCommentContentAsPreview()');
// CHECK IF THIS BUTTON ALREADY BINDED !!!
const previewDiv = document.getElementById('preview');
const commentId = e.target.getAttribute('data-comment-id')
const commentDiv = document.getElementById('comment-' + commentId);
const commentText = commentDiv.querySelector('p').innerText
const closeReplyBtn = previewDiv.querySelector('button');
const previewContent = previewDiv.querySelector('.preview-content');
// set to preview
previewContent.innerText = commentText;
// show reply close button
closeReplyBtn.classList.remove('hidden');
// bind EventListener to "reply close button"
closeReplyBtn.addEventListener('click', closeReply)
function closeReply() {
console.log('bind to btn');
previewContent.innerText = '';
this.removeEventListener('click', closeReply);
closeReplyBtn.classList.add('hidden');
}
}
.hidden {
display: none;
}
.comment {
border-bottom: 1px solid #000;
padding: 5px;
}
.preview {
background-color: #ccc;
padding: 20px;
margin-top: 20px;
}
<div>
<!-- comment list -->
<div id="comment-1" class="comment">
<p>Comment Content 1</p>
<button class="reply-btn" data-comment-id="1">reply</button>
</div>
<div id="comment-2" class="comment">
<p>Comment Content 2</p>
<button class="reply-btn" data-comment-id="2">reply</button>
</div>
</div>
<!-- output -->
<div>
<div id="preview" class="preview">
<div class="preview-content"></div>
<button class="hidden">Close Preview</button>
</div>
</div>
Simulate problem
When you try the example, the following two scenarios occur:
Click reply once and then click "close preview"
Click on reply several times and then on "close preview".
Question
How can I avoid multiple bindings to the same button? I am already thinking about singleton.
Instead of binding a listener to every element in the series, you can bind a single listener once on a common parent of them all, and then use element.matches() to determine if the click target is the one that you want before doing more work. See the following example:
function logTextContent (elm) {
console.log(elm.textContent);
}
function handleClick (ev) {
if (ev.target.matches('.item')) {
logTextContent(ev.target);
}
}
document.querySelector('ul.list').addEventListener('click', handleClick);
<ul class="list">
<li class="item">Item 1</li>
<li class="item">Item 2</li>
<li class="item">Item 3</li>
<li class="item">Item 4</li>
<li class="item">Item 5</li>
</ul>
With the helpful hints from #Zephyr and #jsejcksn I have rewritten the code of the above question. Thus I have achieved my goal of avoiding multiple identical bindings to one element.
const container = document.getElementById('comment-container');
const previewDiv = document.getElementById('preview');
const closeReplyBtn = previewDiv.querySelector('button');
const previewContent = previewDiv.querySelector('.preview-content');
container.addEventListener('click', handleClick);
function handleClick(ev) {
if (ev.target.matches('.reply-btn')) {
if (ev.target.getAttribute('listener') !== 'true') {
removeOtherListenerFlags();
ev.target.setAttribute('listener', 'true');
showCommentContentAsPreview(ev);
}
}
if (ev.target.matches('#preview button')) {
previewContent.innerText = '';
closeReplyBtn.classList.add('hidden');
removeOtherListenerFlags();
}
}
function showCommentContentAsPreview(e) {
console.log('showCommentContentAsPreview()');
const commentId = e.target.getAttribute('data-comment-id')
const commentDiv = document.getElementById('comment-' + commentId);
const commentText = commentDiv.querySelector('p').innerText
// set to preview
previewContent.innerText = commentText;
// show reply close button
closeReplyBtn.classList.remove('hidden');
}
function removeOtherListenerFlags() {
const replyBtns = container.querySelectorAll('.reply-btn')
Object.keys(replyBtns).forEach((el) => {
replyBtns[el].removeAttribute('listener');
})
}
.hidden {
display: none;
}
.comment {
border-bottom: 1px solid #000;
padding: 5px;
}
.preview {
background-color: #ccc;
padding: 20px;
margin-top: 20px;
}
<div id="comment-container">
<div id="comment-listing">
<!-- comment list -->
<div id="comment-1" class="comment">
<p>Comment Content 1</p>
<button class="reply-btn" data-comment-id="1">reply 1</button>
</div>
<div id="comment-2" class="comment">
<p>Comment Content 2</p>
<button class="reply-btn" data-comment-id="2">reply 2</button>
</div>
</div>
<!-- output -->
<div>
<div id="preview" class="preview">
<div class="preview-content"></div>
<button class="hidden">Close Preview</button>
</div>
</div>
</div>
Cool and Thanks!

jQuery .not() and :not with new classes? [duplicate]

This question already has answers here:
jQuery click event not working after adding class
(7 answers)
Closed 2 years ago.
I have some card to flip and check if one is equal to another (memory game).
If I flip the card, I don't want that is possible to click and run function if I click on the same card (that is .flipped) or on another that is flipped. But jQuery .not() and :not not working. Maybe I must read another time the DOM after .toggleClass?
$(".card:not('.flipped')").on("click", function () {
$(this).toggleClass("flipped");
if (first) {
firstCard = $(this).attr("game");
first = false;
} else {
secondCard = $(this).attr("game");
first = true;
checkGame();
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
The code is binding the events when it is called. So whatver the classes are at that moment in time, is what it finds and binds the event.
So you need to check for the class inside of the method and exit it
$(".card").on("click", function() {
var card = $(this);
if (card.hasClass("flipped")) return;
console.log(this);
card.addClass("flipped");
});
.wrapper {
display: flex;
}
.wrapper > .card {
flex: 1;
text-align: center;
line-height: 50px;
font-size: 30px;
}
.card.flipped {
background-color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="wrapper">
<div class="card">1</div>
<div class="card">2</div>
<div class="card">3</div>
<div class="card">4</div>
<div class="card">5</div>
<div class="card">6</div>
<div class="card">7</div>
<div class="card">8</div>
</div>
Other option is using event delegation where you bind the event to the parent and element and have jQuery do the checking if the class is added yet.
$(".wrapper").on("click", ".card:not('.flipped')", function() {
console.log(this);
var card = $(this);
card.addClass("flipped");
});
.wrapper {
display: flex;
}
.wrapper > .card {
flex: 1;
text-align: center;
line-height: 50px;
font-size: 30px;
}
.card.flipped {
background-color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="wrapper">
<div class="card">1</div>
<div class="card">2</div>
<div class="card">3</div>
<div class="card">4</div>
<div class="card">5</div>
<div class="card">6</div>
<div class="card">7</div>
<div class="card">8</div>
</div>
Apply the click event to all cards.
On click, check if the card is flipped. If it is, do nothing.
Cache your jQuery objects.
$(".card").on("click", function() {
const $card = $(this);
if ($card.hasClass("flipped")) return;
$card.toggleClass("flipped");
// .........
});
you have to check it inside your event 'click'. if the card has class 'flipped' break it
$(".card").on("click", function () {
if($(this).hasClass('.flipped')) return;
$(this).toggleClass("flipped");
if (first) {
firstCard = $(this).attr("game");
first = false;
} else {
secondCard = $(this).attr("game");
first = true;
checkGame();
}
});

TicTacToe - trouble alternating players

First mouse click gives me the expected 'x' marker but the next mouse click results in the alert ("Player o: tile has already been selected) even though the grid-item is blank. What am I doing wrong?
In the HTML body, I have created a 3x3 grid, 9 gridItems in total.
$(document).ready(function(){
let grid = $("#grid-container")
let gridItem = $(".grid-item")
function select(){
for (i = 0; i < gridItem.length; i++){
gridItem[i].addEventListener("click", function(){
if ($(this).html() == ""){
$(this).html("x");
nextSelect();
} else {alert ("Player X: tile has already been selected"); select()}
})
}
}
function nextSelect(){
for (i = 0; i < gridItem.length; i++){
gridItem[i].addEventListener("click", function(){
if ($(this).html() == ""){
$(this).html("o");
select();
} else {alert ("Player o: tile has already been selected"); nextSelect()}
})
}
}
select()
});
The addEventListener should be executed only once. Your problem is that you're adding multiple event handlers. A possible version in the snippet bellow.
The idea is that you only need one function and attach it to click event only once per cell, which is done with jQuery .click(callback). To manage the turns you can use a boolean variable (isTurnOfPlayerOne).
$(document).ready(function() {
let isTurnOfPlayerOne = true;
$(".grid-item").click(handleClick);
function handleClick() {
if ($(this).html() == " ") {
let symbol = (isTurnOfPlayerOne ? "x" : "o");
$(this).html(symbol);
} else {
alert("Tile has already been selected");
}
isTurnOfPlayerOne = !isTurnOfPlayerOne;
}
});
.grid-item {
width: 40px;
height: 40px;
display: inline-block;
border: solid 1px #000;
text-align: center;
margin-bottom: 3px;
line-height: 30px;
font-size: 30px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="grid-container">
<div class="grid-item"> </div>
<div class="grid-item"> </div>
<div class="grid-item"> </div>
<br/>
<div class="grid-item"> </div>
<div class="grid-item"> </div>
<div class="grid-item"> </div>
<br/>
<div class="grid-item"> </div>
<div class="grid-item"> </div>
<div class="grid-item"> </div>
</div>

Refactoring jQuery repeating pattern

I have painted my self into a corner in order to quickly prototype.
What's the best way to refactor the following jQuery code? Its functionality is to toggle between some sidebar navigation items. I need it to be more dynamic in order to be scalable.
Would you add the IDs inside the if statements, in an array and iterate through them? Use variables? Create a function and call it on the html side onClick? No matter what I think of, it stills leads to a bunch of repeating code.
Thank you!
// TOGGLING LEFT NAVIGATION
$('#settingsClick').click(function() {
if( $('#addContainer, #noteContainer, #logoContainer, #themeContainer').is(':visible') ) {
$('#addContainer').slideUp(350);
$('#noteContainer').slideUp(350);
$('#logoContainer').slideUp(350);
$('#settingsContainer').slideDown(350);
$('#themeContainer').slideUp(350);
} else {
$('#settingsContainer').slideToggle(350);
}
});
$('#addClick').click(function() {
if( $('#settingsContainer, #noteContainer, #logoContainer, #themeContainer').is(':visible') ) {
$('#settingsContainer').slideUp(350);
$('#noteContainer').slideUp(350);
$('#logoContainer').slideUp(350);
$('#addContainer').slideDown(350);
$('#themeContainer').slideUp(350);
} else {
$('#addContainer').slideToggle(350);
}
});
$('#noteClick').click(function() {
if( $('#settingsContainer, #addContainer, #logoContainer, #themeContainer').is(':visible') ) {
$('#settingsContainer').slideUp(350);
$('#addContainer').slideUp(350);
$('#logoContainer').slideUp(350);
$('#noteContainer').slideDown(350);
$('#themeContainer').slideUp(350);
} else {
$('#noteContainer').slideToggle(350);
}
});
$('#logoClick').click(function() {
if( $('#settingsContainer, #addContainer, #noteContainer, #themeContainer').is(':visible') ) {
$('#settingsContainer').slideUp(350);
$('#addContainer').slideUp(350);
$('#noteContainer').slideUp(350);
$('#logoContainer').slideDown(350);
$('#themeContainer').slideUp(350);
} else {
$('#logoContainer').slideToggle(350);
}
});
$('#themeClick').click(function() {
if( $('#settingsContainer, #addContainer, #noteContainer, #logoContainer').is(':visible') ) {
$('#settingsContainer').slideUp(350);
$('#addContainer').slideUp(350);
$('#noteContainer').slideUp(350);
$('#logoContainer').slideUp(350);
$('#themeContainer').slideDown(350);
} else {
$('#themeContainer').slideToggle(350);
}
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<a id="settingsClick">Click Me</a><br>
<div id="settingsContainer">Content...</div>
<br><br>
<a id="addClick">Click Me</a><br>
<div id="addContainer">Content...</div>
<br><br>
<p> Etc... Etc....</p>
You should group using the common CSS class, i.e. header and content. Using the established relationship you can target the others content holder and content associated with the current clicked header element.
$('.container .header').on('click', function() {
//Get the current element
var $this = $(this);
//find the content
var $content = $this.closest('.container').find('.content'); //$this.next()
//get all contents
var content = $('.container .content');
//Slide up others
content.not($content).slideUp(350);
//Slide down
$content.slideToggle(350);
});
.content {
display: none
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="container">
<div class="header" id="settingsClick">Click Me</div>
<div class="content" id="settingsContainer">Content...</div>
</div>
<div class="container">
<div class="header" id="addClick">Click Me</div>
<div class="content" id="addContainer">Content...</div>
</div>
<div class="container">
<div class="header" id="noteClick">Click Me</div>
<div class="content" id="noteContainer">Content...</div>
</div>
the best bet would be to do it like so
$(document).on('click', ".trigger", function() {
var sibling_content = $(this).siblings(".content");
if (!sibling_content.hasClass('active')) {
$(".content").slideUp('slow').removeClass('active');
sibling_content.slideDown('slow').addClass('active');
} else {
sibling_content.slideUp('slow').removeClass('active');
}
})
.trigger {
background-color: red;
color: white;
font-size: 16px;
}
.content {
background-color: blue;
color: white;
font-size: 16px;
padding: 20px 0;
display: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="container">
<div class="trigger">trigger</div>
<div class="content">content</div>
</div>
<div class="container">
<div class="trigger">trigger</div>
<div class="content">content</div>
</div>
<div class="container">
<div class="trigger">trigger</div>
<div class="content">content</div>
</div>
<div class="container">
<div class="trigger">trigger</div>
<div class="content">content</div>
</div>

Add class to an element without an id

I have a list of items:
<div class="crew-item>
<div class="crew-grid"></div>
<div class="crew-detail></div>
</div>
<div class="crew-item>
<div class="crew-grid"></div>
<div class="crew-detail></div>
</div>
<div class="crew-item>
<div class="crew-grid"></div>
<div class="crew-detail></div>
</div>
When I click on a selected 'crew-grid' I'd like to add a class ('active') to its 'crew-item' parent, but I have no idea how to achieve that using vanilla js or jQuery.
The goal is to reveal the 'crew-detail' part, with active class added to its parent.
Like this?:
$('.crew-grid').on('click', function () {
$(this).closest('.crew-item').addClass('active');
});
Basically, starting from the clicked element, get the closest ancestor element which matches that selector. You don't need an id to target an element, just a way to identify it based on the information you have (in this case the clicked element).
If you want to de-activate other elements at the same time:
$('.crew-grid').on('click', function () {
$('.crew-item').removeClass('active');
$(this).closest('.crew-item').addClass('active');
});
Using jQuery :
$('.crew-grid').click(function() {
$(this).closest('.crew-item').addClass('active');
});
Use Document.querySelectorAll()
var crews = document.querySelectorAll('.crew-item');
if (crews) {
for (var i = 0; i < crews.length; i++) {
var grid = crews[i].querySelector('.crew-grid');
grid.addEventListener('click', toggleActive, false);
}
}
function toggleActive() {
var grids = document.querySelectorAll('.crew-item');
for (var i = 0; i < grids.length; i++) {
if (grids[i].classList.contains('active')) {
grids[i].classList.remove('active');
}
}
this.parentNode.classList.add('active');
}
.crew-item.active {
background: #DDD;
}
.crew-grid:hover {
cursor: pointer;
background: #eee;
}
<div class="crew-item active">
<div class="crew-grid">crew-grid</div>
<div class="crew-detail">crew-detail</div>
</div>
<br>
<div class="crew-item">
<div class="crew-grid">crew-grid</div>
<div class="crew-detail">crew-detail</div>
</div>
<br>
<div class="crew-item">
<div class="crew-grid">crew-grid</div>
<div class="crew-detail">crew-detail</div>
</div>

Categories

Resources