Our first component - WorkoutRunnerComponent

WorkoutRunnerComponent, is the central piece of our 7 Minute Workout app and it will contain the logic to execute the workout.

What we are going to do in the WorkoutRunnerComponent implementation is as follows:

  1. Start the workout
  2. Show the workout in progress and show the progress indicator
  3. After the time elapses for an exercise, show the next exercise
  4. Repeat this process until all the exercises are over

We are ready to create (or scaffold) our component.

From the command line, navigate to the src/app folder and execute the following ng command:

ng generate component workout-runner -is

The generator generates a bunch of files (three) in the workout-runner folder and updates the module declaration in WorkoutRunnerModule to include the newly created WorkoutRunnerComponent.

The -is flag is used to stop generation of a separate CSS file for the component. Since we are using global styles, we do not need component-specific styles.
Remember to run this command from the  src/app folder and not from the src/app/workout-runner folder. If we run the preceding command from src/app/workout-runner, Angular CLI will create a new subfolder with the workout-runner component definition.

The preceding ng generate command for component generates these three files:

  • <component-name>.component.html: This is the component's view HTML.
  • <component-name>.component.spec.ts: Test specification file used in unit testing. 
  • <component-name>.component.ts: Main component file containing component implementation.

Again, we will encourage you to have a look at the generated code to understand what gets generated. The Angular CLI component generator saves us some keystrokes and once generated, the boilerplate code can evolve as desired.

While we see only four decorator metadata properties (such as templateUrl), the component decorator supports some other useful properties too. Look at the Angular documentation for component to learn more about these properties and their application. 

An observant reader might have noticed that the generated selector property value has a prefix abe; this is intentional. Since we are extending the HTML domain-specific language (DSL) to incorporate a new element, the prefix abe helps us demarcate HTML extensions that we have developed. So instead of using <workout-runner></workout-runner> in HTML we use <abe-workout-runner></abe-workout-runner>. The prefix value has been configured in angular.json, see the prefix property.

Always add a prefix to your component selector.

We now have the WorkoutRunnerComponent boilerplate; let's start adding the implementation, starting with adding the model reference.

In workout-runner.component.ts, import all the workout models:

import {WorkoutPlan, ExercisePlan, Exercise} from './model';

Next, we need to set up the workout data. Let's do that by adding some code in the generated ngOnInit function and related class properties to the WorkoutRunnerComponent class:

workoutPlan: WorkoutPlan; 
restExercise: ExercisePlan; 
ngOnInit() { 
   this.workoutPlan = this.buildWorkout(); 
   this.restExercise = new ExercisePlan( 
     new Exercise('rest', 'Relax!', 'Relax a bit', 'rest.png'),  
     this.workoutPlan.restBetweenExercise);   
} 

ngOnInit is a special function that Angular calls when a component is initialized. We will talk about ngOnInit shortly.

The buildWorkout on WorkoutRunnerComponent sets up the complete workout, as we will define shortly. We also initialize a restExercise variable to track even the rest periods as exercise (note that restExercise is an object of type ExercisePlan).

