How to append a single dropdown menu to body in Bootstrap

后端 未结 3 1740
走了就别回头了
走了就别回头了 2021-02-19 22:48

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

3条回答
  •  小鲜肉
    小鲜肉 (楼主)
    2021-02-19 23:13

    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);
                });
            });
        });
    });
    

提交回复
热议问题