-2

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.

Peter Seliger
  • 11,747
  • 3
  • 28
  • 37
shada
  • 45
  • 4
  • 1
    Does this answer your question? [How can I check whether a radio button is selected with JavaScript?](https://stackoverflow.com/questions/1423777/how-can-i-check-whether-a-radio-button-is-selected-with-javascript) – disinfor Feb 02 '23 at 16:19
  • 3
    There's a number of ways this could be done, but could you show your own implementation, or attempted implementation? Even if you have to use the `onclick` attribute to illustrate your current knowledge and approach that's fine, it's just so we know how to best help you. – David Thomas Feb 02 '23 at 16:29

3 Answers3

2

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:

David Thomas
  • 249,100
  • 51
  • 377
  • 410
0

If you can't modify your HTML, it is possible to add click event listeners from within your JavaScript. You don't need to add the onclick attribute to your HTML.

// Divs and toggles must have the same order. You can adjust the data structure here if this isn't desirable.
var divs = [".div-data1", ".div-data2", ".div-data3"];
var toggles = [".threeToggle1", ".threeToggle2", ".threeToggle3"];
var previouslySelectedDiv = null;

// Add click event handler to each toggle
for(let i = 0; i < toggles.length; i++) {
    var toggleElem = document.querySelector(toggles[i]);
    toggleElem.addEventListener("click", () => {
        // Get the div to show and show it by removing the "display: none" styling
        var divElem = document.querySelector(divs[i]);
        divElem.style = "";
        
        // Hide the previously shown div
        if(previouslySelectedDiv !== null) {
            previouslySelectedDiv.style = "display: none";
        }
        previouslySelectedDiv = divElem;
    });
}
<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 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>

With that said, there are probably better ways to do this if you can modify your HTML and/or aren't set on using the "click" event. One way to do this would be to store the query selector of the div which is to be displayed within the attributes of the input:

// Get all radio buttons which have a data-div attribute
const toggles = document.querySelectorAll('input[type="radio"][data-div]');
let lastSelectedDiv = null;

// Initialize shown/hidden state based on checked values
for(const toggle of toggles) {
  const div = document.querySelector(toggle.getAttribute("data-div"));
  if(toggle.checked) {
    lastSelectedDiv = div;
  } else {
    div.style.display = "none";
  }
}

// Add event listener to the radio group which hides the currently visible div and shows the new one.
document.querySelector("fieldset").addEventListener("change", (e) => {
  if(lastSelectedDiv) {
    lastSelectedDiv.style.display = "none";
  }
  const newDiv = document.querySelector(e.target.getAttribute("data-div"));
  newDiv.style.display = "";
  lastSelectedDiv = newDiv;
});
<fieldset class="tw-toggle">
  <input type="radio" name="toggle" value="false" data-div=".div-data1">
  <label class="toggle toggle-yes"><i class="fa fa-times"></i></label>
  <input checked type="radio" name="toggle" value="-1" data-div=".div-data2">
  <label class="toggle toggle-yes"><i class="fa fa-minus"></i></label>
  <input type="radio" name="toggle" value="true" data-div=".div-data3">
  <label class="toggle toggle-yes"><i class="fa fa-check"></i></label>
</fieldset>

<div class="div-data1"> div1 </div>
<div class="div-data2"> div2</div>
<div class="div-data3">div3 </div>
robere2
  • 1,689
  • 2
  • 16
  • 26
0

If it comes to functionality like the one described by the OP, I always encourage people to think in terms of (re-usable) components

  • with the functional implementation not relying to much (or even not at all) on class-names and markup-structure but on custom data-* global attributes,

  • and UI state changes relying preferably always on (a) certain class-name(s).

As for the OP's example one would implement a better structured markup which also cuts back on all the unnecessary HTML-attributes. In addition one would introduce mainly two data-attributes ... data-switch-trigger and data-switch-target where the former identifies the root-element of the switch's triggering part and the latter hints the root-element of the related target-part that is going to see the switch's state-change. The relationship of both root-nodes gets established by each an additional data-id attribute where both components need to feature the same attribute value.

The implementation then can be as straightforward as

  • querying all element nodes which feature the data-switch-trigger attribute ...

  • ... and initializing the behavior of the two-folded component ...

    document
      .querySelectorAll('[data-switch-trigger]')
      .forEach(initializeSwitchComponentBehavior);
    
  • ... with the initializing function querying the related target root-node ...

  • ... and, in case it exists, subscribing a 'change' event-listener to the currently processed trigger root-node (utilizing event-delegation) ...

  • ... where one registers an add-hoc created handler function which binds the target root-node ...

    function initializeSwitchComponentBehavior(triggerRoot) {
      const componentId = triggerRoot.dataset.id;
      const targetRoot = document.querySelector(
        `[data-switch-target][data-id="${ componentId }"]`
      );
      if (targetRoot) {
    
        triggerRoot.addEventListener(
          'change',
          handleSwitchStateChangeAtBoundTargetRoot.bind(targetRoot)
        );
      }
    }
    
  • ... and handles the switch's state-change from this bound node and the received event as follows ...

    • get the triggering node from Event.target

    • get the trigger's target-value from its datasets target-property (the one that corresponds to the very element's data-target attribute)

    • ... and in case such a value exists ...

      • query all of the bound target-root's elements which feature a data-target attribute as well.

      • forEach of this NodeList's target-nodes one does access its dataset.target value as well and does compare it to the one of the triggering element.

        Upon this comparison on either adds or removes a component- and state-specific class-name like e.g. targeted to each target-node's classList.

        And state-changes to the UI are due to all the css rules which come with this class-name.

