javascriptevent-handlingdom-eventsselectors-apievent-delegation

3 way toggle to show 3 different divs in js


I've created a 3 way toggle with 3 states disabled, default, enabled.

On clicking each input, the corresponding div's should be displayed.

var content = function() {
  var divs = ["div-data1", "div-data2", "div-data3"];
  var visibleDivId = null;

  function toggle() {
    //code
  }

  function init() {
    toggle();
  }
  return {
    init: init,
  }
}();

window.onload = function() {
  content.init();
};
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css" rel="stylesheet"/>

<div class="tw-toggle">
  <input type="radio" name="toggle" class="threeToggle1" value="false">
  <label class="toggle toggle-yes"><i class="fa fa-times"></i></label>
  <input checked type="radio" name="toggle" class="threeToggle2" value="-1">
  <label class="toggle toggle-yes"><i class="fa fa-minus"></i></label>
  <input type="radio" name="toggle" class="threeToggle3" value="true">
  <label class="toggle toggle-yes"><i class="fa fa-check"></i></label>
</div>

<div class="div-data1" style="display:none"> div1 </div>
<div class="div-data2" style="display:none"> div2</div>
<div class="div-data3" style="display:none">div3 </div>

How could the toggle works, without passing onclick to HTML ?

Could someone please help.

Thanks.


Solution

  • One approach is as follows, with explanatory comments in the code:

    // a named function, using Arrow syntax, to handle the toggling; this passes
    // a reference to the Event Object (automagically, from the later use of
    // EventTarget.addEventListener()):
    const toggle = (event) => {
    
        // we retrieve the current element to which the event-handler was bound:
        let current = event.currentTarget,
          // we retrieve the parentNode of that element:
          parent = current.parentNode,
          // we use an Array-literal, with the spread syntax, to create an
          // Array of the parent-element's element-children
          children = [...parent.children]
          // and we filter that Array of elements with the anonymous Arrow
          // function of the Array.prototype.filter() method:
          .filter(
            // here we pass 'el', a reference to the current Element of the
            // Array of Elements we're iterating over, to retain only the
            // elements which have a tagName exactly equal to the tagName
            // of the current element:
            (el) => current.tagName === el.tagName
          ),
          // using Array.prototype.findIndex() to retrieve the index of the
          // 'current' element from an Array containing it, and its siblings:
          currentIndex = children.findIndex(
            // here we again pass in a reference to the current element 'el'
            // of the Array of elements, and retrieve the element which is
            // the 'current' (variable-name) element:
            (el) => el === current
          );
    
        // we use document.querySelectorAll() to retrieve the elements matching
        // the selector stored in the element's 'data-group' attribute:
        document.querySelectorAll(current.dataset.group)
          // iterating over those elements, with NodeList.prototype.forEach():
          .forEach(
            // passing in a reference to the current element ('el'), and the
            // index of the current element ('index'); here we update the
            // opacity property, by assessing whether the 'index' variable
            // is exactly-equal to the 'currentIndex' variable. If it is,
            // we return 1 (so the element is fully visible), otherwise
            // we return an invalid empty-string, which removes the
            // opacity from the inline style attribute:
            (el, index) => el.style.opacity = index === currentIndex ? 1 : ''
          );
      },
      // creating a custom Event:
      changeEvent = new Event('change');
    
    // using document.querySelectorAll() to find all <input> elements inside of
    // a .toggleGroup element, and iterating over that NodeList with
    // NodeList.prototype.forEach():
    document.querySelectorAll('.toggleGroup input').forEach(
      // here we - again - pass in a reference to the current element ('el'),
      // and use EventTarget.addEventListener() to bind the toggle() function
      // (note the deliberate omission of the parentheses) as the event-
      // handler for the 'change' event fired on the elements:
      (el) => {
        el.addEventListener('change', toggle);
        // using a Yoda condition to see if the current element is exactly-equal
        // to true (that way we can't accidentally use assignment ('=') instead
        // of comparison ('==' or '===') without generating an error:
        if (true === el.checked) {
          // triggering the 'change' event in order to have the correct element
          // show on page-load:
          el.dispatchEvent(changeEvent);
        }
      }
    );
    *,
     ::before,
     ::after {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    
    main {
      inline-size: clamp(15em, 50%, 900px);
      margin-block: 1em;
      margin-inline: auto;
    }
    
    .toggleGroup,
    .groupToToggle {
      display: flex;
      gap: 1em;
      justify-content: center;
      margin-block: 1em;
    }
    
    .div-data {
      border: 1px solid currentColor;
      border-radius: 0.25em;
      opacity: 0.2;
      padding-block: 0.25em;
      padding-inline: 0.5em;
      transition: opacity 0.4s linear;
    }
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css" type="text/css">
    
    <main>
      <div class="tw-toggle toggleGroup">
        <!--
          Added an id attribute, in order that we can associate the <input> with the <label>,
          I also added a 'data-group' attribute, which contains an attribute-value which
          serves as the CSS selector for the relevant group of elements:
        -->
        <input id="tw-toggle-1" type="radio" name="toggle" class="threeToggle1" value="false" data-group=".div-data">
        <!--
          As above, the 'for' attribute (the attribute-value of which is identical to the
          (unique) 'id' attribute of the relevant element in order to associate the <input>
          and <label> together:
        -->
        <label for="tw-toggle-1" class="toggle toggle-yes"><i class="fa fa-times"></i></label>
    
        <input id="tw-toggle-2" checked type="radio" name="toggle" class="threeToggle2" value="-1" data-group=".div-data">
        <label for="tw-toggle-2" class="toggle toggle-yes"><i class="fa fa-minus"></i></label>
    
        <input id="tw-toggle-3" type="radio" name="toggle" class="threeToggle3" value="true" data-group=".div-data">
        <label for="tw-toggle-3" class="toggle toggle-yes"><i class="fa fa-check"></i></label>
      </div>
    
      <!--
        Added a wrapper to group the related elements together:
      -->
      <div class="groupToToggle">
        <!--
          Added a 'div-data' class-name to easily target all elements with CSS, and
          removed the inline styles preferring instead to move presentation entirely
          to the CSS:
        -->
        <div class="div-data div-data1">div1</div>
        <div class="div-data div-data2">div2</div>
        <div class="div-data div-data3">div3</div>
      </div>
    </main>

    References: