Book Image

Building Large-Scale Web Applications with Angular

By : Chandermani Arora, Kevin Hennessy, Christoffer Noring, Doguhan Uluca
Book Image

Building Large-Scale Web Applications with Angular

By: Chandermani Arora, Kevin Hennessy, Christoffer Noring, Doguhan Uluca

Overview of this book

<p>If you have been burnt by unreliable JavaScript frameworks before, you will be amazed by the maturity of the Angular platform. Angular enables you to build fast, efficient, and real-world web apps. In this Learning Path, you'll learn Angular and to deliver high-quality and production-grade Angular apps from design to deployment.</p> <p>You will begin by creating a simple fitness app, using the building blocks of Angular, and make your final app, Personal Trainer, by morphing the workout app into a full-fledged personal workout builder and runner with an advanced directive building - the most fundamental and powerful feature of Angular.</p> <p>You will learn the different ways of architecting Angular applications using RxJS, and some of the patterns that are involved in it. Later you’ll be introduced to the router-first architecture, a seven-step approach to designing and developing mid-to-large line-of-business apps, along with popular recipes. By the end of this book, you will be familiar with the scope of web development using Angular, Swagger, and Docker, learning patterns and practices to be successful as an individual developer on the web or as a team in the Enterprise.</p> <p>This Learning Path includes content from the following Packt products:</p> <p><span style="background-color: transparent;">•Angular 6 by Example by Chandermani Arora, Kevin Hennessy&nbsp;</span><br /><span style="background-color: transparent;">•Architecting Angular Applications with Redux, RxJS, and NgRx by Christoffer Noring</span><br /><span style="background-color: transparent;">•Angular 6 for Enterprise-Ready Web Applications by Doguhan Uluca</span></p>
Table of Contents (23 chapters)
Title Page
Copyright
Contributors
About Packt
Preface
Index

Cross-component communication using Angular events


It's time now to look at eventing in more depth. Let's add audio support to 7-Minute Workout.

Tracking exercise progress with audio

For the 7-Minute Workout app, adding sound support is vital. One cannot exercise while constantly staring at the screen. Audio clues help the user perform the workout effectively as they can just follow the audio instructions.

Here is how we are going to support exercise tracking using audio clues:

  • A ticking clock soundtrack progress during the exercise
  • A half-way indicator sounds, indicating that the exercise is halfway through
  • An exercise-completion audio clip plays when the exercise is about to end
  • An audio clip plays during the rest phase and informs users about the next exercise

There will be an audio clip for each of these scenarios.

Modern browsers have good support for audio. The HTML5 <audio> tag provides a mechanism to embed audio clips into HTML content. We too will use the <audio> tag to play back our clips.

Since the plan is to use the HTML <audio> element, we need to create a wrapper directive that allows us to control audio elements from Angular. Remember that directives are HTML extensions without a view.

Note

The checkpoint3.4 Git and the trainer/static/audio folder contain all the audio files used for playback; copy them first. If you are not using Git, a snapshot of the chapter code is available at http://bit.ly/ng6be-checkpoint-3-4. Download and unzip the contents and copy the audio files.

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

Note

Since it is the first time we are creating a directive, we encourage you to look at the generated code.

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.

Note

In a standard scenario, directive selectors are attribute-based (such as [abeMyAudioDirective] for the generated code), which helps us identify where the directive has been applied. We deviate from this norm and use an element selector for the MyAudioDirective directive. We want this directive to be loaded for every audio element, and it becomes cumbersome to go to each audio declaration and add a directive-specific attribute. Hence 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 (currentTimeduration, and a Boolean property called playbackComplete). The implementations for these functions and properties just wrap the audio element functions.

Note

Learn about these audio functions from the MDN documentation here: http://bit.ly/html-media-element.

To understand how we use the audio directive, let's create a new component that manages audio playback.

Creating WorkoutAudioComponent for audio support

If we go back and look at the audio cues that are required, there are four distinct audio cues, and hence we are going to create a component with five embedded <audio> tags (two audio tags work together for next-up audio).

From the command line go to the trainer/src/app/workout-runner folder and add a new WorkoutAudioComponent component using Angular CLI. 

Open workout-audio.component.html and replace the existing view template with this HTML snippet:

