13

I've seen the documentation for the dropdown menu as component and separately using javascript.

I'm wondering if it is possible to add a single dropdown menu in the website's body (absoluted positioned relative to the clickable button element).

Why?

  • Because if I have a table with 500 rows I do not want to add the same list of 10 items 500 times making the resulting HTML bigger and slower when dealing with JS.

  • Because the parent element can be hidden but I still want the dropdown menu to be visible until they click outside it unfocusing it.

I found more people asking for this feature but I couldn't find anything in the docs about it.

Alvaro
  • 40,778
  • 30
  • 164
  • 336

5 Answers5

31

As the bootstrap documents say, there are no options for dropdown menus... This is sad, but it means there is currently not a 'bootstrap' solution for the functionality you want. There is now, however, a solution in the Angular-UI/Bootstrap kit if you're using that. The ticket you referenced is closed because it was finally added to the Angular-UI as of July 15th 2015.

All you have to do is 'Add dropdown-append-to-body to the dropdown element to append to the inner dropdown-menu to the body. This is useful when the dropdown button is inside a div with overflow: hidden, and the menu would otherwise be hidden.' (reference)

<div class="btn-group" dropdown dropdown-append-to-body>
  <button type="button" class="btn btn-primary dropdown-toggle" dropdown-toggle>Dropdown on Body <span class="caret"></span>
  </button>
  <ul class="dropdown-menu" role="menu">
    <li><a href="#">Action</a></li>
    <li><a href="#">Another action</a></li>
    <li><a href="#">Something else here</a></li>
    <li class="divider"></li>
    <li><a href="#">Separated link</a></li>
  </ul>
</div>

Hope this helps!


EDIT

In an effort to answer another SO question, I found a solution that works pretty well if you weren't using Angular-UI. It may be 'hacky', but it doesn't break the bootstrap menu functionality, and it seems to play well with most use cases I've used it for.

So I'll leave a few fiddles in case anyone else sees this and is interested. The first illustrates why the use of a body appended menu might be nice, the second shows the working solution:

Problem FIDDLE

The problem: a select dropdown within a panel body

<div class="panel panel-default">
  <div class="panel-body">
    <div class="btn-group">
      <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
        <span data-bind="label">Select One</span>&nbsp;<span class="caret"></span>
      </button>
      <ul class="dropdown-menu" role="menu">
        <li><a href="#">Item 1</a></li>
        <li><a href="#">Another item</a></li>
        <li><a href="#">This is a longer item that will not fit properly</a></li>
      </ul>
    </div>
  </div>
</div>

Solution FIDDLE

(function () {
    // hold onto the drop down menu                                             
    var dropdownMenu;

    // and when you show it, move it to the body                                     
    $(window).on('show.bs.dropdown', function (e) {

        // grab the menu        
        dropdownMenu = $(e.target).find('.dropdown-menu');

        // detach it and append it to the body
        $('body').append(dropdownMenu.detach());

        // grab the new offset position
        var eOffset = $(e.target).offset();

        // make sure to place it where it would normally go (this could be improved)
        dropdownMenu.css({
            'display': 'block',
                'top': eOffset.top + $(e.target).outerHeight(),
                'left': eOffset.left
        });
    });

    // and when you hide it, reattach the drop down, and hide it normally                                                   
    $(window).on('hide.bs.dropdown', function (e) {
        $(e.target).append(dropdownMenu.detach());
        dropdownMenu.hide();
    });
})();

EDIT I finally found where I originally found this solution. Gotta give credit where credit is due!

Community
  • 1
  • 1
