Building  Large-Scale Web Applications with Angular
上QQ阅读APP看书,第一时间看更新

Component lifecycle hooks

The life of an Angular component is eventful. Components get created, change state during their lifetime, and finally, they are destroyed. Angular provides some lifecycle hooks/functions that the framework invokes (on the component) when such an event occurs. Consider these examples:

  • When a component is initialized, Angular invokes ngOnInit
  • When a component's data-bound properties change, Angular invokes ngOnChanges
  • When a component is destroyed, Angular invokes ngOnDestroy

As developers, we can tap into these key moments and perform some custom logic inside the respective component.

The hook we are going to utilize here is ngOnInit. The ngOnInit function gets fired the first time the component's data-bound properties are initialized, but before the view initialization starts.

While ngOnInit and the class constructor seem to look similar, they have a different purpose. A constructor is a language feature and it is used to initialize class members. ngOnInit, on the other hand, is used to do some initialization stuff once the component is ready. Avoid use of a constructor for anything other than member initialization.

Update the ngOnInit function to the WorkoutRunnerComponent class with a call to start the workout:

ngOnInit() { 
... this.start(); }

Angular CLI as part of component scaffolding already generates the signature for ngOnInit. The ngOnInit function is declared on the OnInit interface, which is part of the core Angular framework. We can confirm this by looking at the import section of WorkoutRunnerComponent:

import {Component,OnInit} from '@angular/core'; 
... 
export class WorkoutRunnerComponent implements OnInit {
There are a number of other lifecycle hooks, including ngOnDestroy, ngOnChanges, and ngAfterViewInit, that components support, but we are not going to dwell on any of them here. Look at the developer guide ( https://angular.io/guide/lifecycle-hooks) on lifecycle hooks to learn more about other such hooks.
Implementing the interface ( OnInit in the preceding example) is optional. These lifecycle hooks work as long as the function name matches. We still recommend you use interfaces to clearly communicate the intent.

Time to run our app! Open the command line, navigate to the trainer folder, and type this line:

ng serve --open

The code compiles, but no UI is rendered. What is failing us? Let's look at the browser console for errors.

Open the browser's dev tools (common keyboard shortcut F12) and look at the console tab for errors. There is a template parsing error. Angular is not able to locate the abe-workout-runner component. Let's do some sanity checks to verify our setup:

  • WorkoutRunnerComponent implementation complete - check
  • Component declared in WorkoutRunnerModule- check
  • WorkoutRunnerModule imported into AppModule - check

Still, the AppComponent template cannot locate the WorkoutRunnerComponent. Is it because WorkoutRunnerComponent and AppComponent are in different modules? Indeed, that is the problem! While WorkoutRunnerModule has been imported into AppModuleWorkoutRunnerModule still does not export the new WorkoutRunnerComponent that will allow AppComponent to use it.

Remember, adding a component/directive/pipe to the  declaration section of a module makes them available inside the module. It's only after we export the component/directive/pipe that it becomes available to be used across modules.

Let's export WorkoutRunnerComponent by updating the export array of the WorkoutRunnerModule declaration to the following:

declarations: [WorkoutRunnerComponent],
exports:[WorkoutRunnerComponent]

This time, we should see the following output:

Always export artifacts defined inside an Angular module if you want them to be used across other modules.

The model data updates with every passing second! Now you'll understand why interpolations ({{ }}) are a great debugging tool.

This will also be a good time to try rendering currentExercise without the json pipe and see what gets rendered.

We are not done yet! Wait long enough on the page and we realize that the timer stops after 30 seconds. The app does not load the next exercise data. Time to fix it!

Update the code inside the setInterval function:

if (this.exerciseRunningDuration >=  this.currentExercise.duration) { 
   clearInterval(intervalId); 
   const next: ExercisePlan = this.getNextExercise(); 
   if (next) {
     if (next !== this.restExercise) {
       this.currentExerciseIndex++;
        }
     this.startExercise(next);}
   else { console.log('Workout complete!'); } 
} 

The if condition if (this.exerciseRunningDuration >= this.currentExercise.duration) is used to transition to the next exercise once the time duration of the current exercise lapses. We use getNextExercise to get the next exercise and call startExercise again to repeat the process. If no exercise is returned by the getNextExercise call, the workout is considered complete.

During exercise transitioning, we increment currentExerciseIndex only if the next exercise is not a rest exercise. Remember that the original workout plan does not have a rest exercise. For the sake of consistency, we have created a rest exercise and are now swapping between rest and the standard exercises that are part of the workout plan. Therefore, currentExerciseIndex does not change when the next exercise is rest.

Let's quickly add the getNextExercise function too. Add the function to the WorkoutRunnerComponent class:

getNextExercise(): ExercisePlan { 
    let nextExercise: ExercisePlan = null; 
    if (this.currentExercise === this.restExercise) { 
      nextExercise = this.workoutPlan.exercises[this.currentExerciseIndex + 1]; 
    } 
    else if (this.currentExerciseIndex < this.workoutPlan.exercises.length - 1) { 
      nextExercise = this.restExercise; 
    } 
    return nextExercise; 
} 

The getNextExercise function returns the next exercise that needs to be performed.

Note that the returned object for getNextExercise is an ExercisePlan object that internally contains the exercise details and the duration for which the exercise runs.

The implementation is quite self-explanatory. If the current exercise is rest, take the next exercise from the workoutPlan.exercises array (based on currentExerciseIndex); otherwise, the next exercise is rest, given that we are not on the last exercise (the else if condition check).

With this, we are ready to test our implementation. The exercises should flip after every 10 or 30 seconds. Great!

The current build setup automatically compiles any changes made to the script files when the files are saved; it also refreshes the browser after these changes. But just in case the UI does not update or things do not work as expected, refresh the browser window. If you are having a problem with running the code, look at the Git branch checkpoint2.1 for a working version of what we have done thus far. Or if you are not using Git, download the snapshot of Checkpoint 2.1 (a ZIP file) from http://bit.ly/ng6be-checkpoint2-1 . Refer to the README.md file in the trainer folder when setting up the snapshot for the first time.

We have done enough work on the component for now, let's build the view.