Accordion in a chrome ext using vanilla JS

Viewed 88

I am looking to create a simple accordion within my chrome ext to display data. I am using the following JS tutorial but I seem to be struggling to register a click.

I have returned some data using the following:

//background.js

...

 // Looping through object's key value pair to place into divs
  for (const [key, value] of Object.entries(params)) {
    queryParams += `
      <div class="text-sm my-1">
        <span class="font-bold uppercase mr-1">${key}: </span><span class="font-normal font-mono capitalizev c-word-wrap">${value}</span>
      </div>
    `;
  }

return (completeData += `
    <div class="element my-3">

      <div id="click-parent" class="question flex justify-between px-6 py-4 bg-purple-500">
        <span class="text-base text-white font-bold">${pixelType}</span>
        <button id="click-btn">
          <i class="fas fa-plus-circle"></i>
        </button>
      </div>

<!-- OR -->
<div  class="question flex justify-between px-6 py-4 bg-purple-500">
        <span class="text-base text-white font-bold">${pixelType}</span>
        <button id="click-parent">
          <i id="click-btn" class="click-btn fas fa-plus-circle"></i>
        </button>
      </div>
<!-- end of other example -->

      <div class="answer hideText">
        <span id="pixel-url" class="c-word-wrap text-sm font-mono">${pixelUrl}</span>
          <span id="query-params">${queryParams}</span>
      </div>

    </div>
 
  `);
...

I then have my logic in a separate file

//popup.js

document.addEventListener('DOMContentLoaded', function () {
  // get data
  document.getElementById('getCurrentURL').addEventListener('click', function () {
    chrome.runtime.sendMessage({}, function (response) {
      document.getElementById('data').innerHTML = response;
    });
  });

  // accordion code
  const elements = document.querySelectorAll('.element');

  elements.forEach((element) => {
    let btn = element.querySelector('.question button');
    let icon = element.querySelector('.question button i');
    var answer = element.lastElementChild;
    var answers = document.querySelectorAll('.element .answer');

    btn.addEventListener('click', () => {
      alert('clicked ');
      console.log('clicked');

      answers.forEach((ans) => {
        let ansIcon = ans.parentElement.querySelector('button i');
        if (answer !== ans) {
          ans.classList.add('hideText');
          ansIcon.className = 'fas fa-plus-circle';
        }
      });

      answer.classList.toggle('hideText');
      icon.className === 'fas fa-plus-circle'
        ? (icon.className = 'fas fa-minus-circle')
        : (icon.className = 'fas fa-plus-circle');
    });
  });
});

I am loading my scrips in like so...

popup.html

<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="../main.css"/>

    <!-- TailWind CSS -->
    <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
  </head>
  <body class="flex flex-col min-h-screen">

    <main class="main flex-grow">

        <h1 class="text-center py-2 text-xl font-bold">HCP365 Debugger</h1>

        <section>
          <div id="data" class=""></div>
        </section>

    </main>

    <footer class="mb-4">
      <div class="flex justify-center">
        <button id="getCurrentURL" type="button" class="text-center text-base py-4 px-14 text-white font-bold drop-shadow bg-purple-500 capitalize outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150 hover:bg-purple-800">get url</button>
      </div>
    </footer>

    <!-- Scripts -->
    <script src="popup.js"></script>
    <script src="../background.js"></script>
    <!-- Accordion -->
    <script src="../fa.js"></script>
    <script src="../accordion.js"></script> 
  </body>
</html>

If anyone can guide me in where I am going wrong here that would be great. I have also tried replicating the tutorial outside of a chrome ext and it works perfectly

File Structure

.
├── README.md
├── accordion.js
├── background.js
├── fa.js
├── images
│   ├── icon_128.png
│   ├── icon_16.png
│   ├── icon_32.png
│   └── icon_48.png
├── main.css
├── manifest.json
└── popup
    ├── popup.html
    └── popup.js

Update

I have found a repo where someone has created a simple calc. Within their popup.js they had something similar to

document.getElementById('clickBtn').addEventListener('click', () => {
    document.getElementById('textChange').innerHTML = 'hello i was clicked';
  });

I had a div in popup.html as well as the button. This worked fine when I clicked it.

I went on to add it like so...

document.addEventListener('DOMContentLoaded', function () {
  document.getElementById('getCurrentURL').addEventListener('click', function () {
    chrome.runtime.sendMessage({}, function (response) {
      document.getElementById('data').innerHTML = response;
    });
  });

  // new code here --------------------------------
  document.getElementById('btn').addEventListener('click', () => {
    document.getElementById('textChange').innerHTML = 'hello i was clicked';
  });
  // end ---
});

Using the dynamic button...and it didn't work! This is beyond frustrating now...at least we know it's a issue with the dynamic content.

2 Answers

You might need to add the listener to the document only after DOMContent has completely loaded. In the case of dynamic elements, you might have to add the onClick listener on the container of the elements

See example below:

// wait for the DOM to be loaded
document.addEventListener('DOMContentLoaded', function() {

  var checkPageButton = document.getElementById('checkPage');

  // add listener only after DOM loaded
  checkPageButton.addEventListener('click', function() {
    alert('clicked');
  }, false);
}, false);
<!doctype html>
<html>

<head>
  <title>Tester</title>
  <script src="popup.js"></script>
</head>

<body>
  <h1>Tester</h1>
  <button id="checkPage">Test this now!</button>
</body>

</html>

WOOHOO!! Done it!

I realised something, when I was posting the function inside

document.addEventListener('DOMContentLoaded', function () { ...

I was getting undefined, then I realsed the .btn only appears on once the content has loaded which is within...

document.getElementById('getCurrentURL').addEventListener('click', function () {
    chrome.runtime.sendMessage({}, function (response) {
      document.getElementById('data').innerHTML = response;

So I placed it as it is below.

document.addEventListener('DOMContentLoaded', function () {
  document.getElementById('getCurrentURL').addEventListener('click', function () {
    chrome.runtime.sendMessage({}, function (response) {
      document.getElementById('data').innerHTML = response;

      // ----------------------------------------------------------------
      const elements = document.querySelectorAll('.element');

      elements.forEach((element) => {
        let btn = element.querySelector('.question button');
        let icon = element.querySelector('.question button i');
        var answer = element.lastElementChild;
        var answers = document.querySelectorAll('.element .answer');

        btn.addEventListener('click', () => {
          answers.forEach((ans) => {
            let ansIcon = ans.parentElement.querySelector('button i');
            if (answer !== ans) {
              ans.classList.add('hideText');
              ansIcon.className = 'fas fa-plus-circle';
            }
          });

          answer.classList.toggle('hideText');
          icon.className === 'fas fa-plus-circle'
            ? (icon.className = 'fas fa-minus-circle')
            : (icon.className = 'fas fa-plus-circle');
        });
      });
      // end ---
    });
  });
});

and everything works as intended. Although I had to use let btn = element.querySelector('.question button'); instead of getting the button via it's ID as it would only work once

Related