Jonathan
  • 1,126
  • 10
  • 19
  • Thanks for the reply. [I asked it on bootstrap issues forum](https://github.com/twbs/bootstrap/issues/17537) and got a pretty decent replay. I closed the topic but they opened it again, so it seems they might consider having a look at it! – Alvaro Sep 11 '15 at 16:36
  • I see why you need it now! The Angular-UI solution may not work for that then... but the 'hacky' solution will. And it shouldn't break anything. Hopefully Bootstrap gets with the program and gives us what we want! It would be a nice feature to have! – Jonathan Sep 11 '15 at 16:46
  • Also, your use of the bootstrap menu for contextmenu events is slick! I'll have to remember that for future use! – Jonathan Sep 11 '15 at 16:48
  • 1
    The only minor problem with this solution is that if it's possible to cause the original container to move while the dropdown is open (or resize, etc) then the menu won't move with it. Shouldn't happen often, though... – Coderer Nov 25 '16 at 08:05
  • This is the solution I wanted to use but the placement (set thecss) of the ddropdown by the lib itself happens after the event has been called overriding any css I may set even when using the shown event instead of show – Noman_1 Apr 21 '21 at 07:55
1

For those like me who have the same issue using Angular 6+ and Bootstrap 4+, I wrote a small directive to append the dropdown to the body :

events.ts

/**
 * Add a jQuery listener for a specified HTML event.
 * When an event is received, emit it again in the standard way, and not using jQuery (like Bootstrap does).
 *
 * @param event Event to relay
 * @param node HTML node (default is body)
 *
 * https://stackoverflow.com/a/24212373/2611798
 * https://stackoverflow.com/a/46458318/2611798
 */
export function eventRelay(event: any, node: HTMLElement = document.body) {
    $(node).on(event, (evt: any) => {
        const customEvent = document.createEvent("Event");
        customEvent.initEvent(event, true, true);
        evt.target.dispatchEvent(customEvent);
    });
}

dropdown-body.directive.ts

import {Directive, ElementRef, AfterViewInit, Renderer2} from "@angular/core";
import {fromEvent} from "rxjs";

import {eventRelay} from "../shared/dom/events";

/**
 * Directive used to display a dropdown by attaching it as a body child and not a child of the current node.
 *
 * Sources :
 * <ul>
 *  <li>https://getbootstrap.com/docs/4.1/components/dropdowns/</li>
 *  <li>https://stackoverflow.com/a/42498168/2611798</li>
 *  <li>https://github.com/ng-bootstrap/ng-bootstrap/issues/1012</li>
 * </ul>
 */
@Directive({
    selector: "[appDropdownBody]"
})
export class DropdownBodyDirective implements AfterViewInit {

    /**
     * Dropdown
     */
    private dropdown: HTMLElement;

    /**
     * Dropdown menu
     */
    private dropdownMenu: HTMLElement;

    constructor(private readonly element: ElementRef, private readonly renderer: Renderer2) {
    }

    ngAfterViewInit() {
        this.dropdown = this.element.nativeElement;
        this.dropdownMenu = this.dropdown.querySelector(".dropdown-menu");

        // Catch the events using observables
        eventRelay("shown.bs.dropdown", this.element.nativeElement);
        eventRelay("hidden.bs.dropdown", this.element.nativeElement);

        fromEvent(this.element.nativeElement, "shown.bs.dropdown")
            .subscribe(() => this.appendDropdownMenu(document.body));
        fromEvent(this.element.nativeElement, "hidden.bs.dropdown")
            .subscribe(() => this.appendDropdownMenu(this.dropdown));
    }

    /**
     * Append the dropdown to the "parent" node.
     *
     * @param parent New dropdown parent node
     */
    protected appendDropdownMenu(parent: HTMLElement): void {
        this.renderer.appendChild(parent, this.dropdownMenu);
    }
}

dropdown-body.directive.spec.ts

import {Component, DebugElement} from "@angular/core";
import {By} from "@angular/platform-browser";
import {from} from "rxjs";

import {TestBed, ComponentFixture, async} from "@angular/core/testing";

import {DropdownBodyDirective} from "./dropdown-body.directive";

@Component({
    template: `<div class="btn-group dropdown" appDropdownBody>
        <button id="openBtn" data-toggle="dropdown">open</button>
        <div class="dropdown-menu">
            <button class="dropdown-item">btn0</button>
            <button class="dropdown-item">btn1</button>
        </div>
    </div>`
})
class DropdownContainerTestingComponent {
}

describe("DropdownBodyDirective", () => {

    let component: DropdownContainerTestingComponent;
    let fixture: ComponentFixture<DropdownContainerTestingComponent>;
    let dropdown: DebugElement;
    let dropdownMenu: DebugElement;

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            declarations: [
                DropdownContainerTestingComponent,
                DropdownBodyDirective,
            ]
        });
    }));

    beforeEach(() => {
        fixture = TestBed.createComponent(DropdownContainerTestingComponent);
        component = fixture.componentInstance;
        dropdown = fixture.debugElement.query(By.css(".dropdown"));
        dropdownMenu = fixture.debugElement.query(By.css(".dropdown-menu"));
    });

    it("should create an instance", () => {
        fixture.detectChanges();
        expect(component).toBeTruthy();

        expect(dropdownMenu.parent).toEqual(dropdown);
    });

    it("not shown", () => {
        fixture.detectChanges();

        expect(dropdownMenu.parent).toEqual(dropdown);
    });

    it("show then hide", () => {
        fixture.detectChanges();
        const nbChildrenBeforeShow = document.body.children.length;

        expect(dropdownMenu.parent).toEqual(dropdown);

        // Simulate the dropdown display event
        dropdown.nativeElement.dispatchEvent(new Event("shown.bs.dropdown"));
        fixture.detectChanges();

        from(fixture.whenStable()).subscribe(() => {
            // Check the dropdown is attached to the body
            expect(document.body.children.length).toEqual(nbChildrenBeforeShow + 1);
            expect(dropdownMenu.nativeElement.parentNode.outerHTML)
                .toBe(document.body.outerHTML);

            // Hide the dropdown
            dropdown.nativeElement.dispatchEvent(new Event("hidden.bs.dropdown"));
            fixture.detectChanges();

            from(fixture.whenStable()).subscribe(() => {
                // Check the dropdown is back to its original node
                expect(document.body.children.length).toEqual(nbChildrenBeforeShow);
                expect(dropdownMenu.nativeElement.parentNode.outerHTML)
                    .toBe(dropdown.nativeElement.outerHTML);
            });
        });
    });
});
Junior Dussouillez
  • 2,327
  • 3
  • 30
  • 39