<audio #ticks="MyAudio" loop src="/assets/audio/tick10s.mp3"></audio>
<audio #nextUp="MyAudio" src="/assets/audio/nextup.mp3"></audio>
<audio #nextUpExercise="MyAudio" [src]="'/assets/audio/' + nextupSound"></audio>
<audio #halfway="MyAudio" src="/assets/audio/15seconds.wav"></audio>
<audio #aboutToComplete="MyAudio" src="/assets/audio/321.wav"></audio> 

 

 

 

 

There are five <audio> tags, one for each of the following:

  • Ticking audio: The first audio tag produces the ticking sound and is started as soon as the workout starts.
  • Next up audio and exercise audio: There next two audio tags work together. The first tag produces the "Next up" sound. And the actual exercise audio is handled by the third tag (in the preceding code snippet).
  • Halfway audio: The fourth audio tag plays halfway through the exercise.
  • About to complete audio: The final audio tag plays a piece to denote the completion of an exercise.

Did you notice the usage of the # symbol in each of the audio tags? There are some variable assignments prefixed with #. In the Angular world, these variables are known as template reference variables or at times template variables.

As the platform guide defines:

A template reference variable is often a reference to a DOM element or directive within a template.

Note

Don't confuse them with the template input variables that we have used with the ngFor directive earlier, *ngFor="let video of videos". The template input variable's (video in this case) scope is within the HTML fragment it is declared, whereas the template reference variable can be accessed across the entire template.

Look at the last section where MyAudioDirective was defined. The exportAs metadata is set to MyAudio. We repeat that same MyAudio string while assigning the template reference variable for each audio tag:

#ticks="MyAudio"

The role of exportAs is to define the name that can be used in the view to assign this directive to a variable. Remember, a single element/component can have multiple directives applied to it. exportAs allows us to select which directive should be assigned to a template-reference variable based on what is on the right side of equals.

Typically, template variables, once declared, give access to the view element/component they are attached to, to other parts of the view, something we will discuss shortly. But in our case, we will use template variables to refer to the multiple MyAudioDirective from the parent component's code. Let's understand how to use them.

 

Update the generated workout-audio.compnent.ts with the following outline:

import { Component, OnInit, ViewChild } from '@angular/core';
import { MyAudioDirective } from '../../shared/my-audio.directive';

@Component({
 ...
})
export class WorkoutAudioComponent implements OnInit {
  @ViewChild('ticks') private ticks: MyAudioDirective;
  @ViewChild('nextUp') private nextUp: MyAudioDirective;
  @ViewChild('nextUpExercise') private nextUpExercise: MyAudioDirective;
  @ViewChild('halfway') private halfway: MyAudioDirective;
  @ViewChild('aboutToComplete') private aboutToComplete: MyAudioDirective;
  private nextupSound: string;

  constructor() { } 
  ...
}

The interesting bit in this outline is the @ViewChild decorator against the five properties. The @ViewChild decorator allows us to inject a child component/directive/element reference into its parent. The parameter passed to the decorator is the template variable name, which helps DI match the element/directive to inject. When Angular instantiates the main WorkoutAudioComponent, it injects the corresponding audio directives based on the @ViewChild decorator and the template reference variable name passed. Let's complete the basic class implementation before we look at @ViewChild in detail.

Note

Without exportAs set on the MyAudioDirective directive, the @ViewChild injection injects the related ElementRef instance instead of the MyAudioDirective instance. We can confirm this by removing the exportAs attribute from myAudioDirective and then looking at the injected dependencies in WorkoutAudioComponent.

The remaining task is to just play the correct audio component at the right time. Add these functions to WorkoutAudioComponent:

stop() {
    this.ticks.stop();
    this.nextUp.stop();
    this.halfway.stop();
    this.aboutToComplete.stop();
    this.nextUpExercise.stop();
  }
  resume() {
    this.ticks.start();
    if (this.nextUp.currentTime > 0 && !this.nextUp.playbackComplete) 
        { this.nextUp.start(); }
    else if (this.nextUpExercise.currentTime > 0 && !this.nextUpExercise.playbackComplete)
         { this.nextUpExercise.start(); }
    else if (this.halfway.currentTime > 0 && !this.halfway.playbackComplete) 
        { this.halfway.start(); }
    else if (this.aboutToComplete.currentTime > 0 && !this.aboutToComplete.playbackComplete) 
        { this.aboutToComplete.start(); }
  }

  onExerciseProgress(progress: ExerciseProgressEvent) {
    if (progress.runningFor === Math.floor(progress.exercise.duration / 2)
      && progress.exercise.exercise.name != 'rest') {
      this.halfway.start();
    }
    else if (progress.timeRemaining === 3) {
      this.aboutToComplete.start();
    }
  }

  onExerciseChanged(state: ExerciseChangedEvent) {
    if (state.current.exercise.name === 'rest') {
      this.nextupSound = state.next.exercise.nameSound;
      setTimeout(() => this.nextUp.start(), 2000);
      setTimeout(() => this.nextUpExercise.start(), 3000);
    }
  } 

Having trouble writing these functions? They are available in the checkpoint3.3 Git branch.

There are two new model classes used in the preceding code. Add their declarations to model.ts, as follows (again available in checkpoint3.3):

export class ExerciseProgressEvent {
    constructor(
        public exercise: ExercisePlan,
        public runningFor: number,
        public timeRemaining: number,
        public workoutTimeRemaining: number) { }
}

export class ExerciseChangedEvent {
    constructor(
        public current: ExercisePlan,
        public next: ExercisePlan) { }
} 

These are model classes to track progress events. The WorkoutAudioComponent implementation consumes this data. Remember to import the reference for ExerciseProgressEvent and ExerciseProgressEvent in workout-audio.component.ts.

To reiterate, the audio component consumes the events by defining two event handlers: onExerciseProgress and onExerciseChanged. How the events are generated becomes clear as we move along.

The start and resume functions stop and resume audio whenever a workout starts, pauses, or completes. The extra complexity in the resume function it to tackle cases when the workout was paused during next up, about to complete, or half-way audio playback. We just want to continue from where we left off.

The onExerciseProgress function should be called to report the workout progress. It's used to play the halfway audio and about-to-complete audio based on the state of the workout. The parameter passed to it is an object that contains exercise progress data.

The onExerciseChanged function should be called when the exercise changes. The input parameter contains the current and next exercise in line and helps WorkoutAudioComponent to decide when to play the next up exercise audio.

We touched upon two new concepts in this section: template reference variables and injecting child elements/directives into the parent. It's worth exploring these two concepts in more detail before we continue with the implementation. We'll start with learning more about template reference variables.

Understanding template reference variables

Template reference variables are created on the view template and are mostly consumed from the view. As you have already learned, these variables can be identified by the # prefix used to declare them.

One of the greatest benefits of template variables is that they facilitate cross-component communication at the view template level. Once declared, such variables can be referenced by sibling elements/components and their children. Check out the following snippet:

    <input #emailId type="email">Email to {{emailId.value}} 
    <button (click)= "MailUser(emaild.value)">Send</button> 

This snippet declares a template variable, emailId, and then references it in the interpolation and the button click expression.

The Angular templating engine assigns the DOM object for input (an instance of HTMLInputElement) to the emailId variable. Since the variable is available across siblings, we use it in a button's click expression.

Template variables work with components too. We can easily do this:

    <trainer-app> 
     <workout-runner #runner></workout-runner> 
     <button (click)= "runner.start()">Start Workout</button> 
    </trainer-app> 

In this case, runner has a reference to the WorkoutRunnerComponent object, and the button is used to start the workout.

Note

The ref- prefix is the canonical alternative to #. The #runner variable can also be declared as ref-runner.

Template variable assignment

You may not have noticed but there is something interesting about the template variable assignments described in the last few sections. To recap, the three examples that we have used are:

<audio #ticks="MyAudio" loop src="/static/audio/tick10s.mp3"></audio> 

<input #emailId type="email">Email to {{emailId.value}}

<workout-runner #runner></workout-runner> 

What got assigned to the variable depends on where the variable was declared. This is governed by rules in Angular:

  • If a directive is present on the element, such as MyAudioDirective in the first example shown previously, the directive sets the value. The MyAudioDirective directive sets the ticks variable to an instance of MyAudioDirective.
  • If there is no directive present, either the underlying HTML DOM element is assigned or a component object is assigned (as shown in the input and workout-runner examples).

We will be employing this technique to implement the workout audio component integration with the workout runner component. This introduction gives us the head start that we need.

