Building our own custom directives

Custom directives encompass a vast world of possibilities and use cases, and we would need an entire book for showcasing all the intricacies and possibilities they offer.

In a nutshell, directives allow you to attach advanced behaviors to elements in the DOM. If a directive has a template attached, then it becomes a component. In other words, components are Angular directives with a view, but we can build directives with no attached views that will be applied to already existing DOM elements, making its HTML contents and standard behavior immediately accessible to the directive. This applies to Angular components as well, where the directive will just access its template and custom attributes and events when necessary.

Anatomy of a custom directive

Declaring and implementing a custom directive is pretty easy. We just need to import the Directive class to provide decorator functionalities to its accompanying controller class:

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

Then we define a controller class annotated by the Directive decorator, where we will define the directive selector, input and output properties (if required), optional events applied to the host element, and injectable provider tokens, should our directive's constructor require specific types to be instantiated by the Angular 2 injector when instancing itself (we will cover this in detail in Chapter 5, Building an Application with Angular 2 Components):

@Directive({
  selector: '[selector]',
  inputs: ['inputPropertyName'],
  outputs: ['outputPropertyName'],
  host: {
    '(event1)': 'onMethod1($event)',
    '(target:event2)': 'onMethod2($event)',
    '[prop]': 'expression',
    'attributeName': 'attributeValue'
  },
  providers: [MyCustomType]
})
class myDirective {
  @Input() otherInputPropertyName: any;
  @Output() otherOutputPropertyName: any;

  constructor(myCustomType: MyCustomType) {
    // implementation...
  }
}

Properties and decorators' such as selector, @Input(), or @Output() (same with inputs and outputs) will probably resonate to you from the time when we overviewed the component decorator spec. Although we haven't mentioned all the possibilities in detail yet, the selector may be declared as one of the following:

  • element-name: Select by element name
  • .class: Select by class name
  • [attribute]: Select by attribute name
  • [attribute=value]: Select by attribute name and value
  • not(sub_selector): Select only if the element does not match the sub_selector
  • selector1, selector2: Select if either selector1 or selector2 matches

In addition to this, we will find the host parameter, which specifies the events, actions, properties, and attributes pertaining to the host element (that is, the element where our directive takes action) that we want to access from within the directive. We can therefore take advantage of this parameter to bind interaction handlers against the container component or any other target element of our choice, such as window, document, or body. In this way, we can refer to two very convenient local variables when writing a directive event binding:

  • $event: This is the current event object that triggered the event.
  • $target: This is the source of the event. This will be either a DOM element or an Angular directive.

Besides events, we can update specific DOM properties that belong to the host component. We just need to link any specific property wrapped in braces with an expression handled by the directive as a key-value pair in our directive's host definition.

Note

The optional host parameter can also specify static attributes that should be propagated to a host element, if not present already. This is a convenient way of injecting HTML properties with computed values.

The Angular team has also made available a couple of convenient decorators so that we can more expressively declare our host bindings and listeners straight on the code, like this:

@HostBinding('[class.valid]') 
isValid: boolean; // The host element will feature class="valid"
                  // is the value of 'isValid' is true.

@HostListener('click', ['$event'])
onClick(e) {
   // This function will be executed when the host // component triggers a 'click' event.
}

In the next chapters, we will cover the configuration interface of directives and components in more detail, paying special attention to its life cycle management and how we can easily inject dependencies into our directives. For now, let's just build a simple, yet powerful, directive that will make a huge difference to how our UI is displayed and maintained.

Building a task tooltip custom directive

Let's put in practice some of the settings described above in a custom directive. So far, we have been displaying a tooltip text upon hovering over our pomodoro icons. To do so, we attached a pair of event bindings to the <pomodoro-task-icons> element. While this approach is not wrong, the output is a bit verbose and not reusable at all. At some point we may even need to apply the same [task] binding elsewhere as well and taking advantage of the same tooltip on mouseover would be quite convenient. Let's automate such functionality in a directive that will get automatically applied to any element featuring a [task] attribute, as our <pomodoro-task-icons> elements do. This directive will define input properties to refer to that very same property binding and also the target element we will use as a placeholder. If not available, the directive will just do nothing and will not yield any exception whatsoever. When available, the directive will bind mouseover and mouseout event listeners to the host element (<pomodoro-task-icons> in our example). These listeners will toggle the text inside the DOM element represented by the local reference bound to the placeholder property. Before doing so, we will cache the original value in order to reuse it upon moving the mouse out from the element.

The preceding description takes form in the following directive that you should implement before our components in the pomodoro-tasks.ts file:

@Directive({
  selector: '[task]'
})
class TaskTooltipDirective {
  private defaultTooltipText: string;
  @Input() task: Task;
  @Input() taskTooltip: any;

  @HostListener('mouseover') 
  onMouseOver() {
    if(!this.defaultTooltipText && this.taskTooltip) {
      this.defaultTooltipText = this.taskTooltip.innerText;
    }
    this.taskTooltip.innerText = this.task.name;
  }
  @HostListener('mouseout') 
  onMouseOut() {
    if(this.taskTooltip) {
      this.taskTooltip.innerText = this.defaultTooltipText;
    }
  }
}

Please note the selector in use: [task]. We have not configured the more logical <pomodoro-task-icons> element or created a new selector of our own. We obviously could have done that, but our goal in this exercise is different. We want to bind a special behavior to any DOM element and component that features a [task] attribute with a data binding on it. Precisely because this directive will take action on all elements featuring such property, we can include it as an input property in the directive implementation itself. Then we just need to provide a way to configure what DOM element will become our tooltip placeholder with the taskTooltip input property and we are all set.

As we saw in the previous section, thanks to the @HostListener() decorators, we can bind a listener function in our directive to an event occurred in the host component. This time we bound the mouseover and mouseout event so toggle the text of the target tooltip placeholder, caching its current text beforehand.

In order to see this directive in action, we need to add support for it first at the PomodoroTasksComponent decorator:

@Component({
  selector: 'pomodoro-tasks',
  directives: [PomodoroIconComponent, TaskTooltipDirective],
  pipes: [FormattedTimePipe, pomodoroQueuedOnlyPipe],
  styleUrls: ['pomodoro-tasks.css'],
  templateUrl: 'pomodoro-tasks.html'
})
class PomodoroTasksComponent {
  // No more changes apply 
}

Now, we can update our pomodoro-tasks.html template:

<p>
  <span *ngFor="#queuedTask of tasks | pomodoroQueuedOnly: true">
    <pomodoro-task-icons
      [task]="queuedTask"
      [taskTooltip]="tooltip"
      size="50">
    </pomodoro-task-icons>
  </span>
</p>
<p #tooltip>Mouseover for details</p>

One of the most exciting takeaways of this code example is the low code footprint required for extending elements with this new functionality, and its huge reusability.

After all the latest changes, reload your browser, toggle any task, move your mouse over the newly rendered pomodoro icon and... voilà!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.152.123