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 we
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 :
*
* - https://getbootstrap.com/docs/4.1/components/dropdowns/
* - https://stackoverflow.com/a/42498168/2611798
* - https://github.com/ng-bootstrap/ng-bootstrap/issues/1012
*
*/
@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: `
`
})
class DropdownContainerTestingComponent {
}
describe("DropdownBodyDirective", () => {
let component: DropdownContainerTestingComponent;
let fixture: ComponentFixture
;
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);
});
});
});
});