The other new concept that we promised to cover is child element/directive injection using the ViewChild and ViewChildren decorators.

Using the @ViewChild decorator

The @ViewChild decorator instructs the Angular DI framework to search for some specific child component/directive/element in the component tree and inject it into the parent. This allows the parent component to interact with child components/element using the reference to the child, a new communication pattern!

In the preceding code, the audio element directive (the MyAudioDirective class) is injected into the WorkoutAudioComponent code.

To establish the context, let's recheck a view fragment from WorkoutAudioComponent:

    <audio #ticks="MyAudio" loop src="/static/audio/tick10s.mp3"></audio> 

Angular injects the directive (MyAudioDirective) into the WorkoutAudioComponent property: ticks. The search is done based on the selector passed to the @ViewChild decorator. Let's see the audio example again:

  @ViewChild('ticks') private ticks: MyAudioDirective;

The selector parameter on ViewChild can be a string value, in which case Angular searches for a matching template variable, as before.

Or it can be a type. This is valid and should inject an instance of MyAudioDirective:

@ViewChild(MyAudioDirective) private ticks: MyAudioDirective; 

However, it does not work in our case. Why? Because there are multiple MyAudioDirective directives declared in the WorkoutAudioComponent view, one for each of the <audio> tags. In such a scenario, the first match is injected. Not very useful. Passing the type selector would have worked if there was only one <audio> tag in the view!

Note

Properties decorated with @ViewChild are sure to be set before the ngAfterViewInit event hook on the component is called. This implies such properties are null if accessed inside the constructor.

Angular also has a decorator to locate and inject multiple child components/directives: @ViewChildren.

The @ViewChildren decorator

@ViewChildren works similarly to @ViewChild, except it can be used to inject multiple child types into the parent. Again taking the previous audio component above as an example, using @ViewChildren, we can get all the MyAudioDirective directive instances in WorkoutAudioComponent, as shown here:

@ViewChildren(MyAudioDirective) allAudios: QueryList<MyAudioDirective>; 

Look carefully; allAudios is not a standard JavaScript array, but a custom class, QueryList<Type>. The QueryList class is an immutable collection that contains the reference to the components/directives that Angular was able to locate based on the filter criteria passed to the @ViewChildren decorator. The best thing about this list is that Angular will keep this list in sync with the state of the view. When directives/components get added/removed from the view dynamically, this list is updated too. Components/directives generated using ng-for are a prime example of this dynamic behavior. Consider the preceding @ViewChildren usage and this view template:

<audio *ngFor="let clip of clips" src="/static/audio/ "+{{clip}}></audio> 

The number of MyAudioDirective directives created by Angular depends upon the number of clips. When @ViewChildren is used, Angular injects the correct number of MyAudioDirective instances into the allAudio property and keeps it in sync when items are added or removed from the clips array.

While the usage of @ViewChildren allows us to get hold of all MyAudioDirective directives, it cannot be used to control the playback. You see, we need to get hold of individual MyAudioDirective instances as the audio playback timing varies. Hence the distinct @ViewChild implementation.

Once we get hold of the MyAudioDirective directive attached to each audio element, it is just a matter of playing the audio tracks at the right time.

Integrating WorkoutAudioComponent

While we have componentized the audio playback functionality into WorkoutAudioComponent, it is and always will be tightly coupled to the WorkoutRunnerComponent implementation. WorkoutAudioComponent derives its operational intelligence from WorkoutRunnerComponent. Hence the two components need to interact. WorkoutRunnerComponent needs to provide the WorkoutAudioComponent state change data, including when the workout started, exercise progress, workout stopped, paused, and resumed.

One way to achieve this integration would be to use the currently exposed WorkoutAudioComponent API (stop, resume, and other functions) from WorkoutRunnerComponent.

Something can be done by injecting WorkoutAudioComponent into WorkoutRunnerComponent, as we did earlier when we injected MyAudioDirective into WorkoutAudioComponent.

Declare the WorkoutAudioComponent in the WorkoutRunnerComponent's view, such as:

<div class="row pt-4">...</div>
<abe-workout-audio></abe-workout-audio>

Doing so gives us a reference to the WorkoutAudioComponent inside the WorkoutRunnerComponent implementation:

@ViewChild(WorkoutAudioComponent) workoutAudioPlayer: WorkoutAudioComponent; 