The buildWorkout function is a lengthy function, so it's better to copy the implementation from the workout runner's implementation available in Git branch checkpoint2.1 (http://bit.ly/ng6be-2-1-workout-runner-component-ts). The buildWorkout code looks as follows:

buildWorkout(): WorkoutPlan { 
let workout = new WorkoutPlan('7MinWorkout',  
"7 Minute Workout", 10, []); 
   workout.exercises.push( 
      new ExercisePlan( 
        new Exercise( 
          'jumpingJacks', 
          'Jumping Jacks', 
          'A jumping jack or star jump, also called side-straddle hop
           is a physical jumping exercise.', 
          'JumpingJacks.png', 
          'jumpingjacks.wav', 
          `Assume an erect position, with feet together and 
           arms at your side. ...`, 
          ['dmYwZH_BNd0', 'BABOdJ-2Z6o', 'c4DAnQ6DtF8']), 
        30)); 
   // (TRUNCATED) Other 11 workout exercise data. 
   return workout; 
} 

This code builds the WorkoutPlan object and pushes the exercise data into the exercises array (an array of ExercisePlan objects), returning the newly built workout.

The initialization is complete; now, it's time to actually implement the start workout. Add a start function to the WorkoutRunnerComponent implementation, as follows:

start() { 
   this.workoutTimeRemaining =  
   this.workoutPlan.totalWorkoutDuration(); 
   this.currentExerciseIndex = 0;  
   this.startExercise(this.workoutPlan.exercises[this.currentExerciseIndex]); 
} 

Then declare the new variables used in the function at the top, with other variable declarations:

workoutTimeRemaining: number; 
currentExerciseIndex: number; 

The workoutTimeRemaining variable tracks the total time remaining for the workout, and currentExerciseIndex tracks the currently executing exercise index. The call to startExercise actually starts an exercise. This is how the code for startExercise looks:

startExercise(exercisePlan: ExercisePlan) { 
    this.currentExercise = exercisePlan; 
    this.exerciseRunningDuration = 0; 
    const intervalId = setInterval(() => { 
      if (this.exerciseRunningDuration >=  this.currentExercise.duration) { 
          clearInterval(intervalId);  
      } 
      else { this.exerciseRunningDuration++; } 
    }, 1000); 
} 

We start by initializing currentExercise and exerciseRunningDuration. The currentExercise variable tracks the exercise in progress and exerciseRunningDuration tracks its duration. These two variables also need to be declared at the top:

currentExercise: ExercisePlan; 
exerciseRunningDuration: number; 

We use the setInterval JavaScript function with a delay of one second (1,000 milliseconds) to make progress. Inside the setInterval callback, exerciseRunningDuration is incremented with each passing second. The nested clearInterval call stops the timer once the exercise duration lapses.

TypeScript arrow functions
The callback parameter passed to setInterval ( ()=>{...}) is a lambda function (or an arrow function in ES 2015). Lambda functions are short-form representations of anonymous functions, with added benefits. You can learn more about them at http://bit.ly/ng2be-ts-arrow-functions.

The first cut of the component is almost complete, except it currently has a static view (UI) and hence we cannot verify the implementation. We can quickly rectify this situation by adding a rudimentary view definition. Open workout-runner.component.ts, comment out the templateUrl property, and add an inline template property (template) and set it to the following:

template: `<pre>Current Exercise: {{currentExercise | json}}</pre>
<pre>Time Left: {{currentExercise.duration - exerciseRunningDuration}}</pre>`,
Strings enclosed in backticks ( ` `) are a new addition to ES2015. Also called template literals, such string literals can be multiline and allow expressions to be embedded inside (not to be confused with Angular expressions). Look at the MDN article at http://bit.ly/template-literals for more details.
Inline versus external view template
The preceding  template property is an example of inline component template. This allows the component developer to specify the component template inline instead of using a separate HTML file. The inline template approach generally works for components with a trivial view. Inline templates have a disadvantage: formatting HTML becomes difficult and IDE support is very limited as the content is treated as a string literal. When we externalize HTML, we can develop a template as a normal HTML document. We recommend you use an external template file (specified using templateUrl) for elaborate views. Angular CLI by default generates an external template reference, but we can affect this behavior by passing the  --inline-template flag to the  ng component generation command, such as --inline-template true.

The preceding template HTML will render the raw ExercisePlan object and the exercise time remaining. It has an interesting expression inside the first interpolation: currentExercise | json. The currentExercise property is defined in WorkoutRunnerComponent, but what about the | symbol and what follows it (json)? In the Angular world, it is called a pipe. The sole purpose of a pipe is to transform/format template data.

The json pipe here does JSON data formatting. You will learn more about pipes later in this chapter, but to get a general sense of what the json pipe does, we can remove the json pipe plus the | symbol and render the template; we are going to do this next.

To render the new WorkoutRunnerComponent implementation, it has to be added to the root component's view. Modify src/components/app/app.component.html and replace the h3 tag with the following code:

<div class="container body-content app-container">
<abe-workout-runner></abe-workout-runner>
</div>

While the implementation may look complete, there is a crucial piece missing. Nowhere in the code do we actually start the workout. The workout should start as soon as we load the page.

Component lifecycle hooks are going to rescue us!