The Practical Client

Build Your DOM Additions with Angular and TypeScript

Here’s how, in Angular, you can use directives to dynamically modify HTML in Views without polluting your business logic.

I know that, in my last column, I said that the amount of TypeScript-related documentation available for creating Angular applications was so great that any contribution I could make would be redundant … but then I realized that I couldn’t stop myself from discussing Angular’s attribute directives.

One of the reasons that I like Angular so much is it does such a great job of separating the different components of an application. Attribute directives, for example, allow you to isolate generating your UI from the code required to implement your presentation logic.

The Case for Directives
You don’t often need attribute directives. If you’re generating your HTML at the server, you don’t need Angular at all -- that’s what you have Views for. In Angular, components (which are considered a kind of attribute) provide a template-based way to generate HTML. In my previous case study I used a component to display information for a single customer. In that template, I used the structural attribute NgFor to repetitively generate HTML, and I could also have used NgIf (another structural attribute) to selectively generate HTML.

But if I need to modify the appearance or behavior of HTML elements -- and, especially, if I wanted to be able to use the functionality in multiple places -- I’d want to create an attribute directive. If, on top of all that, I wanted to be able to customize that functionality when I used it … well, then, I’d be driven to use an attribute directive.

Defining a Directive
As the name suggests, attribute directives appear as attributes on elements (I’ll just refer to them as "directives"). The first step in designing a directive, therefore, is deciding on a name for your attribute.

It’s critical here to avoid conflicts with any other name that might be used in an HTML element. For this case study, I’m creating a directive that displays summary information about a collection of customer objects (an even better directive would display summary information about any collection). I decided on phvisDisplayCustomers as my attribute’s name. Incorporating my company’s name (phvis) into the name avoids conflicts with future HTML or Angular names (or directives generated by other developers). This example, added to a component’s template, uses my directive with a span element:

<span phvisDisplayCustomers></span>

To start creating my directive, I add a new TypeScript code file to my project and have it include a class to hold my directive’s code (I cleverly call this file DisplayCustomers.directive.ts). To flag this class as an attribute directive, I decorate the class with a selector that includes my attribute name. I also need an import statement for the Angular resources on which my directive depends. For a simple directive, all I need is the Directive module.

The resulting skeleton for my directive looks like this:

import { Directive } from '@angular/core';

@Directive({ selector: '[phvisDisplayCustomers]' })
export class DisplayCustomersDirective {
  constructor() {
  }
}

Two notes on naming: The class name for your directive doesn’t have to end with the word "Directive" but it’s an Angular practice. In Visual Studio 2015, I got a red wavy line under @angular/core in my import statement when I used "directive" in the file name. That would go away and come back, but it never stopped a compile, showed up in the Error list or hampered my application. I decided to live with it rather than change the name of the file.

Before I can use my directive in a page, however, I need to tell my application that my directive exists. That requires two additions to my application’s app.module.ts file: I need an import statement referencing my class name and my file name; and I need to add my directive to my module’s declarations list. Integrating those additions with the components from my previous column gives the code in Listing 1.

Listing 1: Importing and Declaring a Directive
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpModule } from '@angular/http';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { CustomerListComponent } from './CustomerList.component';
import { DisplayCustomersDirective } from './DisplayCustomers.directive';

@NgModule({
  imports: [
    BrowserModule,
    HttpModule,
    FormsModule
  ],
  declarations: [
    AppComponent,
    CustomerListComponent,
    DisplayCustomersDirective
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Changing the Element
With that work done, I’m ready to build out my directive. I can manipulate the element my directive is attached to directly by using the Angular ElementRef module … but it’s considered a better practice to work with the elements on your page indirectly through a combination of ElementRef and Renderer (by the way, in the latest version of Angular, Renderer has been deprecated in favor of Renderer2 -- however, I’ve been building with an earlier version of Angular, so I’ll stick with Renderer).

My first step in manipulating the page, therefore, is enhancing the import statement at the top of my directive to pick up the new packages I require:

import { Directive, ElementRef, Renderer } from '@angular/core';

It’s then a matter of adding some code to my constructor to accept references to the ElementRef and Renderer modules. Inside the module, I use the Renderer setElementProperty method to update the element to which my directive is attached (with Renderer2, you’d use setElement). I must pass the setElementProperty the element I want to update (available through the ElementRef nativeElement property), the name of the property I want to update (innerHTML, in this case), and the text I want to add to the element. To ensure this is working, I’ll insert the traditional "Hello, World":

constructor(elm: ElementRef, rnd: Renderer) {
  rnd.setElementProperty(elm.nativeElement, 'innerHTML', "Hello, World");
}

Supporting Configuration
I can allow a developer to configure what my directive does by accepting input from other attributes on the same element -- including input from custom attributes that I invent. For example, I can allow the developer to specify what summary information to display by supporting an attribute called selectionType (I’ll look for values such as "none," "sales" or "purchases" in that attribute). A typical example might look like this:

<span phvisDisplayCustomers selectionType="sales"> </span>

To capture the value of the selectionType attribute, I need to add a property to my directive and decorate it with @Input (the Input decorator pulls data from the element into the directive). I’d add a property inside my directive, like this:

export class DisplayCustomerDirective {
  @Input() selectionType: string;

I obviously want to use this property when I generate my display. However, it turns out that Input properties aren’t initialized when the constructor executes. So, rather than use the constructor to add HTML to my element, I’ll use the Angular ngOnInit method for that and only use the constructor to do two things: accept the the ElementRef and Renderer modules and set up fields to hold the modules.

After moving my code and integrating my property, my constructor and ngOnInit method looks like this:

constructor(private elm: ElementRef, private rnd: Renderer) { }

ngOnInit() {
  this.rnd.setElementProperty(this.elm.nativeElement, 'innerHTML', 
    "Your selection is: " + this.selectionType;
}

To support using Input and ngOnInit, I need to extend my directive’s import statement to include them both:

import { Directive, ElementRef, Renderer, Input, OnInit } from '@angular/core';

At this point, I have a simple directive that adds functionality to an element on which it’s used. However, I still need to integrate this directive with the components that use it so they can, for example, pass the list of customers to display. I also add behavior to my element to display the summary information. I’ll address those tasks in my next column.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

Subscribe on YouTube