The WorkoutAudioComponent functions can then be invoked from WorkoutRunnerComponent from different places in the code. For example, this is how pause would change:

    pause() { 
      clearInterval(this.exerciseTrackingInterval); 
      this.workoutPaused = true; 
      this.workoutAudioPlayer.stop(); 
    }

And to play the next-up audio, we would need to change parts of the startExerciseTimeTracking function:

this.startExercise(next); 
this.workoutAudioPlayer.onExerciseChanged(new ExerciseChangedEvent(next, this.getNextExercise()));

This is a perfectly viable option where WorkoutAudioComponent becomes a dumb component controlled by WorkoutRunnerComponent. The only problem with this solution is that it adds some noise to the WorkoutRunnerComponent implementation. WorkoutRunnerComponent now needs to manage audio playback too.

There is an alternative, however.

WorkoutRunnerComponent can expose events that are triggered during different times of workout execution, such as workout started, exercise started, and workout paused. The advantage of having WorkoutRunnerComponent expose events is that it allows us to integrate other components/directives with WorkoutRunnerComponent using the same events. Be it the WorkoutAudioComponent or components we create in future.

Exposing WorkoutRunnerComponent events

Till now we have only explored how to consume events. Angular allows us to raise events too. Angular components and directives can expose custom events using the EventEmitter class and the @Output decorator.

Add these event declarations to WorkoutRunnerComponent at the end of the variable declaration section:

workoutPaused: boolean; 
@Output() exercisePaused: EventEmitter<number> = 
    new EventEmitter<number>();
@Output() exerciseResumed: EventEmitter<number> = 
    new EventEmitter<number>()
@Output() exerciseProgress:EventEmitter<ExerciseProgressEvent> = 
    new EventEmitter<ExerciseProgressEvent>();
@Output() exerciseChanged: EventEmitter<ExerciseChangedEvent> = 
    new EventEmitter<ExerciseChangedEvent>();
@Output() workoutStarted: EventEmitter<WorkoutPlan> = 
    new EventEmitter<WorkoutPlan>();
@Output() workoutComplete: EventEmitter<WorkoutPlan> = 
    new EventEmitter<WorkoutPlan>();

The names of the events are self-explanatory, and within our WorkoutRunnerComponent implementation, we need to raise them at the appropriate times.

Remember to add the ExerciseProgressEvent and ExerciseChangeEvent imports to the model already declared on top. And add the Output and EventEmitter imports to @angular/core.

Let's try to understand the role of the @Output decorator and the EventEmitter class.

The @Output decorator

We covered a decent amount of Angular eventing capabilities in this chapter. Specifically, we learned how we can consume any event on a component, directive, or DOM element using the bracketed () syntax. How about raising our own events?

In Angular, we can create and raise our own events, events that signify something noteworthy has happened in our component/directive. Using the @Output decorator and the EventEmitter class, we can define and raise custom events.

Note

It's also a good time to refresh what we learned about events.

Remember this: it is through events that components can communicate with the outside world. When we declare:

@Output() exercisePaused: EventEmitter<number> = new EventEmitter<number>(); 

It signifies that WorkoutRunnerComponent exposes an event, exercisePaused (raised when the workout is paused).

To subscribe to this event, we can do the following:

<abe-workout-runner (exercisePaused)="onExercisePaused($event)"></abe-workout-runner>

This looks absolutely similar to how we did the DOM event subscription in the workout runner template. See this sample stipped from the workout-runner's view:

<div id="pause-overlay" (click)="pauseResumeToggle()" (window:keyup)="onKeyPressed($event)"> 

The @Output decorator instructs Angular to make this event available for template binding. Events created without the @Output decorator cannot be referenced in HTML.

Note

The @Output decorator can also take a parameter, signifying the name of the event. If not provided, the decorator uses the property name: @Output("workoutPaused") exercisePaused: EventEmitter<number> .... This declares a workoutPaused event instead of exercisePaused.

Like any decorator, the @Output decorator is there just to provide metadata for the Angular framework to work with. The real heavy lifting is done by the EventEmitter class.

Eventing with EventEmitter

Angular embraces reactive programming (also dubbed Rx-style programming) to support asynchronous operations with events. If you are hearing this term for the first time or don't have much idea about what reactive programming is, you're not alone.

