Defining Angular directives

Now that we've built a simple Angular component, let's continue our journey by understanding the Angular directives.

Using Angular directives, we can apply different behavioral or structural changes over the DOM. In this example, we will build a simple tooltip directive.

In contrast to components, directives do not have views and templates. Another core difference between these two concepts is that the given HTML element may have only a single component but multiple directives on it. In other words, directives augment the elements compared to components that are the actual elements in our views.

Angular's official style guide's recommendation is to use directives as attributes, prefixed with a namespace. Keeping this in mind, we will use the tooltip directive in the following way:

<div saTooltip="Hello world!"></div> 

In the preceding snippet, we use the tooltip directive over the div element. As a namespace, its selector uses the sa string.

Note

Since the focus of the book is an efficient and intuitive learning of Angular's concepts, the code snippets may not completely align with the Angular style guide. However, for production applications, following best practices is essential. You can find the official Angular style guide at https://angular.io/styleguide .

Now, let's develop a tooltip directive! Before implementing it, we need to import a couple of symbols from @angular/core. Open a new TypeScript file called app.ts and enter the following content; we'll fill the placeholders later:

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

In the preceding line, we import the following definitions:

  • ElementRef: This allows us to inject the element reference (we're not limited to the DOM only) to the host element. In the sample usage of the preceding tooltip, we get an Angular wrapper of the div element, which holds the saTooltip attribute.
  • Directive: This decorator allows us to add the metadata required for the new directives we define.
  • HostListener(eventname): This is a method decorator that accepts an event name as an argument. During initialization of the directive, Angular will add the decorated method as an event handler for the eventname events fired by the host element.

Let's look at the directive's implementation:

// ch4/ts/tooltip/app.ts 
 
@Directive({
  selector: '[saTooltip]'
})
export class Tooltip {
  @Input() saTooltip:string;

  constructor(private el: ElementRef, private overlay: Overlay) {
    this.overlay.attach(el.nativeElement);
  }

  @HostListener('mouseenter')
  onMouseEnter() {
    this.overlay.open(this.el, this.saTooltip);
  }

  @HostListener('mouseleave')
  onMouseLeave() {
    this.overlay.close();
  }
}

Setting the directive's inputs

In the preceding example, we declared a directive with the saTooltip selector. Note that Angular's HTML compiler is case sensitive, which means that it will distinguish the [satooltip] and [saTooltip] selectors. After that, we declare the input of the directive using the @Input decorator over the saTooltip property. The semantics behind this code is that we declare a property called saTooltip and bind it to the value of the result that we got from the evaluation of the expression passed to the saTooltip attribute.

The @Input decorator accepts a single argument, that is, the name of the attribute we want to bind to. In case we don't pass an argument, Angular will create a binding between the attribute with the same name as the property itself. We will explain the concept of input and output in detail later in this chapter.

Understanding the directive's constructor

The constructor declares two private properties: el of the ElementRef type and overlay of the Overlay type. The Overlay class implements logic to manage the tooltips' overlays and will be injected using the DI mechanism of Angular. In order to declare it as available for injection, we will need to declare the top-level component in the following way:

@Component({ 
  selector: 'app', 
  templateUrl: './app.html', 
  providers: [Overlay], 
  // ... 
}) 
class App {} 

Note

We will take a look at the dependency injection mechanism of Angular in the next chapter, where we will explain the way in which we can declare the dependencies of our services, directives, and components. The implementation of the Overlay class is not important for the purpose of this chapter. However, if you're interested in it, you can find the implementation at ch4/ts/tooltip/app.ts.

Better encapsulation of directives with NgModules

In order to make the tooltip directive available to the Angular's compiler, we will need to explicitly declare where we intend to use it. For instance, take a look at the AppModule class at ch4/ts/tooltip/app.ts:

@NgModule({
  declarations: [Tooltip, App],
  providers: [Overlay],
  imports: [BrowserModule],
  bootstrap: [App],
})
class AppModule {}

To the @NgModule decorator, we pass an object literal that has the declarations property. This property contains a list of all the directives that will be available in all component subtrees with roots any of the components listed in the bootstrap array. Another way to extend the list of available directives is to import a module. For instance, the module BrowserModule includes some very commonly used directives for the browser environment.

At first, it might seem annoying that you should explicitly declare all the directives that are used in your modules; however, this enforces better encapsulation. In AngularJS, all directives are in a global namespace. This means that all the directives defined in the application are accessible in all the templates. This brings in some problems, for example, name collision. In order to deal with this issue, we introduced naming conventions, for instance, the "ng-" prefix of all the directives defined by AngularJS and "ui-" for all directives coming with the Angular UI.

Currently, by explicitly declaring all the directives that are used within a given module, we create a namespace specific to the individual components' subtrees (that is, the directives will be visible to the given root component and all of its successor components). Preventing name collisions is not the only benefit we get; it also helps us with better semantics of the code that we produce, since we're always aware of the directives accessible by the given component when we know in which module it's declared. We can find all the accessible directives of the given component by following the path from the component to the top module and taking the union of all the values of declarations and the declarations of the modules' imports. Given that components are extended from directives, we need to explicitly declare all the used components as well.

Since Angular defines a set of built-in directives, BrowserModule exports them by exporting the module CommonModule, which contains them. This list of predefined directives includes NgClass, NgFor, NgIf, NgStyle, NgSwitch, NgSwitchWhen, and NgSwitchDefault. Their names are quite self-explanatory; we'll take a look at how we can use some of them later in this chapter.

Using NgModules for declaring reusable components

With NgModules, we can achieve a good degree of encapsulation. By explicitly exporting the public components, directives, pipes, and services, we can hide some of the implementation details of our modules. This way we can implement reusable modules and expose only their public interface, and we do not reveal any low-level components to the user of the module.

In order to get a better idea, let's take a look at the following example:

@Component(...)
class ZippyHeader {
  @Input() header: string;
}

@Component(...)
class Zippy {
  @Input() header: string;
  visible = true;
}

@Component(...)
class App {}

In the preceding snippet, we declare the components Zippy, ZippyHeader, and App. Zippy is a component that has a header and a content; we can toggle the visibility of the content by clicking on the header. In the component ZippyHeader, we can implement some logic for handling the click events and/or visualizing the header. In the App component, we use the Zippy component by passing text for it's header and content.

In order to create a working Angular application, we will need to declare an NgModule, which somehow references all the three components. We can approach this in two main ways:

  1. Declare a single NgModule and include all the three components inside of its list of declarations.
  2. Declare two NgModules:
    • One that declares the Zippy and ZippyHeader components, called ZippyModule.
    • Another one that declares the App component and imports the module ZippyModule.

The second approach has a couple of advantages: in ZippyModule, we can declare both Zippy and ZippyHeader, but we can export only Zippy because ZippyHeader is used internally, within Zippy, and we don't have to expose it to the user. By declaring the module ZippyModule, we can import it into other modules in our application where we want to reuse the Zippy component, or we can even extract it as a separate npm module and reuse it in multiple applications.

The second approach will look like this:

// ch4/ts/zippy/app.ts

@Component(...)
class ZippyHeader {...}

@Component(...)
class Zippy {...}

@NgModule({
  declarations: [Zippy, ZippyHeader],
  exports: [Zippy]
  imports: [CommonModule],
})
class ZippyModule {}

@Component(...)
class App {...}

@NgModule({
  imports: [BrowserModule, ZippyModule],
  declarations: [App],
  bootstrap: [App]
})
class AppModule {}

platformBrowserDynamic().bootstrapModule(AppModule);

In the preceding example, in the module ZippyModule, we declare both Zippy and ZippyHeader, but we export only Zippy. We also import the module CommonModule from @angular/common in order to reuse Angular's built-in directives (for instance, NgIf is exported by the CommonModule).

In the AppModule, all we need to do is to import ZippyModule, and this way, we'll be able to use all of its exports and providers. We'll discuss providers further in the next chapter.

Note

Note that good practices suggest that we should implement each individual component into a separate file. For the sake of simplicity in the examples for this book, we've violated this practice. For a list of best practices, visit https://angular.io/styleguide.

Using custom element schema

Now, let's suppose we want to add a timer to our page and reuse a Web Component that we have already built. In this case, our application can look something like this:

//  ch4/ts/custom-element/app.ts

import {Component, NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';

@Component({
  selector: 'my-app',
  template: `
    <h1>Hello {{name}}</h1>
    The current timeout is <simple-timer></simple-timer>
  `
})
class App {
  name: string = 'John Doe';
}

@NgModule({
  imports: [BrowserModule],
  declarations: [App],
  bootstrap: [App]
})
class AppModule {}

platformBrowserDynamic().bootstrapModule(AppModule);

Now, if we run our application, we'll get the following error:

Using custom element schema

Figure 5

When Angular parses the template of the App component, it will find the <simple-timer></simple-timer>. It is not an element defined by the HTML specification and it doesn't match any of the selectors of the directives declared or imported in the AppModule, so the framework will throw an error.

So, how we can use Angular with custom components? The solution is to use the schemas property of the object literal we pass to @NgModule:

import {..., CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';

//...

@NgModule({
  imports: [BrowserModule],
  declarations: [App],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  bootstrap: [App]
})
class AppModule {}

This way we change the default schema that Angular uses for the validation of the elements and their attributes during parsing.

By default, the framework will throw an error if it finds an element that doesn't match the element selector of any of the imported or declared directive, or an element defined by the HTML5 spec.

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

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