Toggle icon class across all the rows in the table

Viewed 135

In this javascript code below there are two icons. fa-circle-pause and fa-circle-play.

When a user clicks the play icon in any one of the rows in the table, it is updated with the icon fa-circle-pause. I have been able to do that for the single rows.

But I am not able to update the rest of the rows in the table with the icon fa-circle-play.

How can I do that using just the javascript?

const data = [{
  Name: 'Chapter 1',
  TrackId: 1
}, {
  Name: 'Chapter 2',
  TrackId: '2',
}, , {
  Name: 'Chapter 3',
  TrackId: '3',
}, , {
  Name: 'Chapter 4',
  TrackId: '4',
}]; // any json data or array of objects
const tableData = data.map(function(value) {
  return (
    `<tr>
           <th scope="row">
            <span data-track-id='${value.TrackId}' class="play-button btn">
                <i class="fa-solid fa-circle-play"></i>
            </span>
          </th>
          <td>${value.Name}</td>
     </tr>`
  );
}).join('');
const tabelBody = document.querySelector("#tableBody");
tableBody.innerHTML = tableData;

const pauseIconClassName = 'fa-circle-pause'
const playIconClassName = 'fa-circle-play'

const btns = document.querySelectorAll('.play-button')

function onChange(event) {
  // get the button elememt from the event
  const buttonElement = event.currentTarget.querySelector('i');

  const isPlayButton = buttonElement.classList.contains(playIconClassName)

  if (isPlayButton) {
    buttonElement.classList.remove(playIconClassName)
    buttonElement.classList.add(pauseIconClassName)
  } else {
    buttonElement.classList.remove(pauseIconClassName)
    buttonElement.classList.add(playIconClassName)
  }
}

btns.forEach(btn => {
  btn.addEventListener('click', onChange)
})
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet" />

<table border="2">
  <thead class="thead-dark">
    <tr>
      <th scope="col">#</th>
      <th scope="col">Track</th>
    </tr>
  </thead>
  <tbody id="tableBody">

  </tbody>
</table>

3 Answers

Simply iterate through all the buttons of the table. While looping, check to see if it's the button we clicked on, if it isn't we can change the class accordingly.

const data = [{
  Name: 'Chapter 1',
  TrackId: 1
}, {
  Name: 'Chapter 2',
  TrackId: '2',
}, , {
  Name: 'Chapter 3',
  TrackId: '3',
}, , {
  Name: 'Chapter 4',
  TrackId: '4',
}]; // any json data or array of objects
const tableData = data.map(function(value) {
  return (
    `<tr>
           <th scope="row">
            <span data-track-id='${value.TrackId}' class="play-button btn">
                <i class="fa-solid fa-circle-play"></i>
            </span>
          </th>
          <td>${value.Name}</td>
     </tr>`
  );
}).join('');
const tabelBody = document.querySelector("#tableBody");
tableBody.innerHTML = tableData;

const pauseIconClassName = 'fa-circle-pause'
const playIconClassName = 'fa-circle-play'

const btns = document.querySelectorAll('.play-button')

function onChange(event) {
  // get the button element from the event
  const buttonElement = event.currentTarget.querySelector('i');

  const isPlayButton = buttonElement.classList.contains(playIconClassName)

  if (isPlayButton) {
    buttonElement.classList.remove(playIconClassName)
    buttonElement.classList.add(pauseIconClassName)
  } else {
    buttonElement.classList.remove(pauseIconClassName)
    buttonElement.classList.add(playIconClassName)
  }
  
  // Find all the buttons
  let buttons = tabelBody.querySelectorAll('i.fa-solid');
  
  // Loop through all buttons
  for(let button of buttons)
  {
    if(button !== buttonElement) // Is this the button we clicked on?
    {
      // If not, reset back to play
      button.classList.remove(pauseIconClassName)
      button.classList.add(playIconClassName)
    }
  }
}

btns.forEach(btn => {
  btn.addEventListener('click', onChange)
})
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet" />

<table border="2">
  <thead class="thead-dark">
    <tr>
      <th scope="col">#</th>
      <th scope="col">Track</th>
    </tr>
  </thead>
  <tbody id="tableBody">

  </tbody>