Reactive programming is all about programming against asynchronous data streams. Such a stream is nothing but a sequence of ongoing events ordered based on the time they occur. We can imagine a stream as a pipe generating data (in some manner) and pushing it to one or more subscribers. Since these events are captured asynchronously by subscribers, they are called asynchronous data streams.

The data can be anything, ranging from browser/DOM element events to user input to loading remote data using AJAX. With Rx style, we consume this data uniformly.

In the Rx world, there are Observers and Observables, a concept derived from the very popular Observer design patternObservables are streams that emit data. Observers, on the other hand, subscribe to these events.

The EventEmitter class in Angular is primarily responsible for providing eventing support. It acts both as an observer and observable. We can fire events on it and it can also listen to events.

There are two functions available on EventEmitter that are of interest to us:

  • emit: As the name suggests, use this function to raise events. It takes a single argument that is the event data. emit is the observable side.
  • subscribe: Use this function to subscribe to the events raised by EventEmittersubscribe is the observer side.

Let's do some event publishing and subscriptions to understand how the preceding functions work.

Raising events from WorkoutRunnerComponent

Look at the EventEmitter declaration. These have been declared with the type parameter. The type parameter on EventEmitter signifies the type of data emitted.

Let's add the event implementation to workout-runner.component.ts, starting from the top of the file and moving down.

Add this statement to the end of the start function:

this.workoutStarted.emit(this.workoutPlan);

We use the emit function of  EventEmitter  to raise a workoutStarted event with the current workout plan as an argument.

To pause, add this line to raise the exercisePaused event:

this.exercisePaused.emit(this.currentExerciseIndex); 

To resume, add the following line:

this.exerciseResumed.emit(this.currentExerciseIndex); 

Each time, we pass the current exercise index as an argument to emit when raising the exercisePaused and exerciseResumed events.

Inside the startExerciseTimeTracking function, add the highlighted code after the call to startExercise:

this.startExercise(next); 
this.exerciseChanged.emit(new ExerciseChangedEvent(next, this.getNextExercise()));

The argument passed contains the exercise that is going to start (next) and the next exercise in line (this.getNextExercise()).

To the same function, add the highlighted code:

this.tracker.endTracking(true); 
this.workoutComplete.emit(this.workoutPlan); 
this.router.navigate(['finish']); 

The event is raised when the workout is completed.

In the same function, we raise an event that communicates the workout progress. Add this statement:

--this.workoutTimeRemaining; 
this.exerciseProgress.emit(new ExerciseProgressEvent( 
    this.currentExercise,
    this.exerciseRunningDuration, 
    this.currentExercise.duration -this.exerciseRunningDuration, 
    this.workoutTimeRemaining));

That completes our eventing implementation.

As you may have guessed, WorkoutAudioComponent now needs to consume these events. The challenge here is how to organize these components so that they can communicate with each other with the minimum dependency on each other.

Component communication patterns

As the implementation stands now, we have:

  • A basic WorkoutAudioComponent implementation
  • Augmented WorkoutRunnerComponent by exposing workout life cycle events

These two components just need to talk to each other now.

If the parent needs to communicate with its children, it can do this by:

  • Property binding: The parent component can set up a property binding on the child component to push data to the child component. For example, this property binding can stop the audio player when the workout is paused:
        <workout-audio [stopped]="workoutPaused"></workout-audio>

Property binding, in this case, works fine. When the workout is paused, the audio is stopped too. But not all scenarios can be handled using property bindings. Playing the next exercise audio or halfway audio requires a bit more control.

  • Calling functions on child components: The parent component can also call functions on the child component if it can get hold of the child component. We have already seen how to achieve this using the @ViewChild and @ViewChildren decorators in the WorkoutAudioComponent implementation. This approach and its shortcomings have also been discussed briefly in the Integrating WorkoutAudioComponent section.

There is one more not-so-good option. Instead of the parent referencing the child component, the child references the parent component. This allows the child component to call the parent component's public functions or subscribe to parent component events.

We are going to try this approach and then scrap the implementation for a better one! A lot of learning can be derived from the not-so-optimal solution we plan to implement.

Injecting a parent component into a child component

Add the WorkoutAudioComponent to the WorkoutRunnerComponent view just before the last closing div:

 <abe-workout-audio></abe-workout-audio> 

