5 Angular directives you can use in your project

In a recent blog, we showed off some examples of Angular pipes we use in our code base. Now, we’d like to talk about directives. Angular directives allow you to attach behavior to elements in the DOM and reuse it across your project. The framework already comes with a number of convenient directives like NgStyle, NgIf, NgFor, and NgSwitch.

We’ve written over 65 custom directives so far and would like to share the top five that we think you’ll find useful in your project. Just note that the directives in the demos below were intentionally distilled to bare minimum for clarity and don’t have extensive customization options or destructors/cleanup code. Use them as a starting point.

#1 Callout

Virtually no application goes without callouts or tooltips, which are small popup elements providing more detail about their owner.

Here is a minimal example of a callout directive that dynamically creates a callout component with specified text when you hover over an element:

The CalloutComponent used in this demo is created on the fly using viewContainer.createComponent() and destroyed when the mouse leaves the element. Here we insert the callout immediately after the element that the directive is applied to (this is where the injected ViewContainerRef points to). For more elaborate configuration options, see our earlier article on building Angular components on the fly.

#2 Deep disabled

In larger applications, it’s not uncommon to disable entire groups of UI elements as the state changes. For example, the Lucidchart editor dynamically enables and disables certain tool groups depending on what shapes are currently selected:

Fragment of the Lucidchart editor showing a toolbar with some button groups disabled
Fragment of the Lucidchart editor. Line options are disabled because a block is selected.

Instead of repeating the expression for disabled state on each individual component, we can make a special disabled directive that will cascade down the disabled state from one of the parent components.

Let’s prototype this:

Most of the “magic” here is actually done by Angular’s dependency injection framework:

class DisabledDirective {
    constructor(
        @SkipSelf() @Optional() private optParent: DisabledDirective
    ) {...}
}

This line in the constructor of the directive means “inject the nearest parent DisabledDirective, skipping myself, if there is one.” Now, during the change detection cycle, we will check not only the boolean value of the current directive but the parent too:

newValue = !!this.appDisabled || this.optParent.disabled;

After implementing the directive, we update the code of our components like form fields and buttons to be aware of it (again using the dependency injection mechanism).

@Component({
  selector: 'app-button',
  template: `
        <button [disabled]="disabled"><ng-content></ng-content></button>
  `
})
export class ButtonComponent {
    private disabledDirective: DisabledDirective;
    
    constructor(changeDetector: ChangeDetectorRef, @Optional() optDisabled: DisabledDirective) {
        this.disabledDirective = resolve(optDisabled);
        this.disabledDirective.onChange(this, (newValue) => {
            changeDetector.markForCheck();
        });
    }
    
    get disabled(): boolean {
        return this.disabledDirective.disabled;
    }
}

While this directive is quite simple, it reduces code duplication and allows us to toggle areas of the UI on multiple levels without any explicit communication between components.

We also added the ability to disable the cascading of the disabled state on select elements (like the Fullscreen button in the demo above) via the disabledStopPropagation attribute.

#3 animatedIf

*ngIf is perhaps one of the most widely used built-in Angular directives. But what if we want to use animation to toggle components in our app? The following directive extends ngIf to support

The directive simply toggles the showing and hiding classes on the container element and  assumes that the animation is done via CSS which provides great flexibility—you don’t have to touch the directive code to change animation.

private hide() {
    let container = this.getContainer();
    if (!!container) {
        container.classList.remove('showing');
        container.classList.add('hiding');

        animationEndSafe(container, 1000).then(() => {
            this.ngIf = this.visible;
            container.classList.remove('hiding');
        });

        this.animatedIfOnHide.emit();
    }
}

The demo implements a simple fade animation, but it can easily be tweaked to be slide or grow/shrink or any combination of them. The animationEndSafe function is simply a wrapper around a listener to the animationend event that calls the callback after a specified timeout if the event hasn’t fired. This is to ensure that the code doesn’t get “stuck” in case the container element doesn’t have any animation defined on it.

#4 Window resize thresholds

CSS3 Media Queries (technically called media features) greatly simplified responsive design for web developers, allowing us to alter page layout based on features like screen size, orientation, and pixel density. In Angular world, however, a significant part of the app’s UI rendering is taken over by the framework.

The following directive lets you define a series of window width “breakpoints” and alter the template when transitions between the thresholds happen.

The directive listens to Window’s resize event through the convenient host binding:

@Directive({
    selector: '[appWindowResize]',
    host: {
        '(window:resize)': 'onResize()',
    }
})

The only other technicality worth mentioning is that whenever you’re listening to DOM events that may fire frequently like resize or mouse movement, make sure to debounce your event handler so that it doesn’t execute an excessive number of times, creating unnecessary CPU load. Many third party libraries contain a debounce function, but we included our implementation in the demo:

// Callback debounce utility
function debounce<F extends(...args: any[]) => void>(f: F, timeout: number, target?: any): F {
    let timer: number|undefined = undefined;
    return (function(this: any, ...args: any[]) {
               target = target || this;

               timer && clearTimeout(timer);
               timer = setTimeout(() => {
                   f.apply(target, args);
               }, timeout);
           }) as F;
}

private onResize = debounce(() => {
    const offsetWidth = this.getWidth();
    ...
}, 200);

#5 focus

There is a native autofocus attribute in HTML5 spec for automatically focusing form fields upon page load. For example, this provides developers with an easy way to focus the login form on a page, saving the visitor the time to click on an input field before they can start typing in their credentials. However, the attribute won’t work in an Angular app where the framework builds the DOM dynamically. This directive is the Angular equivalent of the autofocus attribute.


What are the directives you use often in your projects? Share in the comments.

7 Comments

  1. […] Our engineers have written over 65 custom directives so far. Here are the top five that we think you’ll find useful in your project. Read more […]

  2. Did you custom build the dragging library for your charts or are you using some other angular library? I have been looking for a good solution for this for a while and haven’t found one.

  3. Dmitry PashkevichDecember 21, 2017 at 3:36 pm

    Hi Nayfin, we built our own. It consists of several classes and has a few things specific to our product so I didn’t include it in the article. Feel free to post back if you find a good general purpose library for that so that we can point other readers to it.

  4. Wladimir VladimirowiczMay 14, 2018 at 2:10 am

    HeyDmitry, can U update plunkers? Thanks

  5. Dmitry PashkevichMay 14, 2018 at 11:28 pm

    Looks like there is an issue with Plunkr, I’ll reach out to them

  6. Thanks!! Have you shared these on github?

  7. @Dmitry I still haven’t had any luck finding a library that suits my needs. Is there any chance you folks will open source yours? You guys seem to have all the fancy stuff I need!

Your email address will not be published.