Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Jonathan Dowdle
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Jonathan Dowdle@jdowdle )

Using An Item Template With An HTML Dropdown Menu Component In Angular 2 RC 3

By Ben Nadel on

A while ago, I played around with trying to create an HTML Dropdown menu component in Angular 2. This was quite a non-trivial task, but very worthwhile. Recently, however, I discovered that you could pass Template references into components for dynamic rendering. And, while not exactly the same thing, I wanted to see if the use of a TemplateRef could significantly decrease the complexity of a custom HTML dropdown component in Angular 2 RC 3.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

The idea behind a custom HTML dropdown menu component is that the rendering of each item can be controlled by the calling context. In my previous HTML downdown post, I did this by nesting Item components inside the Dropdown component. In that approach, each Item component had to inject and then communicate with the parent Dropdown component. This worked, but required intricate communication, management of the component life-cycle, and imperative references to the HTML markup.

If we could replace that nested Item component with a simple TemplateRef, it could remove a lot of complexity around that communication and component life-cycle management. Not to mention that we might be able to remove any imperative manipulation of the HTML. To explore this idea, I put together a simple HTML dropdown menu that looks for a TemplateRef in its content children:

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  •  
  • // Import the application components and services.
  • import { HtmlDropdownComponent } from "./html-dropdown.component";
  •  
  • interface Friend {
  • id: number;
  • name: string;
  • avatar: string;
  • }
  •  
  • @Component({
  • selector: "my-app",
  • directives: [ HtmlDropdownComponent ],
  •  
  • // I our view, notice that we are providing a TemplateRef as a child element of the
  • // HtmlDropdownComponent. The dropdown component will query for this template and
  • // then use it to render both the option items as well as the root item.
  • template:
  • `
  • <p>
  • <strong>Best Friend</strong>: {{ bestFriend?.name || "None selected" }}
  • &mdash;
  • <a (click)="clearSelection()">Clear selection</a>
  • </p>
  •  
  • <html-dropdown
  • [items]="friends"
  • [(value)]="bestFriend"
  • placeholder="Select Friend">
  •  
  • <template let-friend="item">
  • <img [src]="( './img/' + friend.avatar )" />
  • <span class="name">
  • {{ friend.name }}
  • </span>
  • </template>
  •  
  • </html-dropdown>
  • `
  • })
  • export class AppComponent {
  •  
  • // I hold the friend that is selected as the best friend.
  • public bestFriend: Friend;
  •  
  • // I hold the collection of friends.
  • public friends: Friend[];
  •  
  •  
  • // I initialize the component.
  • constructor() {
  •  
  • this.bestFriend = null;
  • this.friends = [
  • {
  • id: 1,
  • name: "Joanna",
  • avatar: "joanna-avatar.jpg"
  • },
  • {
  • id: 2,
  • name: "Kim",
  • avatar: "kim-avatar.jpg"
  • },
  • {
  • id: 3,
  • name: "Sarah",
  • avatar: "sarah-avatar.jpg"
  • },
  • {
  • id: 4,
  • name: "Tricia",
  • avatar: "tricia-avatar.jpg"
  • }
  • ];
  •  
  • }
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I clear the best-friend selection.
  • public clearSelection() : void {
  •  
  • this.bestFriend = null;
  •  
  • }
  •  
  • }

Notice that we are passing in the [value] and [items] input properties to the HtmlDropdownComponent; but, that we are also providing a TemplateRef as the definition of how to render each item - in this case, with an avatar and a name. From a component consumption standpoint, this is really close to where we want to be, at least for a simple HTML dropdown menu.