</table>

There were a few required changes, like looping over the buttons in the onChange handler and changing the class of the button child i, which had the required classNames

const data = [{
  Name: 'Chapter 1',
  TrackId: 1
}, {
  Name: 'Chapter 2',
  TrackId: '2',
}, , {
  Name: 'Chapter 3',
  TrackId: '3',
}, , {
  Name: 'Chapter 4',
  TrackId: '4',
}]; // any json data or array of objects
const tableData = data.map(function(value) {
  return (
    `<tr>
           <th scope="row">
            <span data-track-id='${value.TrackId}' class="play-button btn">
                <i class="fa-solid fa-circle-play"></i>
            </span>
          </th>
          <td>${value.Name}</td>
     </tr>`
  );
}).join('');
const tabelBody = document.querySelector("#tableBody");
tableBody.innerHTML = tableData;

const pauseIconClassName = 'fa-circle-pause'
const playIconClassName = 'fa-circle-play'

const btns = document.querySelectorAll('.play-button')

function playButton(btn) {
  const i = btn.children[0]
  i.classList.remove(playIconClassName)
  i.classList.add(pauseIconClassName)
}

function pauseButton(btn) {
  const i = btn.children[0]
  i.classList.remove(pauseIconClassName)
  i.classList.add(playIconClassName)
}

function onChange(event) {
  // get the button elememt from the event
  const buttonElement = event.currentTarget;
  const isPlayButton = buttonElement.querySelector("i").classList.contains(playIconClassName)

  // Pause all the buttons
  btns.forEach(btn => pauseButton(btn))
  
  // Play current button if needed
  if (isPlayButton) {
    playButton(buttonElement)
  }
}

btns.forEach(function(btn) {
  btn.addEventListener('click', onChange)
})
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet" />

<table border="2">
  <thead class="thead-dark">
    <tr>
      <th scope="col">#</th>
      <th scope="col">Track</th>
    </tr>
  </thead>
  <tbody id="tableBody">

  </tbody>
</table>

Update

"...how do I get the data-track-id of the icon that is active?"

There's a test added that finds .fa-circle-pause.parentElement.dataset.trackId The parts of this test is separated from the real code, just remove or omit the sections commented with the following title: "For Demo Purposes" when you no longer need it.