1

A small modifiaction to @Jonathan answer in order to work with Bootstrap 5 and the latest version of Datatables while using the table-responsive class of Bootstrap to make the table responsive (See in full page mode for full effect):

$(function() {
  $(document).ready(function() {
    $("#example").DataTable({});
  });

  let parents = [];
  let menus = [];

  // and when you show it, move it to the body
  $(window).on('show.bs.dropdown', function(e) {

    let target = $(e.target);

    // save the parent
    parents.push(target.parent());

    // grab the menu
    let dropdownMenu = target.next();

    // save the menu
    menus.push(dropdownMenu);

    // detach it and append it to the body
    $('body').append(dropdownMenu.detach());

    // grab the new offset position
    let eOffset = target.offset();

    // make sure to place it where it would normally go (this could be improved)
    dropdownMenu.css({
      'display': 'block',
      'top': eOffset.top + target.outerHeight(),
      'left': eOffset.left
    });
  });

  // and when you hide it, reattach the drop down, and hide it normally
  $(window).on('hide.bs.dropdown', function(e) {

    menus.forEach(function(element, index) {
      let parent = parents[index];
      let dropdownMenu = element;

      parent.append(dropdownMenu.detach());
      dropdownMenu.hide();

      menus.splice(index, 1);
      parents.splice(index, 1);
    })
  });
});
body {
  display: flex;
  height: 25rem;
  width: 100%;
}

#card {
  width: 50%;
  margin: auto;
}
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3" crossorigin="anonymous"></script>
<link href="//cdn.datatables.net/1.12.1/css/jquery.dataTables.min.css" rel="stylesheet" />
<script src="//cdn.datatables.net/1.12.1/js/jquery.dataTables.min.js"></script>

<html>

