Recreating the appearance of a Fabric-style dropdown for Angular 2 is my current project. There are two key elements that I need to focus on:
- I'm making use of ngModel
- The dropdown component is unaware of its children until queried. The dropdown items will be in another template, as it's the parent component that holds the necessary data for insertion.
This is what I've developed 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 ISSUE: this.items remains an empty array, preventing access to child components.
public ngAfterContentInit() {
this.items.changes.subscribe((list: any) => {
// Subscribing again on every change.
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;
}));
});
});
// At initialization, display the *name* of the chosen value.
// ONCE AGAIN: items array being empty causes inability to set initial value. What could be the issue?
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 sourced from here.)
Below is how I've utilized these components within the application:
<dropdown [(ngModel)]="responseType" ngDefaultControl>
<dropdownItem [value]="'All'"></dropdownItem>
<dropdownItem *ngFor="let r of responseTypes" [value]="r.value" [name]="r.name"></dropdownItem>
</dropdown>
The problem arises when the dropdown's QueryList of @ContentChildren is consistently empty, resulting in no notifications upon clicking on dropdownItems. Why does the QueryList remain void and how might this be resolved? Have I overlooked something crucial here? (Alternatively, using a service for communication between dropdown and dropdownItem instead of a QueryList could be considered, but for now, why does the QueryList remain unpopulated?)
I attempted using @ViewChildren without success. Additional errors were encountered when adding FabricDropdownItemComponent to the directives of dropdown, including:
Error: Unexpected directive value 'undefined' on the View of component 'FabricDropdownComponent'
.
Plunker Link: https://plnkr.co/edit/D431ihORMR7etrZOBdpW?p=preview