PotatoEngineer
PotatoEngineer

Reputation: 1642

Why isn't @ContentChildren getting populated in my custom Angular 2 dropdown?

I'm trying to replicate the look of a Fabric-style dropdown, for use in Angular 2. There are two important bits I'm trying to get right:

  1. I'm using ngModel
  2. The dropdown component doesn't know about its children until it queries them. The dropdown items are going to be in something else's template, because it's the "something else" that's going to have the data that needs to be inserted.

Here's what I have so far for the dropdown and dropdownItem components:

@Component({
    selector: "dropdown",
    template: ` <div class="ms-Dropdown" [ngClass]="{'ms-Dropdown--open': isOpen}">
                    <span class="ms-Dropdown-title" (click)="toggleDropdown()"> {{selectedName}} </span>
                    <ul class="ms-Dropdown-items">
                        <ng-content></ng-content>
                    </ul>
                </div>`,
    providers: [QueryList]
})
export class FabricDropdownComponent extends AbstractValueAccessor implements AfterContentInit {
    public selectedName: string;
    public isOpen: boolean;
    private subscriptions: Subscription[];
    constructor( @ContentChildren(FabricDropdownItemComponent) private items: QueryList<FabricDropdownItemComponent>) {
        super();
        this.subscriptions = [];
        this.selectedName = "Filler text, this should be replaced by 'Thing'";
        this.isOpen = false;
    }

    // HERE'S THE PROBLEM: this.items is an empty array, so I can't find any child components.
    public ngAfterContentInit() {
        this.items.changes.subscribe((list: any) => {
            // On every change, re-subscribe.
            this.subscriptions.forEach((sub: Subscription) => sub.unsubscribe());
            this.subscriptions = [];
            this.items.forEach((item: FabricDropdownItemComponent) => {
                this.subscriptions.push(item.onSelected.subscribe((selected: INameValuePair) => {
                    this.value = selected.value;
                    this.selectedName = selected.name;
                    this.isOpen = false;
                }));
            });
        });

        // During init, display the *name* of the selected value.
        // AGAIN: items is empty, can't set the initial value. What's going on?
        this.items.forEach((item: FabricDropdownItemComponent) => {
            if (item.value === this.value) {
                this.selectedName = item.name;
            }
        })
    }

    public toggleDropdown() { 
        this.isOpen = !this.isOpen;
    }
}

@Component({
    selector: "dropdownItem",
    template: `<li (click)="select()" class="ms-Dropdown-item" [ngClass]="{'ms-Dropdown-item--selected': isSelected }">{{name}}</li>`
})
export class FabricDropdownItemComponent implements OnInit {
    @Input() public name: string;
    @Input() public value: any;
    @Output() public onSelected: EventEmitter<INameValuePair>;
    public isSelected: boolean;
    constructor() {
        this.onSelected = new EventEmitter<INameValuePair>();
        this.isSelected = false;
    }

    public ngOnInit() {
        if (!this.name) {
            this.name = this.value.toString();
        }
    }

    public select() {
        this.onSelected.emit({ name: this.name, value: this.value });
        this.isSelected = true;
    }

    public deselect() {
        this.isSelected = false;
    }
}

(AbstractValueAccessor comes from this other answer.)

And here's how I'm actually using them in the app:

<dropdown [(ngModel)]="responseType" ngDefaultControl>
    <dropdownItem [value]="'All'"></dropdownItem>
    <dropdownItem *ngFor="let r of responseTypes" [value]="r.value" [name]="r.name"></dropdownItem>
</dropdown>

My problem is that the dropdown's QueryList of @ContentChildren is always empty, so it doesn't get notified when I click on one of the dropdownItems. Why is the QueryList empty, and how can I fix that? What have I missed here? (I suppose I could just use a service to communicate between dropdown and dropdownItem, instead of a QueryList, but that's not what I'm asking about here - why is the QueryList empty?)

I've tried using @ViewChildren instead, but that didn't work. And I tried adding the FabricDropdownItemComponent to the directives of dropdown, but that just gave a different error: Error: Unexpected directive value 'undefined' on the View of component 'FabricDropdownComponent'

Plunker: https://plnkr.co/edit/D431ihORMR7etrZOBdpW?p=preview

Upvotes: 4

Views: 1813

Answers (2)

G&#252;nter Z&#246;chbauer
G&#252;nter Z&#246;chbauer

Reputation: 658263

Make

@ContentChildren(FabricDropdownItemComponent) private items: QueryList<FabricDropdownItemComponent>

a property of your class instead of a constructor parameter

export class FabricDropdownComponent extends AbstractValueAccessor implements AfterContentInit {
  @ContentChildren(FabricDropdownItemComponent) private items: QueryList<FabricDropdownItemComponent>
  constructor() {}
  ....
}

Upvotes: 0

osdamv
osdamv

Reputation: 3583

There is an open issue related to @ContentChildren

However for your particular problem there is a workaround using HostListener

Or use a MutationObserver

Upvotes: 1

Related Questions