Toggle Not Hiding Other Elements Upon Button Clicks

I have a series of buttons that reveal their own content onclick. My function is supposed to ‘toggle’ the content for every button that is clicked. Meaning, when the second (or third, or so on) buttons are clicked, the previous content displayed should “display:none” or hide. However, I’m not getting the outcome I want. What’s happening is that if I was to go through and click each button ONCE (there are nine), and then go back and click the first button, I don’t see the content for the first button link again. The way my current function is working now is that once a button is first clicked, the class of the button is “rv_button_opened” but you have to click it again in order for it to “rv_button_closed” and hide the content. That’s not something I want…I want to get rid of the extra step and hide any previously opened content and reveal the button’s clicked content.

So I’m not sure if I need to create an additional code for the parent to find if the button has a class of ‘rv_button_opened’ to automatigically force the class ‘rv_vbutton_closed’, or if there is an easier way to accomplish this scenario?

<style>body:not(.et-fb) .rv_element { display: none; }</style>
<script>
jQuery(function($){
	var revealButtons = {
		'.rv_button_1': '.rv_element_1',
		'.rv_button_2': '.rv_element_2',
    '.rv_button_3': '.rv_element_3',
    '.rv_button_4': '.rv_element_4',
    '.rv_button_5': '.rv_element_5',
    '.rv_button_6': '.rv_element_6',
    '.rv_button_7': '.rv_element_7',
    '.rv_button_8': '.rv_element_8',
    '.rv_button_9': '.rv_element_9'
	};
	$.each(revealButtons, function(revealButton, revealElement) {
		$(revealButton).click(function(e){
			e.preventDefault();
			$(revealElement).toggle();
			$(revealButton).toggleClass('rv_button_opened rv_button_closed');
		});
	});
});
</script>

Vanilla JS, not sure if this is what you are after.

HTML

<div class='buttons'>
  <button class='rv_button' data-number='01'>Button 01</button>
  <button class='rv_button' data-number='02'>Button 02</button>
  <button class='rv_button' data-number='03'>Button 03</button>
  <button class='rv_button' data-number='04'>Button 04</button>
  <button class='rv_button' data-number='05'>Button 05</button>
</div>
<div class='elements'>
  <div class='rv_element' data-number='01'>Element 01</div>
  <div class='rv_element' data-number='02'>Element 02</div>
  <div class='rv_element' data-number='03'>Element 03</div>
  <div class='rv_element' data-number='04'>Element 04</div>
  <div class='rv_element' data-number='05'>Element 05</div>
</div>

JS

// querySelector helper
const getElem = (selector, root = document) => root.querySelector(selector)

const createHandler = function() {

  // store for previous elements
  const storedElements = {prevButton: null, prevElement: null}

  // return handler function
  return (event) => {

    const currentButton = event.target

    // if currentButton isn't a button then exit
    if (!currentButton.matches('.rv_button')) return

    // get previous elements
    const {prevButton, prevElement} = storedElements

    if (prevButton !== null) {
      prevButton.classList.remove('clicked')
      prevElement.classList.remove('show')
    }

    const num = currentButton.dataset.number
    const currentElement = getElem(`.elements [data-number='${num}']`)

    currentButton.classList.add('clicked')
    currentElement.classList.add('show')

    // store button and element references for next time
    storedElements.prevButton = currentButton
    storedElements.prevElement = currentElement
  }
}

getElem('.buttons').addEventListener('click', createHandler())

Gut feeling tells me there is a simpler way.

1 Like

I had a go in jquery just in case that was already being used :slight_smile:

Basically just hide everything and then show the correct one.

e.g.

if (!$(revealButton).hasClass("rv_button_opened")) {
        $(".rv_element").hide();
        $(".rv_button").removeClass("rv_button_opened");
      }
      $(revealElement).toggle();
      $(revealButton).toggleClass("rv_button_opened rv_button_closed");

It assumes theres a common class on the buttons and another common class on the elements.

The version by @rpg_digital is of course nicer :slight_smile:

2 Likes

Actually @PaulOB, I think you have answered the question where as I’ve re-invented the wheel (Options I guess). Good job man :slight_smile:

1 Like

Thank you, rpg_digital. You’ve given me a great alternative!

2 Likes

Thank you, PaulOB. That’s exactly what I was looking for!

1 Like

I did use a different approach

HTML

<div class='buttons' id ='btn_bank' data-active = "">
  <button class='rv_button' id ='btn_01'>Button 01</button>
  <button class='rv_button' id ='btn_02'>Button 02</button>
  <button class='rv_button' id ='btn_03'>Button 03</button>
  <button class='rv_button' id ='btn_04'>Button 04</button>
  <button class='rv_button' id ='btn_05'>Button 05</button>
