关闭

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

212人阅读 评论(0) 收藏 举报
分类:

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;
   
  }
   
  }
view rawapp.component.ts hosted with ❤ by GitHub

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.

原文链接: http://www.bennadel.com/blog/3116-using-an-item-template-with-an-html-dropdown-menu-component-in-angular-2-rc-3.htm

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:25396次
    • 积分:412
    • 等级:
    • 排名:千里之外
    • 原创:1篇
    • 转载:101篇
    • 译文:0篇
    • 评论:1条
    最新评论