4

I'm creating a table with multiple rows, all having an "Options" button that is supposed to show a dropdown context menu. To keep the code shorter, I'm using a single div in order to reuse it as a common markup for the context menu.

I'm using Bootstrap 5.1.3 and jQuery 3.6.0. Following is my code:

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

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Test Code</title>
  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
</head>

<body>
  <table id="myTable" class="table table-hover">
    <thead>
      <tr>
        <th>#</th>
        <th>Document</th>
        <th>Reference</th>
        <th>Action</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>1</td>
        <td>General Policies</td>
        <td>GP-01-2022</td>
        <td>
          <div class="dropdown">
            <a href="#" class="btn btn-primary optionsButton" data-bs-toggle="dropdown" aria-expanded="false" id="doc1">Options</a>
          </div>
        </td>
      </tr>
      <tr>
        <td>2</td>
        <td>Training Material</td>
        <td>GP-02-2022</td>
        <td>
          <div class="dropdown">
            <a href="#" class="btn btn-primary optionsButton" data-bs-toggle="dropdown" aria-expanded="false" id="doc2">Options</a>
          </div>
        </td>
      </tr>
    </tbody>
  </table>

  <ul id="contextMenu" class="dropdown-menu">
    <li><a tabindex="-1" href="#" class="dropdown-item downloadLink">Download</a></li>
    <li><a tabindex="-1" href="#" class="dropdown-item propertiesLink">Properties</a></li>
  </ul>

  <script>
    //save the selector so you don't have to do the lookup everytime
    var $dropdown = $('#contextMenu');

    $('.optionsButton').click(function(event) {

      //get document ID
      var id = this.id;

      //move dropdown menu
      $(this).after($dropdown);

      //update links
      $dropdown.find(".downloadLink").attr("href", "/data/download?id=" + id);
      $dropdown.find(".propertiesLink").attr("href", "/data/viewproperties?id=" + id);

      //show dropdown
      $(this).dropdown();
    });
  </script>


</body>

</html>

In this code I'm facing two types of problems. Firstly, the dropdown menu isn't opening. When I inspect the code in Developer Mode, I can see that the jQuery script is successfully transferring the contextmenu DIV underneath the "Options" button so that it becomes nested as required by Bootstrap. But then the $(this).dropdown(); isn't opening the menu.

Second error is that in Developer Mode console, I see this error every time I click the 'Options' button:

dropdown.js:285 Uncaught TypeError: Failed to execute 'getComputedStyle' on 'Window': parameter 1 is not of type 'Element'.

And the stack trace of this error points to dropdown.js, does not specify where the error lies in my code.
Need help in trying to diagnose the issue here. I'm fairly new to Bootstrap and jQuery. Thanks.

0

3 Answers 3

5
+100

TL;DR: Don't move the #contextMenu anywhere. Read: Solution*


The error you're getting

dropdown.js:285 Uncaught TypeError: Failed to execute 'getComputedStyle' on 'Window': parameter 1 is not of type 'Element'.

is related to the Bootstrap (v5.1.3): dropdown.js code:

// @l62:
const SELECTOR_MENU = '.dropdown-menu'

// @l103:
  constructor(element, config) {
    super(element);
    //...
    this._menu = this._getMenuElement()
    //...
  }

// @l269:
  _getMenuElement() {
    return SelectorEngine.next(this._element, SELECTOR_MENU)[0]
  }

// @l285:
   _getPlacement() {
    // ...
    const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end'
    // ...
  }