</div>
<div class='elements'>
  <div class='rv_element' id ='data_01'>Element 01</div>
  <div class='rv_element' id ='data_02'>Element 02</div>
  <div class='rv_element' id ='data_03'>Element 03</div>
  <div class='rv_element' id ='data_04'>Element 04</div>
  <div class='rv_element' id ='data_05'>Element 05</div>
</div>

JS

document.getElementById('btn_bank').addEventListener('click', btnListener, false);
	const btnListener = ev => {
		ev.stopPropogation();
		ev.preventDefault();
		let tgtBtn = "";
		let data = "";
		let tgt = ev.target;  // the button element
		let container = tgt.closest(":not(button)"); // the div element
		let prevBtn = container.dataset.active;
		if(prevBtn.match(/^btn_0\d$/)) {  //  if the prevBtn string is "btn_0" followed by a digit
			data = prevBtn.replace(/^btn/, "data");  // replace "btn" with "data"
			document.getElementById('prevBtn').classList.remove('clicked');
			document.getElementById('data').classList.remove('show');
		}	
		tgt.classList.add("clicked");
		data = tgt.id.replace(/^btn/, "data");  // The data id is the modified button id
		document.getElementById('data').classList.remove('show');
		container.dataset.active = tgt.id;
	}

I stored the clicked button id in the containing div element and when a button is clicked I retrieved the button id and removed the clicked class and modified the button id to get the relevant data element id to remove the show class.
Then the target’s id is used to set it’s class to clicked and modified to get the data element’s id and set its class to show.
Finally the target id is stored in the containing elements custom data ready for the next click;

2 Likes

First off a typo, should be ‘stopPropagation’. :slight_smile:

I think the use of id’s is maybe up for debate, but just focusing on your use of a string match and a regular expression.

It’s a minor thing, but you could look at the RegExp test method or Element.matches as an alternative.

Some code examples to illustrate

const buttonId = 'btn_03'

// String.prototype.match returns an array or 'null' if no match.
// match is useful when you want to do something with that returned match.

console.log(buttonId.match(/^btn_\d/))
// returned array ['btn_0', index: 0, input: 'btn_03', groups: undefined]
// index 0 is the full match

// This time with a capturing group

console.log(buttonId.match(/^btn_(\d+)/))
// returned array ['btn_03', '03', index: 0, input: 'btn_03', groups: undefined]
// index 0 is the full match
// index 1 is the capturing group match (\d+)

// So we might want to use that number and can destructure as follows
const [rvButtonId, rvNumber] = buttonId.match(/^btn_(\d+)/)
console.log(rvNumber) // 03

// If all you need is a boolean (true/false) returned, you can use regex test instead
console.log(/^btn_\d{1}$/.test(buttonId)) // false
console.log(/^btn_\d{2}$/.test(buttonId)) // true

Personally I would lean towards Element.matches and a css selector. Not as granular as a proper regex, but specific enough I would have thought given our root element for the search is the button container element.

const div = document.createElement('div')
const button = document.createElement('button')
button.id = 'btn_03'
div.append(button)

console.log(div.firstChild.matches('[id^="btn_"]')) // true
console.log(div.firstChild.matches('[id^="data_"]')) // false

It’s also consistent with your use of ‘closest’, which leads to one more standout for me

let container = tgt.closest(":not(button)")

Wouldn’t this be better?

let container = tgt.closest("div.buttons")

If down the line you need to amend the HTML, maybe wrapping the buttons e.g.

<div class='buttons' id ='btn_bank' data-active = "">
  <div class='button_wrapper'>
    <button class='rv_button' id ='btn_01'>Button 01</button>
  </div>
  ...
</div>

then the :not(buttons) is no longer going to select the container.

Just a few thoughts, obviously take or leave them.

ps. Just want to say ‘nice one’, for tackling this in vanilla js :smiley:

1 Like

Yes, how tasks are tackled does boil down to personal style and knowledge and familiarity with the function that are chosen.
I like to store state data in custom data where it is easily available. Custom data is very handy for passing data to for among other things, forms contained within modal dialogs.
I prefer to use id’s when I want to interact with single elements. I feel it is clearer which element is involved. When dealing with groups of elements then selection by className or by css selectors is appropriate.
I only use vanilla js. There are usually several ways of accomplishing things in JS and using a library restricts you to their way of doing things.
Having a discussion of different methods is beneficial to everyone and should be encouraged.
So thanks for your insight, I will copy you suggestions for future reference.

3 Likes

Sorry @dennisjn crossed wires. A 4am post, I thought I was responding to the op toad78, who was using jquery.

I’m losing it.

1 Like

HEY! I just appreciate the input! I’m glad to see that there’s more than one way to skin a cat!

2 Likes

Thank you, gents.
Obviously there’s more than one way to make this work, and I appreciate everyone’s input!

Your assistance has been invaluable, and I thank you!

2 Likes

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.