Next, inject WorkoutRunnerComponent into WorkoutAudioComponent. Open workout-audio.component.ts and add the following declaration and update the constructor:

private subscriptions: Array<any>; 
 
constructor( @Inject(forwardRef(() => WorkoutRunnerComponent)) 
    private runner: WorkoutRunnerComponent) { 
    this.subscriptions = [ 
      this.runner.exercisePaused.subscribe((exercise: ExercisePlan) => 
          this.stop()), 
      this.runner.workoutComplete.subscribe((exercise: ExercisePlan) => 
          this.stop()), 
      this.runner.exerciseResumed.subscribe((exercise: ExercisePlan) => 
          this.resume()), 
      this.runner.exerciseProgress.subscribe((progress: ExerciseProgressEvent) => 
          this.onExerciseProgress(progress)),
      this.runner.exerciseChanged.subscribe((state: ExerciseChangedEvent) =>  
          this.onExerciseChanged(state))]; 
    } 

And remember to add these imports:

    import {Component, ViewChild, Inject, forwardRef} from '@angular/core'; 
    import {WorkoutRunnerComponent} from '../workout-runner.component'  

Let's try to understand what we have done before running the app. There is some amount of trickery involved in the construction injection. If we directly try to inject WorkoutRunnerComponent into WorkoutAudioComponent, it fails with Angular complaining of not being able to find all the dependencies. Read the code and think carefully; there is a subtle dependency cycle issue lurking. WorkoutRunnerComponent is already dependent on WorkoutAudioComponent, as we have referenced WorkoutAudioComponent in the WorkoutRunnerComponent view. Now by injecting WorkoutRunnerComponent in WorkoutAudioComponent, we have created a dependency cycle.

Cyclic dependencies are challenging for any DI framework. When creating a component with a cyclic dependency, the framework has to somehow resolve the cycle. In the preceding example, we resolve the circular dependency issue by using an @Inject decorator and passing in the token created using the forwardRef() global framework function.

Once the injection is done correctly, inside the constructor, we attach a handler to the WorkoutRunnerComponent events, using the subscribe function of EventEmitter. The arrow function passed to subscribe is called whenever the event occurs with a specific event argument. We collect all the subscriptions into a subscription array. This array comes in handy when we unsubscribe, which we need to, to avoid memory leaks.

A bit about EventEmitter: the EventEmmiter subscription (subscribe function) takes three arguments:

    subscribe(generatorOrNext?: any, error?: any, complete?: any) : any 
  • The first argument is a callback, which is invoked whenever an event is emitted
  • The second argument is an error callback function, invoked when the observable (the part that is generating events) errors out
  • The final argument takes a callback function that is called when the observable is done publishing events

We have done enough to make audio integration work. Run the app and start the workout. Except for the ticking audio, all the \ audio clips play at the right time. You may have to wait some time to hear the other audio clips. What is the problem?

As it turns out, we never started the ticking audio clip at the start of the workout. We can fix it by either setting the autoplay attribute on the ticks audio element or using the component life cycle events to trigger the ticking sound. Let's take the second approach.

Using component life cycle events

The injected MyAudioDirective in WorkoutAudioComponent, shown as follows, is not available till the view is initialized:

<audio #ticks="MyAudio" loop src="/assets/audio/tick10s.mp3"></audio>
<audio #nextUp="MyAudio" src="/assets/audio/nextup.mp3"></audio>
...

We can verify it by accessing the ticks variable inside the constructor; it will be null. Angular has still not done its magic and we need to wait for the children of WorkoutAudioComponent to be initialized.

The component's life cycle hooks can help us here. The AfterViewInit event hook is called once the component's view has been initialized and hence is a safe place from which to access the component's child directives/elements. Let's do it quickly.

Update WorkoutAudioComponent by adding the interface implementation, and the necessary imports, as highlighted:

