The Practical Client

Integrating Angular 2 Directives

It’s easy to integrate your own attribute directives into Angular 2 templates to pass data from your component to your directive, have it respond to events on your page or even have your directive fire events to be processed by the component using it.

In a previous column, I described how to create an Angular attribute directive. My goal was to define an attribute (called phvisDisplayCustomers) that could be added to any element in a component’s template, be customized by the developer using it and then have it display summary information about a collection in that component.

But there are still some additional features I should provide. My directive would be more generally useful, for example, if it could be passed the data to display, rather than being tightly bound to a single component. It would also be useful if that attribute could respond to events in the page and, in turn, fire events to notify the component where it’s used when something interesting happens. I’ll handle all these issues in this column (and improve the syntax of my attribute while I’m at it).

Passing Data
In my previous column, I showed how my directive could reference other attributes on the same element -- this allows the developer to customize my directive’s behavior by setting values in those "other" attributes. Specifically, I allowed the developer to add another attribute (called selectionType) to the element and use that directive to customize the directive’s behavior.

I can use the same technique to allow the developer to pass data from my component to my attribute. In my component’s template, I could add another attribute (called custLength) to the element using my attribute. A developer using this new custLength attribute would then bind it to the length property of the collection whose length is to be displayed, just by using Angular’s "double moustache" syntax. In this example, the custLength property is bound to the length property of a collection called customers:

<span phvisDisplayCustomers 
      selectionType="multiple" 
      custLength={{customers.length}}> </span>
However, because the point of this attribute is to display that length property, forcing the developer to add an additional attribute to catch that data seems awkward. In addition, my phvisDisplayCustomers is currently an "attribute-without-a-value," which, thanks to XHTML, looks odd.

So, rather than define a new Input property called custLength, I would define an Input property on my phvisDisplayCustomers attribute itself. Internally, I can still give that property any name I want (custLength seems more sensible to me than "phvisDisplayCustomers").

The code at the top of my directive to define this custLength property and have it piggyback on my phvisDisplayCustomers attribute would look like this:

export class DisplayCustomersDirective {
  @Input("phvisDisplayCustomers") custLength: number;

The developer who wants to pass the length of the customers collection to my attribute would now write this more reasonable syntax:

<span phvisDisplayCustomers={{customers.length}} 
  selectionType="multiple" > </span>

Internally, I can use the custLength property in my directive’s methods. Here, I’m using the directive’s ngOnInit method to insert the value into the element with my attribute:

ngOnInit() {
  this.rnd.setElementProperty(this.elm.nativeElement, 'innerHTML', "Hello, World " + 
  this.custLength);
}

The ability to pass an alias to @Input allows me to give my attribute a name that makes sense to developers (phvisDisplayCustomers) while giving the properties I use names that make sense to me (custLength, in this case).

Adding Behavior
Up until now my directive has been very passive. I might want my directive to be a little more active by responding to events for the element to which my attribute is attached. I might, for example, want my directive to pop up a dialog box when the user clicks on the element to which my directive has been added.

To accomplish that, I use @HostListener to decorate the method I want to execute when the event is raised, passing the name of the event I want to respond to. To respond to a click event, for example, I just need to add this code inside my directive’s class:

@HostListener('click') SelectedItem() {
  alert(this.custLength);
}

To support this, I’ll need to extend the Imports statement in my directive to pick up HostListener from the Angular core library. With all the other modules that I’m already using, my directive’s import statement now looks like this:

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

Notifying the Hosting Component
Of course, it’s not impossible that the component that my directive is being used in might need to take action if my directive has been clicked. To notify my component that something has happened, I need to emit an event. The first step in that process is to define an "emittable property" decorated with @Output and initialize that property with an EventEmitter, specifying the kind of object that I’ll be returning in the event.

This example defines an emittable property called selected and initializes it with an EventEmitter that will return a number:

@Output() selected = new EventEmitter<number>();

I might as well import two additional modules I’ll need to support this change (that would be Output and Emitter) right now. After adding those definitions, the import statement in my directive for the Angular core library looks like this:

import { Directive, ElementRef, Renderer, Input, OnInit, HostListener, Output, EventEmitter } from "@angular/core";

With my emitter property defined, I can now use it in any of my directive’s methods. I want to use the emitter to signal that the element has been clicked by the user, so I’ll add the necessary code to the method tied to the directive’s click event (I’ll replace the alert that I used in the initial version of the method).

To emit the event I call the property’s emit method. I’ll pass my directive’s custLength property as part of the event, like this:

@HostListener('click') SelectedItem() {
  this.selected.emit(this.custLength);
}

Effectively, I’ve just added another attribute to the element where the developer is using my phvisCustomersDisplay attribute. By default, that attribute has the same name as my property ("selected" in this case). As I did with @Input, I could assign a different name to the attribute, but in this case I won’t bother.

The component that uses my attribute declaration can now, in its template, bind a method to this new attribute named "selected." Like any other event in Angular, if I want to bind a method to an attribute, I just enclose the attribute name in parentheses and set it to the method in my component I want run when the event occurs.

After binding my new selected property to a method called onSelected, the span element that uses my attribute declaration looks like this:

<span phvisDisplayCustomers="{{customers.length}}" 
      selectionType="multiple" 
      (selected)="onSelected($event)" > </span>

In this example I’ve chosen to pass the $event object to the component’s onSelected method. In Angular, $event holds whatever object I passed to the emit method when I raised the event. In this case, that will be the number I passed to the selected property’s emit method in my directive’s SelectedItem method.

Here’s an onSelected method I could add to my component to catch that numeric value and display it:

onSelected(count: number) {
  alert(count);
}

Testing
Code that simply catches the event and displays the data passed to it wouldn’t make much sense in a real application. However, it might well be the code I’d put in a simple test harness to exercise a directive.

My directive is very loosely coupled to everything else on the template where it’s used (except, of course, for the attributes the directive has defined for itself -- selected and selectionType, in this case). As a result, creating a test harness for my directive is easy. For this phvisDisplayCustomers directive, something like the code in Listing 1 would do the trick.

Listing 1: A Simple Test Harness for an Attribute Directive
import { Component } from '@angular/core';
@Component({
  selector: 'CustomersDisplayHarness',
  template: '<span phvisDisplayCustomers="3" 
                   selectionType="multiple" 
                   (selected)="onTesting($event)" > </span>',   
})

export class CustomerDisplayHarnessComponent {   
  onTesting(count: number) {
    if (count != 3)
      { alert("failed: Count is not correct"); }
        else
      { alert("succeeded"); }
   }
}

However, this code would only check the data passed to the custLength property. Purely to support a more complete test harness, I might be tempted to add an event to phvisDisplayCustomers that just returns all the information about the directive’s internal state. I could then bind my onTesting method to that event, catch the data the event returns and check all the returned values used inside my directive. Alternatively, I could take a more black-box approach and add more components to the harness to demonstrate that my attribute is working correctly. It would be up to me how simple or elaborate I wanted my test harness to be.

But this just sums up what I like about Angular: Angular makes it easy for me to create single-purpose, loosely coupled components (like my directive) that I can assemble to create complex applications … or just simple test harnesses that demonstrate the component’s functionality.

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

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

  • TypeScript Tops New JetBrains 'Language Promise Index'

    In its latest annual developer ecosystem report, JetBrains introduced a new "Language Promise Index" topped by Microsoft's TypeScript programming language.

Subscribe on YouTube