function handleSwitchStateChangeAtBoundTargetRoot(evt) {
  // the component's bound target root reference.
  const targetRoot = this;

  const triggerNode = evt.target;
  const targetValue = triggerNode.dataset.target;

  if (targetValue) {
    targetRoot
      .querySelectorAll('[data-target]')
      .forEach(targetNode => {

        const value = targetNode.dataset.target;
        if (value === targetValue) {

          targetNode.classList.add('targeted')
        } else {
          targetNode.classList.remove('targeted')
        }
      });
  }
}

function initializeSwitchComponentBehavior(triggerRoot) {
  const componentId = triggerRoot.dataset.id;
  const targetRoot = document.querySelector(
    `[data-switch-target][data-id="${ componentId }"]`
  );
  if (targetRoot) {

    triggerRoot.addEventListener(
      'change',
      handleSwitchStateChangeAtBoundTargetRoot.bind(targetRoot)
    );
  }
}

function main() {
  document
    .querySelectorAll('[data-switch-trigger]')
    .forEach(initializeSwitchComponentBehavior);
}
main();
*,
::before,
::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}
fieldset {
  margin: 0;
  padding: 0;
  border: none;
}

[data-switch-trigger],
[data-switch-target] {
  display: flex;
  gap: 1em;
  justify-content: center;
  margin-block: 4px;
}
[data-switch-trigger] > label {
  display: inline-block;
  padding: 8px;
}

[data-switch-target] > [data-target] {
  border: 1px solid currentColor;
  border-radius: 0.25em;
  opacity: 0.2;
  padding-block: 0.25em;
  padding-inline: 0.5em;
  transition: opacity 0.4s linear;
}
[data-switch-target] > [data-target].targeted {
  opacity: 1;
}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css" type="text/css">

<main>
  <fieldset data-switch-trigger data-id="012-3456-789">
    <label title="disabled">
      <input type="radio" name="target" data-target="disabled" />
      <i class="fa fa-times"></i>
    </label>
    <label title="default">
      <input type="radio" name="target" data-target="default" />
      <i class="fa fa-minus"></i>
    </label>
    <label title="enabled">
      <input type="radio" name="target" data-target="enabled" />
      <i class="fa fa-check"></i>
    </label>
  </fieldset>

  <div data-switch-target data-id="012-3456-789">
    <div data-target="disabled">disabled</div>
    <div data-target="default">default</div>
    <div data-target="enabled">enabled</div>
  </div>


  <fieldset data-switch-trigger data-id="987-6543-210">
    <label title="foo">
      <input type="radio" name="target" data-target="foo" />
      <i class="fa fa-times"></i>
    </label>
    <label title="bar">
      <input type="radio" name="target" data-target="bar" />
      <i class="fa fa-minus"></i>
    </label>
    <label title="baz">
      <input type="radio" name="target" data-target="baz" />
      <i class="fa fa-check"></i>
    </label>
  </fieldset>

  <div data-switch-target data-id="987-6543-210">
    <div data-target="foo">foo</div>
    <div data-target="bar">bar</div>
    <div data-target="baz">baz</div>
  </div>
</main>
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37