How to wrap component which uses ng-content

Bug description:

It’s not possible currently to wrap any component of the library which uses ng-content/transclusion. This seems be caused by @ContentChildren which can’t query 2+ levels of transcluded content.

So the following does not work:

@Component({
  selector: 'my-accordion',
  template: `
    <ngb-accordion>
      <ng-content></ng-content>
    </ngb-accordion>
  `
})
export default class MyAccordionComponent {}

And used as <my-accordion><ngb-panel>Hello world<ngb-pabel></my-accordion> because ngb-panel can’t be queried by ngb-accordion. Maybe using { descendants: true } with @ContentChildren(NgbPanel) could solve this issue.

How would you suggest to wrap components?

Version of Angular, ng-bootstrap, and Bootstrap:

Angular: 2.4.5

ng-bootstrap: 1.0.0-alpha.19

Bootstrap: Bootstrap 4 alpha6

2 thoughts on “How to wrap component which uses ng-content

  1. Guys I think this is definitely not the problem of ng-bootstrap, it is a problem of Angular platform.
    Angular does not support ContentChildren queries deeper than one level (your wrapper component is second level), and it is so by the design.

    A great explanation can be found here: angular/angular#20810 (comment)

  2. I expanded upon @mcelotti‘s solution and successfully wrapped the accordion:

    import {
        Component,
        ContentChildren,
        EventEmitter,
        Input, OnInit,
        Output,
        QueryList,
        TemplateRef,
        ViewChild
    } from "@angular/core";
    import {NgbAccordion, NgbPanelChangeEvent} from "@ng-bootstrap/ng-bootstrap";
    
    // tslint:disable-next-line:no-empty-interface
    export interface AccordionPanelChangeEvent extends NgbPanelChangeEvent {
    }
    
    let nextId = 0;
    
    @Component({
        selector: "app-accordion-panel",
        template: "<ng-template #innerTemplate><ng-content></ng-content></ng-template>"
    })
    export class AccordionPanelComponent {
        @ViewChild("innerTemplate")
        public innerTemplate: TemplateRef<any>;
    
        @Input()
        public disabled = false;
    
        @Input()
        public id = `accordion-panel-${nextId++}`;
    
        @Input()
        public title: string;
    
        @Input()
        public type: string;
    }
    
    @Component({
        selector: "app-accordion",
        templateUrl: "./accordion.component.html"
    })
    export class AccordionComponent implements OnInit {
        @ViewChild("innerAccordion")
        public innerAccordion: NgbAccordion;
    
        @ContentChildren(AccordionPanelComponent)
        public panels: QueryList<AccordionPanelComponent>;
    
        @Input()
        public activeIds: string | string[] = [];
    
        @Input()
        public closeOthers: boolean;
    
        @Input()
        public destroyOnHide = true;
    
        @Input()
        public type: string;
    
        @Output()
        public panelChange = new EventEmitter<AccordionPanelChangeEvent>();
    
        public ngOnInit() {
            this.innerAccordion.panelChange.subscribe((event: NgbPanelChangeEvent) => {
                this.panelChange.emit(event as AccordionPanelChangeEvent);
            });
        }
    
        public isExpanded(panelId: string): boolean {
            return this.innerAccordion.isExpanded(panelId);
        }
    
        public expandAll() {
            this.innerAccordion.expandAll();
        }
    
        public collapse(panelId: string) {
            this.innerAccordion.collapse(panelId);
        }
    
        public collapseAll() {
            this.innerAccordion.collapseAll();
        }
    
        public toggle(panelId: string) {
            this.innerAccordion.toggle(panelId);
        }
    }
    

    And the HTML:

    <ngb-accordion
        #innerAccordion
        [activeIds]="activeIds" [closeOthers]="closeOthers" [destroyOnHide]="destroyOnHide" [type]="type">
        <ngb-panel
                *ngFor="let panel of panels"
                [disabled]="panel.disabled"
                [id]="panel.id"
                [title]="panel.title"
                [type]="panel.type">
            <ng-template ngbPanelContent>
                <ng-template [ngTemplateOutlet]="panel.innerTemplate"></ng-template>
            </ng-template>
        </ngb-panel>
    </ngb-accordion>