Once the HtmlDropdownComponent has a reference to this item TemplateRef, it can use it to render both the options in the item collection as well as the selected option in the dropdown menu root. The fact that it can use the TemplateRef to render both of these means that we no longer have to worry about cloning HTML, which is a huge win!

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  • import { ContentChild } from "@angular/core";
  • import { EventEmitter } from "@angular/core";
  • import { TemplateRef } from "@angular/core";
  •  
  • @Component({
  • selector: "html-dropdown",
  • inputs: [ "items", "value", "placeholder" ],
  • outputs: [ "valueChange" ],
  •  
  • // Query for the template being provided by the calling context.
  • queries: {
  • itemTemplate: new ContentChild( TemplateRef )
  • },
  • host: {
  • "[class.is-open]": "isShowingItems"
  • },
  • template:
  • `
  • <div (click)="toggleItems()" class="dropdown-root" [ngSwitch]="!! value">
  • <div *ngSwitchCase="true" class="dropdown-item-content">
  •  
  • <template
  • [ngTemplateOutlet]="itemTemplate"
  • [ngOutletContext]="{ item: value, index: -1 }">
  • </template>
  •  
  • </div>
  • <div *ngSwitchCase="false" class="placeholder">
  •  
  • {{ placeholder || "Nothing Selected" }}
  •  
  • </div>
  • </div>
  •  
  • <ul *ngIf="isShowingItems" class="dropdown-items">
  • <li
  • *ngFor="let item of items ; let index = index ;"
  • (click)="selectItem( item )"
  • class="dropdown-item">
  •  
  • <div class="dropdown-item-content">
  •  
  • <template
  • [ngTemplateOutlet]="itemTemplate"
  • [ngOutletContext]="{ item: item, index: index }">
  • </template>
  •  
  • </div>
  •  
  • </li>
  • </ul>
  • `
  • })
  • export class HtmlDropdownComponent {
  •  
  • // I determine if the dropdown items are being shown.
  • public isShowingItems: boolean;
  •  
  • // INPUT: I am the collection of items used to render the dropdown items.
  • public items: any[];
  •  
  • // INPUT: I am the text to show when no item is selected.
  • public placeholder: string;
  •  
  • // INPUT: I am the currently selected value.
  • public value: any;
  •  
  • // OUTPUT: I am the output event stream that emits the item selected by the user.
  • public valueChange: EventEmitter<any>;
  •  
  •  
  • // I initialize the component.
  • constructor() {
  •  
  • this.isShowingItems = false;
  • this.valueChange = new EventEmitter();
  •  
  • }
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I hide the dropdown items.
  • public hideItems() : void {
  •  
  • this.isShowingItems = false;
  •  
  • }
  •  
  •  
  • // I select the given item.
  • // --
  • // NOTE: Since this is a one-way data flow, the selection is being emitted rather
  • // than applied directly to the value.
  • public selectItem( item: any ) : void {
  •  
  • this.hideItems();
  • this.valueChange.emit( item );
  •  
  • }
  •  
  •  
  • // I show the dropdown items.
  • public showItems() : void {
  •  
  • this.isShowingItems = true;
  •  
  • }
  •  
  •  
  • // I show or hide the dropdown items depending on their current visibility.
  • public toggleItems() : void {
  •  
  • this.isShowingItems
  • ? this.hideItems()
  • : this.showItems()
  • ;
  •  
  • }
  •  
  • }

This isn't a fully-featured dropdown menu; but, for the brevity of the code, it actually accomplishes quite a bit. Notice that we are gathering the TemplateRef through a ChildContent query. Then, we use that TemplateRef to render both the items and the menu root. This makes the entire view declarative - no manually manipulating the HTML; no cloning of LI item content into the root. The entire state of the view is managed directly by Angular.

There's a lot of dropdown menu functionality that we're not implementing for the sake of simplicity. But, when we run this code, you can see the items being rendered based on the template:


 
 
 

 
 Using a TemplateRef to provide custom item rendering in an HTML dropdown menu component in Angular 2 RC 3. 
 
 
 

As you can see, both the dropdown menu root as well as the dropdown menu items are being rendered by the TemplateRef that was supplied by the calling context.

I'm rather enamored with this idea of being able to pass TemplateRefs around in Angular 2. I think it can really simplify some solutions. Plus, the TemplateRefs have the added benefit of not existing until they are rendered. In comparison, ng-content components exist regardless of whether or not they are being rendered in the DOM (Document Object Model). So, to some degree, TemplateRef may have less overhead. Definitely a topic worth noodling on.




Reader Comments

Great one Ben.
Quick question, where do you use:

host: {
"[class.is-open]": "isShowingItems"
},

as I didnt find that class in the HTML template

tx

Sean

Reply to this Comment

With hot friends like them, it must be harder to choose your best friend than writing this component ;)

Seriously though, good article. I learnt a lot and it sure simplifies the previous version (although I haven't waded all the way through). Thank you.

Does this version of the component have the same feature set as the previous one? If not, were there any features that were problematic to implement in this version?

Reply to this Comment

@Guy,

Great question - the feature set it not exactly the same. By using a custom item renderer, at least in the current approach, what I lose the ability to pass-in a mixed collection of data in the HTML. Meaning, in the first exploration, I relied on parent-child component communication:

<html-menu>
. . . . <html-menu-item />
. . . . <html-menu-item />
. . . . <html-menu-item />
</html-menu>

The benefit of that approach is that the markup can be be produced with a combination of static AND data-driven inputs. Like:

<html-menu>
. . . . <html-menu-item />
. . . . <html-menu-item *ngFor=" .... "/>
. . . . <html-menu-item />
</html-menu>

... where I have two "static" Item instances and one that is an ngFor-driven collection.

With this current approach, and the custom item renderer, the entire collection of items has be passed in as a collection for an [items] input property. That means that if I wanted two static menu items and one collection-based, I'd have to programmatically combine those two in the controller, like:

this.menuItems = [ staticItemA, ...collectionItems, staticItemB ];

... and then pass it in as the input, [items]="menuItems".

Of course, it all comes down to how important having the items be HTML-driven or data-driven.

You could probably also combine the two approaches somehow; but, that might start to create a bit more a complicated mental model.

Reply to this Comment

Thank you so much Ben. I can't express how helpful this was to me especially because documentation of TemplateRef is basically non-existent. *gives virtual cup of coffee*

Reply to this Comment

Hi guys,

Could anyone point me regarding loading different views or HTML templates in a one single page i.e. Showing different sections all with same width n height having different data using angular 2!

Thanks in advance

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.