import {..., AfterViewInit} from '@angular/core'; 
... 
export class WorkoutAudioComponent implements OnInit, AfterViewInit { 
    ngAfterViewInit() { 
          this.ticks.start(); 
    }

Go ahead and test the app. The app has come to life with full-fledged audio feedback. Nice!

While everything looks fine and dandy on the surface, there is a memory leak in the application now. If, in the middle of the workout, we navigate away from the workout page (to the start or finish page) and again return to the workout page, multiple audio clips play at random times.

It seems that WorkoutRunnerComponent is not getting destroyed on route navigation, and due to this, none of the child components are destroyed, including WorkoutAudioComponent. The net result? A new WorkoutRunnerComponent is being created every time we navigate to the workout page but is never removed from the memory on navigating away.

The primary reason for this memory leak is the event handlers we have added in WorkoutAudioComponent. We need to unsubscribe from these events when the audio component unloads, or else the WorkoutRunnerComponent reference will never be dereferenced.

Another component lifecycle event comes to our rescue here: OnDestroy Add this implementation to the WorkoutAudioComponent class:

    ngOnDestroy() { 
      this.subscriptions.forEach((s) => s.unsubscribe()); 
    }

Also, remember to add references to the OnDestroy event interface as we did for AfterViewInit.

Hope the subscription array that we created during event subscription makes sense now. One-shot unsubscribe!

This audio integration is now complete. While this approach is not an awfully bad way of integrating the two components, we can do better. Child components referring to the parent component seems to be undesirable.

Note

Before proceeding, delete the code that we have added to workout-audio.component.ts from the Injecting a parent component into a child component section onward.

Sibling component interaction using events and template variables

What if WorkoutRunnerComponent and WorkoutAudioComponent were organized as sibling components? 

If WorkoutAudioComponent and WorkoutRunnerComponent become siblings, we can make good use of Angular's eventing and template reference variables. Confused? Well, to start with, this is how the components should be laid out:

    <workout-runner></workout-runner> 
    <workout-audio></workout-audio> 

Does it ring any bells? Starting from this template, can you guess how the final HTML template would look? Think about it before you proceed further.

Still struggling? As soon as we make them sibling components, the power of the Angular templating engine comes to the fore. The following template code is enough to integrate WorkoutRunnerComponent and WorkoutAudioComponent:

<abe-workout-runner (exercisePaused)="wa.stop()" 
    (exerciseResumed)="wa.resume()" 
    (exerciseProgress)= "wa.onExerciseProgress($event)" 
    (exerciseChanged)= "wa.onExerciseChanged($event)" 
    (workoutComplete)="wa.stop()" 
    (workoutStarted)="wa.resume()"> 
</abe-workout-runner> 
<abe-workout-audio #wa></abe-workout-audio> 

The WorkoutAudioComponent template variable, wa, is being manipulated by referencing the variable in the event handler expressions on WorkoutRunnerComponent. Quite elegant! We still need to solve the biggest puzzle in this approach: Where does the preceding code go? Remember, WorkoutRunnerComponent is loaded as part of route loading. Nowhere in the code have we had a statement like this:

    <workout-runner></workout-runner> 

We need to reorganize the component tree and bring in a container component that can host WorkoutRunnerComponent and WorkoutAudioComponent. The router then loads this container component instead of WorkoutRunnerComponent. Let's do it.

Generate a new component code from command line by navigating to trainer/src/app/workout-runner and executing:

ng generate component workout-container -is

Copy the HTML code with the events described to the template file. The workout container component is ready.

We just need to rewire the routing setup. Open app-routing.module.ts. Change the route for the workout runner and add the necessary import:

import {WorkoutContainerComponent} 
        from './workout-runner/workout-container/workout-container.component'; 
..
{ path: '/workout', component: WorkoutContainerComponent },

And we have a working audio integration that is clear, concise, and pleasing to the eye!

It's time now to wrap up the chapter, but not before addressing the video player dialog glitch introduced in the earlier sections. The workout does not stop/pause when the video player dialog is open.

We are not going to detail the fix here, and urge the readers to give it a try without consulting the checkpoint3.4 code.

Here is an obvious hint. Use the eventing infrastructure!

And another one: raise events from VideoPlayerComponent, one for each playback started and ended.

And one last hint: the open function on the dialog service (Modal) returns a promise, which is resolved when the dialog is closed.

Note

If you are having a problem with running the code, look at the checkpoint3.4 Git branch for a working version of what we have done thus far. Or if you are not using Git, download the snapshot of checkpoint3.4 (a ZIP file) from http://bit.ly/ng6be-checkpoint-3-4. Refer to the README.md file in the trainer folder when setting up the snapshot for the first time.