Developing custom animation directives

We have said several times that one of the advantages of the CssAnimationBuilder API is its reusability. Putting together any given animation setup and applying it on not one but many HTML elements later on becomes a breeze. However, directives are the perfect solution when it comes to managing reusability in the Angular arena. So why not get the best of both worlds? As we will see in the next example, wrapping animation within directives becomes the go-to solution for many case scenarios.

Our last animation example in this chapter will introduce a brand new custom directive into our application. The Highlight directive leverages the CssAnimationBuilder API to change the background color of any given DOM element on the fly, resetting the background color to its original state at the end of the animation. This kind of flashing effect has become quite widespread in modern web design for making the user aware that something has just happened on some part of the UI.

Let's start by creating the directive controller class file inside the directives subfolder of our shared features folder, and populate it with the following script. Please note how we keep applying the file naming conventions we embrace and how our directive will be mapped to a CSS class selector this time:

app/shared/directives/highlight.directive.ts

import { Directive, ElementRef, OnInit } from '@angular/core';
import { AnimationBuilder } from '@angular/platform-browser/src/animate/animation_builder';
import { CssAnimationBuilder } from '@angular/platform-browser/src/animate/css_animation_builder';

@Directive({
  selector: '.pomodoro-highlight',
  providers: [AnimationBuilder]
})
export default class HighlightDirective {
  cssAnimationBuilder: CssAnimationBuilder;

  constructor(
    private animationBuilder: AnimationBuilder,
    private elementRef: ElementRef) {

    this.cssAnimationBuilder = animationBuilder.css()
      .setDuration(300)
      .setToStyles({ backgroundColor: '#fff5a0' });
  }

  ngOnInit() {
    let animation = this.cssAnimationBuilder.start(
      this.elementRef.nativeElement
    );

    animation.onComplete(() => {
      animation.applyStyles({ backgroundColor: 'inherit' });
    });
  }
}

The code is pretty simple in its implementation. We basically build a directive mapped to a CSS class selector whose constructor instantiates a CSS animation consisting of a background-color interpolation to a certain tone of yellow (defined by the #fff5a0 hex value) along 300 milliseconds. This is our desired flashy effect. The ngOnInit hook method, which is executed in the very moment that the component affected by this directive is rendered in the view, fires the animation and resets the background color of the affected DOM element back to its original value. As we can see, we are taking advantage of the applyStyles() method of the Animation class. This method, along with other methods exposed in its API such as addClasses() or removeClasses() (both expecting an string array with the class names to add or remove, respectively), allows us to interact with the CSS bindings of the animated DOM element.

Before moving on, we need to ensure this new directive is available for the rest of features coexisting in our application, so we need to expose this new directive from the shared facade module as well. The code is as follows:

app/shared/shared.ts

import Queueable from './interfaces/queueable';
import Task from './interfaces/task';

import FormattedTimePipe from './pipes/formatted-time.pipe';
import QueuedOnlyPipe from './pipes/queued-only.pipe';

import AuthenticationService from './services/authentication.service';
import SettingsService from './services/settings.service';
import TaskService from './services/task.service';

import RouterOutletDirective from './directives/router-outlet.directive';
import HighlightDirective from './directives/highlight.directive';

const SHARED_PIPES: any[] = [
  FormattedTimePipe,
  QueuedOnlyPipe
];

const SHARED_PROVIDERS: any[] = [
  AuthenticationService,
  SettingsService,
  TaskService
];

const SHARED_DIRECTIVES: any[] = [
  RouterOutletDirective,
  HighlightDirective
];

export {
  Queueable,
  Task,

  FormattedTimePipe,
  QueuedOnlyPipe,
  SHARED_PIPES,

  AuthenticationService,
  SettingsService,
  TaskService,
  SHARED_PROVIDERS,

  RouterOutletDirective,
  HighlightDirective,
  SHARED_DIRECTIVES
};

As you can see in the new refactored facade, the Highlight directive is part of the SHARED_DIRECTIVES symbol. Therefore, it is available for use on any component already declaring that token in its directives property, such as TasksComponent, whose template we are about to tweak now.

Open the component's template and decorate the ngFor element with an additional class, as follows:

app/tasks/tasks.component.html

<tr *ngFor="let task of tasks; let i = index"
  class="ng-animate highlight">

Reload the application and rejoice by watching how our task list flashes upon loading on screen, just to return back to its normal state. Remember that we can apply the same behavior to any other piece of DOM in our application just by importing the directive and binding the class name in the DOM element of our choice. However, you are probably wondering why we built all this boilerplate for delivering just a flashy effect. Wouldn't it be easier to wrap everything around a CSS class perhaps? Well, that is correct... unless you want to interact with the animation, and that is what we are going to do next.

Interacting with our directive from the template

In fairness, having a directive triggering an animation like this makes no sense, but it would be great if we could interact with the animation. Moreover, if we could actually interact right from the template. To do so, we can assign an exportable token name to our directive so we can refer to it from the same element intervened by the directive. Do you remember how we used to refer to the ngForm directive when handling forms? Here, we take advantage of the same technique, as we will see later. First, proceed to update the Highlight directive by adding a new property to the directive setup named exportAs, with the value highlight. The value defined there will become the name we should refer to when trying to access the directive API from within outside. How shall we do this? A little bit of patience, first let's ditch the ngOnInit method by changing its name to colorize, thereby removing the type from the first line of imports and the interface implementation from the class. The code is as follows:

app/shared/directives/highlight.directive.ts

import { Directive, ElementRef } from '@angular/core';
import { AnimationBuilder } from '@angular/platform-browser/src/animate/animation_builder';
import { CssAnimationBuilder } from '@angular/platform-browser/src/animate/css_animation_builder';

@Directive({
  selector: '.pomodoro-highlight',
  providers: [AnimationBuilder],
  exportAs: 'pomodoroHighlight'
})
export default class HighlightDirective {
  cssAnimationBuilder: CssAnimationBuilder;

  constructor(
    private animationBuilder: AnimationBuilder,
    private elementRef: ElementRef) {

    this.cssAnimationBuilder = animationBuilder.css()
      .setDuration(300)
      .setToStyles({ backgroundColor: '#fff5a0' });
  }

  colorize() {
    let animation = this.cssAnimationBuilder.start(
      this.elementRef.nativeElement
    );

    animation.onComplete(() => {
      animation.applyStyles({ backgroundColor: 'inherit' });
    });
  }
}

Rerun the example. Now the table does not flash upon loading, which is fine. With all this in place, it's time to update our template. First, we will add a local reference named row (or whatever name you fancy) in the same HTML node impacted by the directive, pointing to highlight which is, as we now know, the public name of our directive. Same as we used to do when referencing the state and validity of our forms with ngForm, now we can access the directive public API, which exposes the colorize() method we just created out of the former ngOnInit interface method. We can safely execute that method now just by pointing to the local template reference, like this:

app/tasks/tasks.component.html

<tr *ngFor="let task of tasks; let i = index"
    class="ng-animate pomodoro-highlight"
    #row="pomodoroHighlight"
    (click)="row.colorize()">

Reload the application and click on any task, queuing it up and off. Its row will flash momentarily.

Tip

In the implementation of the Highlight directive, we hardcoded the CSS value in body of the directive. In order to make this directive more sustainable and scalable, we should prevent this approach in larger applications. As a personal exercise, we would suggest you to refactor the directive to use an attribute selector, such as [pomodoroHighlight], whose value is parsed by a class member annotated with the @Input decorator, so you can configure custom flashing colors when binding the directive in your component templates.

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

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