
Building Angular directives to wrap HTML audio
If you have worked a lot with JavaScript and jQuery, you may have realized we have purposefully shied away from directly accessing the DOM for any of our component implementations. There has not been a need to do it. The Angular data-binding infrastructure, including property, attribute, and event binding, has helped us manipulate HTML without touching the DOM.
For the audio element too, the access pattern should be Angularish. In Angular, the only place where direct DOM manipulation is acceptable and practiced is inside directives. Let's create a directive that wraps access to audio elements.
Navigate to trainer/src/app/shared and run this command to generate a template directive:
ng generate directive my-audio
Since the directive is added to the shared module, it needs to be exported too. Add the MyAudioDirective reference in the exports array too (shared.module.ts). Then update the directive definition with the following code:
import {Directive, ElementRef} from '@angular/core'; @Directive({ selector: 'audio', exportAs: 'MyAudio' }) export class MyAudioDirective { private audioPlayer: HTMLAudioElement; constructor(element: ElementRef) { this.audioPlayer = element.nativeElement; } }
The MyAudioDirective class is decorated with @Directive. The @Directive decorator is similar to the @Component decorator except we cannot have an attached view. Therefore, no template or templateUrl is allowed!
The preceding selector property allows the framework to identify where to apply the directive. We have replaced the generated [abeMyAudioDirective] attribute selector with just audio. Using audio as the selector makes our directive load for every <audio> tag used in HTML. The new selector works as an element selector.
The use of exportAs becomes clear when we use this directive in view templates.
The ElementRef object injected in the constructor is the Angular element (audio in this case) for which the directive is loaded. Angular creates the ElementRef instance for every component and directive when it compiles and executes the HTML template. When requested in the constructor, the DI framework locates the corresponding ElementRef and injects it. We use ElementRef to get hold of the underlying audio element in the code (the instance of HTMLAudioElement). The audioPlayer property holds this reference.
The directive now needs to expose an API to manipulate the audio player. Add these functions to the MyAudioDirective directive:
stop() { this.audioPlayer.pause(); } start() { this.audioPlayer.play(); } get currentTime(): number { return this.audioPlayer.currentTime; } get duration(): number { return this.audioPlayer.duration; } get playbackComplete() { return this.duration == this.currentTime; }
The MyAudioDirective API has two functions (start and stop) and three getters (currentTime, duration, and a Boolean property called playbackComplete). The implementations for these functions and properties just wrap the audio element functions.
To understand how we use the audio directive, let's create a new component that manages audio playback.