Changes

  • All <span>s are changed into <a> because they are focusable. This a minor change and is cosmetic so functionality isn't affected if you prefer <span>. Throughout this answer I will be mentioning a "button" -- to clarify the <a> (or if you prefer <span>) is the "button".

  • Bootstrap 5 stylesheet and script has been added. It looks like you might be using Bootstrap so I added it. The JavaScript is not dependent on the BS script (it's not even used, I just added it for the sake of completeness).

  • Instead of binding each button to a click event, only one event listener is registered on <tbody>. This setup provides a few advantages:

    • Anything within the <tbody> has the potential to trigger a click event -- sounds messy but it won't be as long as the event handler (the function that is called when an event is triggered) only handles the elements needed to react to an event and then ignores the rest.

    • Any element added to <tbody> dynamically later on can also trigger events if the event handler allows it to. This "blanket" rule covers as many elements we want as long as they are within <tbody>. Normally, newly added elements will not have any event handler until new event handlers are explicitly bound to them.

    • This is done through one event listener for an unlimited number of elements that are static (ie being there when the page was loaded) and those that are dynamic as well. The only disadvantage is that we rely on event bubbling, so when a button triggers an event, the ancestor element (<tbody>) will call the event handler. There are some events that do not bubble (blur, focus, mouseenter, mouseleave, to name a few), for those you'll need to bind the event to individual elements.

  • See events and event delegation.

The event handler, switchBtn(event) is designed to:

  • Reference the button that the user clicked
    const clicked = event.target;
    
  • Make sure that the only element to react to being clicked is a button
    if (clicked.matches('.play-button .fa-solid')) {//...
    
  • Find the button with .fa-circle-pause class and toggle the .fa-circle-pause/play classes of the clicked element
    let active = this.querySelector('.fa-circle-pause');
    clicked.classList.toggle('fa-circle-play');
    clicked.classList.toggle('fa-circle-pause');
    
  • Then finally, remove .fa-circle-pause class and add the .fa-circle-play class to the button that has the .fa-circle-pause class before if it isn't the button clicked.
    if (active && active !== clicked) {
      active.classList.remove('fa-circle-pause');
      active.classList.add('fa-circle-play');
    }
    
  • The creation of table rows is wrapped in a function called: createRows(objArray). Two major changes are:
    • .forEach() replaces .map() since an array of buttons is not needed because only one event listener is needed and the event handler doesn't need a loop either since it finds and accesses the "active" button directly.
    • .insertAdjacentHTML() replaces .innerHTML. .iAH is .innerHTML on steroids. We can direct it to add htmlString in front ("beforebegin"), behind ("afterend"), inside as the first child ("afterbegin"), or inside as last child ("beforeend") of a given element without destroying anything in the DOM (which .innerHTML does with great vigor). With this change, createRows() is reusable.

Forgot to mention that event.preventDefault() was added to switchBtn(). This is just to stop <a> from jumping when clicked, if you switch back to <span> then it's no longer needed.

The objective of the question was to make the buttons behave like radio buttons in that if one is active the others are not. This was accomplished, moreover, it also toggles any button as well.

  • To test click a button then click a different button: Only one button stays active (.fa-circle-pause).

  • Click a button and then click the same button again: The button toggle between states without the other buttons reacting.

Example

<!DOCTYPE html>
<html lang="en">

<head>
  <title></title>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css" rel="stylesheet">
  <style>
    .vertical-headers {width: 2.5em;}
    .play-button {color: black}
    .play-button:focus .fa-solid {color: #007ff}
    tr:focus-within {background-color: rgba(0, 55, 255, 0.2)}
    /* For Demo Purposes */
    .testBtn {margin-top: 40%;}
    .testBtn::before {content: attr(value)}
    /*------------------*/
  </style>
</head>

<body>
  <main class="container">
    <section class="row">
      <div class='col'>
        <table class='table table-sm align-middle table-hover table-bordered'>
          <colgroup>
            <col class='vertical-headers'>
            <col class='chapters'>
          </colgroup>
          <thead class="table-dark">
            <tr>
              <th scope="col" class='text-center'>#</th>
              <th scope="col">Track</th>
            </tr>
          </thead>
          <tbody>

          </tbody>
        </table>
      </div>
      <!-- For Demo Purposes -->
      <div class='col'>
        <button class='testBtn btn btn-success' value='X'></button>
      </div>
      <!---------------------->
    </section>
  </main>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
  <script>
    const data = [{
      Name: "Chapter 1",
      TrackId: 1
    }, {
      Name: "Chapter 2",
      TrackId: 2
    }, {
      Name: "Chapter 3",
      TrackId: 3
    }, {
      Name: "Chapter 4",
      TrackId: 4
    }];
    /* For Demo Purposes */
    document.querySelector('.testBtn').onclick = (e) => {
      let active = document.querySelector('.fa-circle-pause');
      if (active) {
        e.target.value = active.parentElement.dataset.trackId ;
      }
    }
    /*------------------*/
    
    const panel = document.querySelector('tbody');

    createRows(data);

    panel.addEventListener('click', switchBtn);

    function switchBtn(event) {
      event.preventDefault();
      const clicked = event.target;
      if (clicked.matches('.play-button .fa-solid')) {
        let active = this.querySelector('.fa-circle-pause');
        clicked.classList.toggle('fa-circle-play');
        clicked.classList.toggle('fa-circle-pause');
        if (active && active !== clicked) {
          active.classList.remove('fa-circle-pause');
          active.classList.add('fa-circle-play');
        }
      }
    }

    function createRows(objArray) {
      objArray.forEach((chapter, index) => {
        let row = `<tr><th scope="row">
     <a href='#' data-track-id="${chapter.TrackId}" class="play-button">
     <i class="fa-solid fa-circle-play fa-2x"></i></a></th><td>${chapter.Name}</td></tr>`;
        panel.insertAdjacentHTML('beforeend', row);
      });
    }
  </script>
</body>

</html>

Related