here: SelectorEngine.next(this._element,

as you can see, there's no way to pass to the constructor another Element .menu-dropdown besides the one that BS hardcoded, and that's a next Sibling element in the Method _getMenuElement() ~line285.

BS assigns a "click" Event to every button with data-bs-toggle="dropdown" and blindly expects to toggle a next sibling Element dropdown — which does not actually exists (yet)!

I would either:

  • extend the class Dropdown manually, or
  • raise an Issue and create a pull-request to the related Bootstrap module

as a way to pass any desired Element as the this._menu; something like:

  constructor(element, config) {
    //...
    // Fix: allow for custom reusable menu Element
    this._menu = config.menuElement || this._getMenuElement()
    //...
  }

Disclaimer: There are some changes in the main branch regarding the above stripped-off code, I'm not sure if at the time of writing those issues were addressed.


In the meantime what you can simply do, without using the "mousedown" Event (to be one step ahead the BS's "click" event - like in this duplicate question's answer), and without using the silly Event.stopPropagation() (which should never be used, besides you really, really know what you're doing, or for debugging purpose only) — is:

Solution:

Don't move the UL#contextMenu using .after() or (with JS) .insertAdjacentElement(), rather, on instantiation of the extended Popper instances change the expected Bootstrap this._menu property to point to the desired reusable Element — your in-body "#contextMenu" like:

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

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Test Code</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
</head>

<body>
  <table id="myTable" class="table table-hover">
    <thead>
      <tr>
        <th>#</th>
        <th>Document</th>
        <th>Reference</th>
        <th>Action</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>1</td>
        <td>General Policies</td>
        <td>GP-01-2022</td>
        <td>
          <div class="dropdown">
            <button type="button" class="btn btn-primary optionsButton" data-bs-toggle="dropdown" aria-expanded="false" id="doc1">Options</button>
          </div>
        </td>
      </tr>
      <tr>
        <td>2</td>
        <td>Training Material</td>
        <td>GP-02-2022</td>
        <td>
          <div class="dropdown">
            <button type="button" class="btn btn-primary optionsButton" data-bs-toggle="dropdown" aria-expanded="false" id="doc2">Options</button>
          </div>
        </td>
      </tr>
    </tbody>
  </table>

  <ul id="contextMenu" class="dropdown-menu">
    <li><button type="button" tabindex="-1" class="dropdown-item downloadLink">Download</button></li>
    <li><button type="button" tabindex="-1" class="dropdown-item propertiesLink">Properties</button></li>
  </ul>

  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>

  <script>
    // DOM utility functions:
    const el = (sel, par) => (par || document).querySelector(sel);
    const els = (sel, par) => (par || document).querySelectorAll(sel);

    // Task: BS5 Popper fix for single static dropdown menu:
    const elDropdown = el('#contextMenu');
    const elsBtns = els(".optionsButton");
    const dropdownList = [...elsBtns].map(function(elBtn) {
      const instance = new bootstrap.Dropdown(elBtn);
      instance._menu = elDropdown;
      return instance;
    });
    // console.log(dropdownList); 
  </script>

</body>

</html>

The nice thing of the above is that there are no changes in the DOM that would trigger a reflow. The Popper code will calculate the best position of your floating contextMenu and call it job-done.
The not so nice thing is that special care should be given in the case you dynamically add TR elements to the Table; in the means that every newly added Button should be instantiated upon creation as a new bootstrap.Dropdown(elBtn)

Using "mousedown"

Another (not so good) solution to your original idea is to (unnecessarily) move the dropdown in DOM. It can be achieved using the "mousedown" Event, in order to move the dropdown "ahead-of-time" — before the BS's "click" event triggers (as suggested in this related question's answer). But such will not work correctly. Clicking one button after the other, a flash of content / glitch (of the actual dropdown) can be seen. There might be ways to mitigate the issue… but, why. Anyways, FYEO here's the code:

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

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Test Code</title>

  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
</head>

<body>
  <table id="myTable" class="table table-hover">
    <thead>
      <tr>
        <th>#</th>
        <th>Document</th>
        <th>Reference</th>
        <th>Action</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>1</td>
        <td>General Policies</td>
        <td>GP-01-2022</td>
        <td>
          <div class="dropdown">
            <button type="button" class="btn btn-primary optionsButton" data-bs-toggle="dropdown" aria-expanded="false" id="doc1">Options</button>
          </div>
        </td>
      </tr>
      <tr>
        <td>2</td>
        <td>Training Material</td>
        <td>GP-02-2022</td>
        <td>
          <div class="dropdown">
            <button type="button" class="btn btn-primary optionsButton" data-bs-toggle="dropdown" aria-expanded="false" id="doc2">Options</button>
          </div>
        </td>
      </tr>
    </tbody>
  </table>

  <ul id="contextMenu" class="dropdown-menu">
    <li><button type="button" tabindex="-1" class="dropdown-item downloadLink">Download</button></li>
    <li><button type="button" tabindex="-1" class="dropdown-item propertiesLink">Properties</button></li>
  </ul>

  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>

  <script>
    // DOM utility functions:
    const el = (sel, par) => (par || document).querySelector(sel);
    const els = (sel, par) => (par || document).querySelectorAll(sel);

    // Task: BS5 Popper fix for single static dropdown menu:
    const elDropdown = el('#contextMenu');
    const elsBtns = els(".optionsButton");

    const prepareDropdown = (evt) => {
      const elBtn = evt.currentTarget;
      elBtn.insertAdjacentElement("afterend", elDropdown);
    };

    elsBtns.forEach(elBtn => elBtn.addEventListener("mousedown", prepareDropdown));
  </script>


</body>

</html>

PS: use always <button type="button"> instead of Anchors (as in the examples above) if you don't need to navigate, but just a plain UI interaction button Element.

The way that BS uses and implements popups, selects etc. is kind of broken anyways. If a popup (or modal) is already opened, by clicking another button — the second (or the same) popup should be shown immediately (not after a second click). It's a UI/UX flaw in the design. Bootstrap often implements ideas quite oddly, but don't forget you can always help the Open Source community by providing a Pull Request.


If you're interested on how to create a (similar) Popup from scratch using JavaScript — you can find out more here: Show custom popup on mouse location.

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks for the very detailed answer. It clears my mystery of why errors were happening. Clearly there's something in BS 5 that's causing the error, because the same original code of mine seems to be working on a lower version of JSQuery and BS, but I cannot afford to downgrade at this moment. Thanks!
@FarazAzhar exactly, Kyle's answer on this other similar question works pretty fine with BS3. You're welcome.
I came across your nice solution while trying to brew something with jQuery (which did not work). I am trying to figure out how to extract data- attribute values and also the id value using your solution. I need to pass these values +id to the contextMenu buttons, so that they can be utilized accordingly. Would you have any idea how to do this? And can your solution be accomplished with jQuery?
@Sha this is a quite heavy request that goes out of the scope of this answer. Perhaps try to ask a New question. I'll be happy to take a look at it.
@RokoC.Buljan - I have posted a new question. If you find time, please take a look - would love your inputs: stackoverflow.com/questions/74719066/…
0

I have commented your loading of the bootstrap cdn and dealt with only with your actual Javascript.

You only had a logic to display the dropdown, albeit I changed it from $(this).dropdown(); to $dropdown.show(), because your dropdown is already tracked with this variable.

I have also implemented the toggling effect, that is, if your dropdown is opened, then we have the following scenarios:

  • you click somewhere else in the table, in which case the dropdown will be hidden
  • you click on the same button, in which case the dropdown will be hidden
  • you click on a similar button in another line, in which case the dropdown will be properly moved to the appropriate place using the appropriate parameters

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Test Code</title>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
</head>
<body>
    <table id="myTable" class="table table-hover" onclick="hideall()">
        <thead>
            <tr>
                <th>#</th>
                <th>Document</th>
                <th>Reference</th>
                <th>Action</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>1</td>
                <td>General Policies</td>
                <td>GP-01-2022</td>
                <td>
                    <div class="dropdown">
                        <a href="#" class="btn btn-primary optionsButton" aria-expanded="false" id="doc1">
                            Options
                        </a>
                    </div>
                </td>
            </tr>
            <tr>
                <td>2</td>
                <td>Training Material</td>
                <td>GP-02-2022</td>
                <td>
                    <div class="dropdown">
                        <a href="#" class="btn btn-primary optionsButton" aria-expanded="false" id="doc2">
                            Options
                        </a>
                    </div>
                </td>
            </tr>
        </tbody>
    </table>
    


    <ul id="contextMenu" class="dropdown-menu">
        <li><a tabindex="-1" href="#" class="dropdown-item downloadLink">Download</a></li>
        <li><a tabindex="-1" href="#" class="dropdown-item propertiesLink">Properties</a></li>
    </ul>
    
    <script>
        //save the selector so you don't have to do the lookup everytime
        var $dropdown = $('#contextMenu');
        var activeOptionsButton;

        $('.optionsButton').click(function(event) {
        
            event.stopPropagation();
            if (activeOptionsButton === this) {
                hideall();
                activeOptionsButton = undefined;
                return;
            }
            activeOptionsButton = this;
            //get document ID
            var id = this.id;
            
            //move dropdown menu
            $(this).after($dropdown);

            //update links
            $dropdown.find(".downloadLink").attr("href", "/data/download?id="+id);
            $dropdown.find(".propertiesLink").attr("href", "/data/viewproperties?id="+id);
            
            //show dropdown
            $dropdown.show();
        });
        
        function hideall() {
            $dropdown.hide();
        }
    
    </script>


</body>
</html>

12 Comments

Although your code works well, its not the solution for me. I cannot disable the Bootstrap's JS because it will break the rest of the UI (not part of this post). The $(this).dropdown(); is a Bootstrap function which is called on the container holding the context menu, not the context menu itself.
@FarazAzhar uncommented bootstrap and removed data-bs-toggle="dropdown" from the options buttons. Can you test again with the edited snippet. I think the current snippet either solves your problem or is very close to solving it.
Never use event.stopPropagation(). An app should always be able to be notified and eventually respond on any kind of Events both from your code or third-party. There's not a single valid point in using stopPropagation(). Also this example does not works properly.
@RokoC.Buljan have you tried to solve this without stopPropagation? Was it working? Can you show us the snipped/fiddle? Also, can you provide the error/glitch that you have seen testing my snippet? Do you have reproduction steps or other debugging details? Thanks!
On a second click the Dropdown does not open. Using a manual Query .show() and .hide() defies the purpose of the plugin - to internally handle the state. The plugin clearly got broken somewhere between Bootstrap version 3 and 5, see in this duplicate question. As soon you assign data-bs-toggle the Popper and BS logic becomes too invasive, not allowing (as far as I've tested and researched) to reuse a single dropdown as component. No, stopPropagation is still for the same reasons not a good solution.
|
-2

try this..

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Test Code</title>
  
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js" integrity="sha384-VHvPCCyXqtD5DqJeNxl2dtTyhF78xXNXdkwX1CZeRusQfRKp+tA7hAShOK/B/fQ2" crossorigin="anonymous"></script>
  
</head>
<body>
    <table id="myTable" class="table table-hover">
        <thead>
            <tr>
                <th>#</th>
                <th>Document</th>
                <th>Reference</th>
                <th>Action</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>1</td>
                <td>General Policies</td>
                <td>GP-01-2022</td>
                <td>
                    <div class="dropdown">
                        <a href="#" class="btn btn-primary optionsButton" data-bs-toggle="dropdown" aria-expanded="false" id="doc1">
                            Options
                        </a>
                    </div>
                </td>
            </tr>
            <tr>
                <td>2</td>
                <td>Training Material</td>
                <td>GP-02-2022</td>
                <td>
                    <div class="dropdown">
                        <a href="#" class="btn btn-primary optionsButton" data-bs-toggle="dropdown" aria-expanded="false" id="doc2">
                            Options
                        </a>
                    </div>
                </td>
            </tr>
        </tbody>
    </table>
    


    <ul id="contextMenu" class="dropdown-menu">
        <li><a tabindex="-1" href="#" class="dropdown-item downloadLink">Download</a></li>
        <li><a tabindex="-1" href="#" class="dropdown-item propertiesLink">Properties</a></li>
    </ul>
    
    <script>
        //save the selector so you don't have to do the lookup everytime
        var $dropdown = $('#contextMenu');

        $('.optionsButton').click(function(event) {
            
            //get document ID
            var id = this.id;
            
            //move dropdown menu
            $(this).after($dropdown);

            //update links
            $dropdown.find(".downloadLink").attr("href", "/data/download?id="+id);
            $dropdown.find(".propertiesLink").attr("href", "/data/viewproperties?id="+id);
            
            //show dropdown
            $(this).dropdown();
        });
    
    </script>


</body>
</html>

3 Comments

Thanks. You've downgraded the Bootstrap script version? I tried this code, the dropdown menu opens up but never closes again, even if clicked outside or on the button again.
pls check your js
My JS is right there in the post. I need help in diagnosing the issue with the JS, that's why I posted the question. Appreciate if you can identify what the problem with the JS is. Thanks.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.