19 thoughts on “Dropdown append to body

  1. This workaround breaks tab navigation and accessibility. We still very much need an official solution. Can’t believe this is still not a thing.

  2. We use bootstrap 3.2 and use custom directive. Maybe it’ll help someone.

    import { Directive, forwardRef, Inject, OnDestroy } from '@angular/core';
    import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap';
    import { Subscription } from 'rxjs';
    import { positionElements } from '../positioning';
    
    @Directive({
        selector: '[ngbDropdown][appDropdownAppendToBody]'
    })
    export class DropdownAppendToBodyDirective implements OnDestroy {
    
        private onChangeSubscription: Subscription;
    
        constructor(@Inject(forwardRef(() => NgbDropdown)) private dropdown: NgbDropdown) {
    
            this.onChangeSubscription = this.dropdown.openChange.subscribe((open: boolean) => {
                this.dropdown['_menu'].position = (triggerEl: HTMLElement, placement: string) => {
                    if (!this.isInBody()) {
                        this.appendMenuToBody();
                    }
                    positionElements(triggerEl, this.dropdown['_menu']['_elementRef'].nativeElement, placement, true);
                };
    
                if (open) {
                    if (!this.isInBody()) {
                        this.appendMenuToBody();
                    }
                } else {
                    setTimeout(() => this.removeMenuFromBody());
                }
            });
        }
    
        ngOnDestroy() {
            this.removeMenuFromBody();
            if (this.onChangeSubscription) {
                this.onChangeSubscription.unsubscribe();
            }
        }
    
        private isInBody() {
            return this.dropdown['_menu']['_elementRef'].nativeElement.parentNode === document.body;
        }
    
        private removeMenuFromBody() {
            if (this.isInBody()) {
                window.document.body.removeChild(this.dropdown['_menu']['_elementRef'].nativeElement);
            }
        }
    
        private appendMenuToBody() {
            window.document.body.appendChild(this.dropdown['_menu']['_elementRef'].nativeElement);
        }
    }
    
    <div ngbDropdown appDropdownAppendToBody  [placement]="['bottom-right', 'top-right']">
        <button ngbDropdownToggle></button>
        <div ngbDropdownMenu>
            ..
        </div>
    </div>
    

    We had to move positioning.ts to out app because it wasn’t compiled.

  3. Using bootstrap 4 and related ngBootstrap, still no support for this. Maybe make this an higher priority after 2 years of this issue being open?

    We have come up with a custom directive to reposition the ngbDropdown menu under the body element instead.

    Here is the directive code:

    [...]
    export class DropdownPositionDirective implements AfterContentInit, OnDestroy {
    [...]

    And use it like this:

    <div ngbDropdown ngbDropdownReposition>
     <!-- Rest of the dropdown code -->
    </div>
    

    Hope it helps 🙂

    This solution works in putting the div under body but make attributes of the original directive not work anymore. Specifically placement don’t work.

    Also the div don’t follow the button anymore if you scroll the page, it stay in the place it has opened (which is a problem shared by the original NgbTooltip directive so, not really an issue with the code that the good @mcorcuera provided.

  4. We use bootstrap 3.2 and use custom directive. Maybe it’ll help someone.

    import { Directive, forwardRef, Inject, OnDestroy } from '@angular/core';
    import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap';
    import { Subscription } from 'rxjs';
    import { positionElements } from '../positioning';
    
    @Directive({
        selector: '[ngbDropdown][appDropdownAppendToBody]'
    })
    export class DropdownAppendToBodyDirective implements OnDestroy {
    
        private onChangeSubscription: Subscription;
    
        constructor(@Inject(forwardRef(() => NgbDropdown)) private dropdown: NgbDropdown) {
    
            this.onChangeSubscription = this.dropdown.openChange.subscribe((open: boolean) => {
                this.dropdown['_menu'].position = (triggerEl: HTMLElement, placement: string) => {
                    if (!this.isInBody()) {
                        this.appendMenuToBody();
                    }
                    positionElements(triggerEl, this.dropdown['_menu']['_elementRef'].nativeElement, placement, true);
                };
    
                if (open) {
                    if (!this.isInBody()) {
                        this.appendMenuToBody();
                    }
                } else {
                    setTimeout(() => this.removeMenuFromBody());
                }
            });
        }
    
        ngOnDestroy() {
            this.removeMenuFromBody();
            if (this.onChangeSubscription) {
                this.onChangeSubscription.unsubscribe();
            }
        }
    
        private isInBody() {
            return this.dropdown['_menu']['_elementRef'].nativeElement.parentNode === document.body;
        }
    
        private removeMenuFromBody() {
            if (this.isInBody()) {
                window.document.body.removeChild(this.dropdown['_menu']['_elementRef'].nativeElement);
            }
        }
    
        private appendMenuToBody() {
            window.document.body.appendChild(this.dropdown['_menu']['_elementRef'].nativeElement);
        }
    }
    
    <div ngbDropdown appDropdownAppendToBody  [placement]="['bottom-right', 'top-right']">
        <button ngbDropdownToggle></button>
        <div ngbDropdownMenu>
            ..
        </div>
    </div>
    

    We had to move positioning.ts to out app because it wasn’t compiled.

    I just copied your code and everything works fine.

    Good job

    Thanks 🙂