<body>
  <div class="card" id="card">
    <div class="card-body">
      <div class="table-responsive">
        <table id="example" class="display responsive nowrap" width="100%">
          <thead>
            <tr>
              <th>Name</th>
              <th>Position</th>
              <th>Office</th>
              <th>Age</th>
              <th>Start date</th>
              <th>Stuff 1</th>
              <th>Stuff 2</th>
              <th>Stuff 3</th>
              <th>Stuff 4</th>
              <th>Stuff 5</th>
              <th>Stuff 6</th>
              <th>Stuff 7</th>
              <th>Stuff 8</th>
              <th>Actions</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>Tiger Nixon</td>
              <td>System Architect</td>
              <td>Edinburgh</td>
              <td>61</td>
              <td>2011-04-25</td>
              <td>2011-04-25</td>
              <td>2011-04-25</td>
              <td>2011-04-25</td>
              <td>2011-04-25</td>
              <td>2011-04-25</td>
              <td>2011-04-25</td>
              <td>2011-04-25</td>
              <td>2011-04-25</td>
              <td>
                <div class="dropdown">
                  <button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
                    Dropdown button
                  </button>
                  <ul class="dropdown-menu">
                    <li><a class="dropdown-item" href="#">Action</a></li>
                    <li><a class="dropdown-item" href="#">Another action</a></li>
                    <li><a class="dropdown-item" href="#">Something else here</a></li>
                    <li><a class="dropdown-item" href="#">Action</a></li>
                    <li><a class="dropdown-item" href="#">Another action</a></li>
                    <li><a class="dropdown-item" href="#">Something else here</a></li>
                    <li><a class="dropdown-item" href="#">Action</a></li>
                    <li><a class="dropdown-item" href="#">Another action</a></li>
                    <li><a class="dropdown-item" href="#">Something else here</a></li>
                  </ul>
                </div>
              </td>
            </tr>
            <tr>
              <td>Tiger Nixon</td>
              <td>System Architect</td>
              <td>Edinburgh</td>
              <td>61</td>
              <td>2011-04-25</td>
              <td>2011-04-25</td>
              <td>2011-04-25</td>
              <td>2011-04-25</td>
              <td>2011-04-25</td>
              <td>2011-04-25</td>
              <td>2011-04-25</td>
              <td>2011-04-25</td>
              <td>2011-04-25</td>
              <td>
                <div class="dropdown">
                  <button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
                    Dropdown button
                  </button>
                  <ul class="dropdown-menu">
                    <li><a class="dropdown-item" href="#">Action</a></li>
                    <li><a class="dropdown-item" href="#">Another action</a></li>
                    <li><a class="dropdown-item" href="#">Something else here</a></li>
                    <li><a class="dropdown-item" href="#">Action</a></li>
                    <li><a class="dropdown-item" href="#">Another action</a></li>
                    <li><a class="dropdown-item" href="#">Something else here</a></li>
                    <li><a class="dropdown-item" href="#">Action</a></li>
                    <li><a class="dropdown-item" href="#">Another action</a></li>
                    <li><a class="dropdown-item" href="#">Something else here</a></li>
                  </ul>
                </div>
              </td>
            </tr>
          </tbody>
          <tfoot>
            <tr>
              <th>Name</th>
              <th>Position</th>
              <th>Office</th>
              <th>Age</th>
              <th>Start date</th>
              <th>Stuff 1</th>
              <th>Stuff 2</th>
              <th>Stuff 3</th>
              <th>Stuff 4</th>
              <th>Stuff 5</th>
              <th>Stuff 6</th>
              <th>Stuff 7</th>
              <th>Stuff 8</th>
              <th>Actions</th>
            </tr>
          </tfoot>
        </table>
      </div>
    </div>
  </div>
</body>

</html>
HMZ
  • 2,949
  • 1
  • 19
  • 30
0

Not sure about bootstrap 3, but if you're using bootstrap 4, you can add "data-boundary="window" to the dropdown trigger. It will append it to the body and then you can position it using absolute positioning.

Charraye
  • 79
  • 2
  • I don't really think so, since it's only adding one attribute to the html element, no additional js needed other than what's already in bootstrap. – Charraye Aug 30 '19 at 18:36
0

Using bootstrap 4, you can place the dropdown outside like this:

<link href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.bundle.min.js"></script>

<!-- Assuming a big table -->
<table>
  <tr>
    <td>
      <!-- data-target is a selector -->
      <a href="#" role="button" data-toggle="dropdown" data-target="#dropdown-container" aria-haspopup="true" aria-expanded="false">
            O
          </a>
    </td>
  </tr>
</table>
<div id="dropdown-container">
  <div id="dropdown-menu" class="dropdown-menu">
    <a class="dropdown-item" href="#">Action</a>
    <a class="dropdown-item" href="#">Another action</a>
    <a class="dropdown-item" href="#">Something else here</a>
  </div>
</div>

I had to have a container as target to make the Popper point to the right container. If you have issues with placement, let a comment, I will add a more complex solution i had to implement overrinding the Popper placement.

Noman_1
  • 473
  • 1
  • 6
  • 17