6. Lifecycle Hooks

LoggerService:

import { Injectable } from '@angular/core';

@Injectable()
export class LoggerService {
  logs: string[] = [];
  previousMessage = '';
  previousMessageCount = 1;

  log(message: string) {
    if (message === this.previousMessage) {
      // repeat message; update last log entry with count
      this.logs[this.logs.length - 1] = message + `${this.previousMessage += 1}x`;
    } else {
      // new message log it
      this.previousMessage = message;
      this.previousMessageCount = 1;
      this.logs.push(message);
    }
  }

  clear() {
    this.logs.length = 0;
  }

  // schedules a view refresh to ensure display catches up
  tick() { this.tick_then(() => { }); }
  tick_then(fn: () => any) { setTimeout(fn, 0); }

}

TechnologyLifeCycleHookComponent:

import { OnInit, OnChanges, DoCheck, AfterContentInit, AfterContentChecked,
        AfterViewInit, AfterViewChecked, OnDestroy, SimpleChanges } from '@angular/core';
import { Component, Input } from '@angular/core';
import { LoggerService } from './logger.service';

let nextId = 1;

export class TechnologyLifeCycleHook implements OnInit {
  constructor(private loggerService: LoggerService) { }

  ngOnInit() {
    this.logIt(`OnInit`);
  }

  logIt(message: string) {
    this.loggerService.log(`#${nextId++} ${message}`);
  }
}

@Component({
  selector: 'app-technology-life-cycle-hook',
  template: '<p>Now you see my technology: {{ name }}</p>',
  styles: ['p { background: #ff9999; padding: 8px;}']
})
export class TechnologyLifeCycleHookComponent extends TechnologyLifeCycleHook
                                              implements OnInit, OnChanges, DoCheck, AfterContentInit,
                                              AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy {
@Input() name: string;

private verb = 'initialized';

constructor(loggerService: LoggerService) {
            super(loggerService);
            const is = this.name ? 'is' : 'is not';
            this.logIt(`name ${is} known at construction`);
          }

          // only called for/if there is an @input variable set by parent
          ngOnChanges(changes: SimpleChanges) {
            const changesMsgs: string[] = [];
            for (const propName in changes) {
              if (propName === 'name') {
                const name = changes['name'].currentValue;
                changesMsgs.push(`name ${this.verb} to ${name}`);
              } else {
                changesMsgs.push(propName + ' ' + this.verb);
              }
            }
            this.logIt(`OnChanges: ${changesMsgs.join('; ')}`);
            this.verb = 'changed'; // next time it will be a change
          }

          // beware called frequently, called in every change detection cycle anywhere on the page
          ngDoCheck() { this.logIt(`DoCheck`); }

          ngAfterContentInit() { this.logIt('AfterContentInit'); }

          // beware called frequently, called in every change detection cycle anywhere on the page
          ngAfterContentChecked() { this.logIt('AfterContentChecked'); }

          ngAfterViewInit() { this.logIt('AfterViewInit'); }

          // beware called frequently, called in every change detection cycle anywhere on the page
          ngAfterViewChecked() { this.logIt('AfterViewChecked'); }

          ngOnDestroy() { this.logIt('OnDestroy'); }

}

TechnologyLifeCycleHookHostComponent:

import { Component } from '@angular/core';
import { LoggerService } from './logger.service';

@Component({
  selector: 'app-technology-life-cycle-hook-host',
  template: `
        <div class="host">
            <h2>Technology-LifeCycle-Hook</h2>
            <button (click)="toggleChild()">
                {{ hasChild ? 'Destroy' : 'Create' }} TechnologyLifeCycleHookComponent
            </button>
            <button (click)="updateTechnology()" [hidden]="!hasChild">Update Technology</button>

            <app-technology-life-cycle-hook *ngIf="hasChild" [name]="technologyName">
            </app-technology-life-cycle-hook>

            <h4>-- Lifecycle Hook Log --</h4>
            <div *ngFor="let message of hookLog">{{ message }}</div>
        </div>
  `,
  styles: [`.host { background: #ff3333; color: white; } h2 { color: white; }`]
})
export class TechnologyLifeCycleHookHostComponent {

      hasChild = false;
      hookLog: string[];

      technologyName = 'Angular 4';
      private loggerService: LoggerService;

      constructor(loggerService: LoggerService) {
        this.loggerService = loggerService;
        this.hookLog = loggerService.logs;
      }

      toggleChild() {
        this.hasChild = !this.hasChild;
        if (this.hasChild) {
          this.technologyName = 'Angular 4';
          this.loggerService.clear(); // clear log on create
        }
        this.loggerService.tick();
      }

      updateTechnology() {
        this.technologyName += '*';
        this.loggerService.tick();
      }

}

Let's view the results in our Chromium Web Browser:

Spy Directive:

import { Directive, OnInit, OnDestroy } from '@angular/core';

import { LoggerService } from './logger.service';

let nextId = 1;

@Directive({ selector: '[mySpy]'})
export class SpyDirective implements OnInit, OnDestroy {

    constructor(private loggerService: LoggerService) { }

    ngOnInit() { this.logIt(`onInit`); }

    ngOnDestroy() { this.logIt(`onDestroy`); }

    private logIt(message: string) {
      this.loggerService.log(`Spy #${nextId++} ${message}`);
    }

}

Spy Component:

import { Component } from '@angular/core';
import { LoggerService } from './logger.service';

@Component({
  selector: 'app-spy-host',
  template: `
        <div class="host">
              <h2>Spy Directive</h2>
              <input [(ngModel)]="newName" (keyup.enter)="addTechnology()">
              <button (click)="addTechnology()">Add Technology</button>
              <button (click)="reset()">Reset Technologies</button>
              <p></p>
              <div *ngFor="let technology of technologies" mySpy class="technologies">
                {{ technology }}
              </div>
              <h4>-- Spy Lifecycle Hook Log --</h4>
              <div *ngFor="let message of spyLog">{{ message }}</div>
        </div>
  `,
  styles: [`.host { background: #ffcccc; } .technologies { background: #ff6666; }`]
})
export class SpyHostComponent {
  newName = 'Angular 5';
  technologies: string[] = ['Angular 4', 'Angular CLI', 'Angular Material'];
  spyLog: string[];

  constructor(private loggerService: LoggerService) {
    this.spyLog = loggerService.logs;
  }

  addTechnology() {
    if (this.newName.trim()) {
      this.technologies.push(this.newName.trim());
      this.newName = '';
      this.loggerService.tick();
    }
  }

  reset() {
    this.loggerService.log('-- reset --');
    this.technologies.length = 0;
    this.loggerService.tick();
  }

}

App Component:

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
        <h1>{{ title }}</h1>
          <div class="left">
                  <app-spy-host></app-spy-host>
          </div>
          <div class="right">
            <img [src]="imageUrl" class="img-round" alt="princess image">
          </div>
  `
})
export class AppComponent {
  title = 'Tour of Technologies';
  imageUrl = '../assets/polymer6.jpg';

}

Let's view the results in Chromium:

OnChanges()

OnChangesComponent:

import { Component, Input, OnChanges, SimpleChanges, ViewChild} from '@angular/core';

class Technology {
  constructor(public name: string) { }
}

@Component({
  selector: 'app-on-changes',
  template: `
        <div class="technology">
              <p>{{ technology.name }} can {{ power }}</p>
              <h4>-- Change Log --</h4>
              <div *ngFor="let change of changeLog"> {{ change }}</div>
        </div>
  `,
  styles: [`
            .technology { background: LightYellow; padding: 8px; margin-top: 8px; }
            p { background: Yellow; padding: 8px; margin-top: 8px; }
    `]
})
export class OnChangesComponent implements OnChanges {
  @Input() technology: Technology;
  @Input() power: string;

  changeLog: string[] = [];

  ngOnChanges(changes: SimpleChanges) {
    for (const propName of Object.keys(changes)) {
      const change = changes[propName];
      const current = JSON.stringify(change.currentValue);
      const previous = JSON.stringify(change.previousValue);
      this.changeLog.push(`${propName}: currentValue = ${current}, previousValue = ${previous}`);
    }
  }
  reset() { this.changeLog.length = 0; }
}

/********************************************************/
@Component({
  selector: 'app-on-changes-host',
  template: `
      <div class="host">
            <h2>{{ title }}</h2>
            <table>
                  <tr><td>Power: </td><td><input [(ngModel)]="power"></td></tr>
                  <tr><td>Technology.name: </td><td><input [(ngModel)]="technology.name"></td></tr>
            </table>
            <p><button (click)="reset()">Reset Log</button></p>
            <app-on-changes [technology]="technology" [power]="power"></app-on-changes>
      </div>
  `,
  styles: [`.host { background: #ffcccc; }`]
})
export class OnChangesHostComponent {
  technology: Technology;
  power: string;

  title = 'OnChanges';
  @ViewChild(OnChangesComponent) childView: OnChangesComponent;

  constructor() {
    this.reset();
  }

  reset() {
    // new technology object every time; triggers onChanges
    this.technology = new Technology('Angular 5');
    // setting power only triggers onChanges if this value is different
    this.power = 'data-bind';
    if (this.childView) { this.childView.reset(); }
  }

}

Let's have a look at this beauty in our Chromium Browser:

DoCheck()

DoCheckComponent:

import { Component, DoCheck, Input, ViewChild } from '@angular/core';

class Technology {
  constructor(public name: string) { }
}

@Component({
  selector: 'app-do-check',
  template: `
            <div class="technology">
                  <p>{{ technology.name }} can {{ power }}</p>

                  <h4>-- Change Log --</h4>
                  <div *ngFor="let change of changeLog">{{ change }}</div>
            </div>
  `,
  styles: [`
        .technology { background: LightYellow; padding: 8px; margin-top: 8px }
        p { background: Yellow; padding: 8px; margin-top: 8px }
    `]
})
export class DoCheckComponent implements DoCheck {
       @Input() technology: Technology;
       @Input() power: string;
       changeLog: string[] = [];

       private changeDetected = false;
       private oldTechnologyName = '';
       private oldPower = '';
       private oldLogLength = 0;
       private noChangeCount = 0;

       ngDoCheck() {
          if (this.technology.name !== this.oldTechnologyName) {
              this.changeDetected = true;
              this.changeLog.push(`DoCheck: Technology name changed to "${this.technology.name}"
                                   from "${this.oldTechnologyName}"`);
              this.oldTechnologyName = this.technology.name;
          }
          if (this.power !== this.oldPower) {
            this.changeDetected = true;
            this.changeLog.push(`DoCheck: Power changed to "${this.power}" from "${this.oldPower}"`);
            this.oldPower = this.power;
          }
          if (this.changeDetected) {
            this.noChangeCount = 0;
          } else {
            // log hook was called when no relevant change
            const count = this.noChangeCount += 1;
            const noChangeMessage = `DoCheck called ${count}x when no change to technology or power`;
            if (count === 1) {
              // add new no change method
              this.changeLog.push(noChangeMessage);
            } else {
              // update last no change message
              this.changeLog[this.changeLog.length - 1] = noChangeMessage;
            }
          }
          this.changeDetected = false;
       }

       reset(): void {
         this.changeDetected = true;
         this.changeLog.length = 0;
       }
}


/*************************************************************/
@Component({
  selector: 'app-do-check-host',
  template: `
        <div class="host">
              <h2>{{ title }}</h2>
              <table>
                      <tr><td>Power</td><td><input [(ngModel)]="power"></td></tr>
                      <tr><td>Technology.name</td><td><input [(ngModel)]="technology.name"></td></tr>
              </table>
              <p><button (click)="reset()">Reset Log</button></p>
              <app-do-check [technology]="technology" [power]="power"></app-do-check>
        </div>
  `,
  styles: [`.host { background: #ff9999; }`]
})
export class DoCheckHostComponent {
  technology: Technology;
  power: string;

  title = 'DoCheck';
  @ViewChild(DoCheckComponent) childView: DoCheckComponent;

  constructor() { this.reset(); }

  reset(): void {
    this.technology = new Technology('Angular 5');
    this.power = 'service worker';
    if (this.childView) { this.childView.reset(); }
  }

}

Let's view the running application in Chromium:

AfterViewComponent:

import { AfterViewChecked, AfterViewInit, Component, ViewChild } from '@angular/core';

import { LoggerService } from './logger.service';

@Component({
  selector: 'app-my-child-view',
  template: `<input [(ngModel)]="technology">`
})
export class ChildViewComponent {
  technology = 'Angular 5';
}

/**********************************************/

@Component({
  selector: 'app-after-view',
  template: `
        <div>-- child view begins --</div>
            <app-my-child-view></app-my-child-view>
        <div>-- child view ends --</div>
          ` + `
          <p *ngIf="comment" class="comment">
                {{ comment }}
          </p>
          `
})
export class AfterViewComponent implements AfterViewChecked, AfterViewInit {
      comment = '';
      private previousTechnology = '';

      // query for viewchild of type childviewcomponent
      @ViewChild(ChildViewComponent) viewChild: ChildViewComponent;

      constructor(private loggerService: LoggerService) { this.logIt('AfterView constructor'); }

      ngAfterViewInit() {
        // viewchild is set after view is initialized
        this.logIt('AfterViewInit');
        this.doSomething();
      }

      ngAfterViewChecked() {
        // viewchild is updated after view has been checked
        if (this.previousTechnology === this.viewChild.technology) {
          this.logIt('AfterViewChecked (no change)');
        } else {
          this.previousTechnology = this.viewChild.technology;
          this.logIt('AfterView checked');
          this.doSomething();
        }
      }

      // this surrogate for real business logic sets the comment
      private doSomething(): void {
        const cmt = this.viewChild.technology.length > 10 ? `That's a long name` : '';
        if (cmt !== this.comment) {
          // wait a tick because component's view has already been checked
          this.loggerService.tick_then(() => this.comment = cmt);
        }
      }

      private logIt(method: string): void {
        const child = this.viewChild;
        const message = `${method}: ${child ?  child.technology : 'no' } child view`;
        this.loggerService.log(message);
      }

}

/**********************************************/

@Component({
  selector: 'app-after-view-host',
  template: `
        <div class="host">
            <h2>AfterView</h2>
            <app-after-view *ngIf="show"></app-after-view>
            <h4>-- AfterView Logs --</h4>
            <p><button (click)="reset()">Reset</button></p>
            <div *ngFor="let message of logs">{{ message }}</div>
        </div>
  `,
  styles: ['.host { background: #ffe6e6; }']
})
export class AfterViewHostComponent {
  logs: string[];
  show = true;

  constructor(private loggerService: LoggerService) { this.logs = loggerService.logs; }

  reset(): void {
    this.logs.length = 0;
    // quickly remove and reload afterviewcomponentwhich recreates it
    this.show = false;
    this.loggerService.tick_then(() => this.show = true);
  }

}

AfterContentComponent:

import { AfterContentChecked, AfterContentInit, Component, ContentChild } from '@angular/core';

import { LoggerService } from './logger.service';

@Component({
  selector: 'app-my-child',
  template: `<input [(ngModel)]="technology">`
})
export class ChildContentComponent {
  technology = 'Angular 5';
}

/**********************************************/

@Component({
  selector: 'app-after-content',
  template: `
      <div>-- projected content begins --</div>
              <ng-content></ng-content>
      <div>-- projected content ends --</div>
  ` +
  `
      <p *ngIf="comment" class="comment">
            {{ comment }}
      </p>
  `
})
export class AfterContentComponent implements AfterContentChecked, AfterContentInit {
      comment = '';
      private previousTechnology = '';

      // query for content child of type childcomponent
      @ContentChild(ChildContentComponent) contentChild: ChildContentComponent;

      constructor(private loggerService: LoggerService) { this.logIt('AfterContent constructor'); }

      ngAfterContentInit() {
        // contentchild is set after content has been initialized
        this.logIt('AfterContentInit');
        this.doSomething();
      }

      ngAfterContentChecked() {
        // contentchild is updated after content has been checked
        if (this.previousTechnology === this.contentChild.technology) {
          this.logIt('AfterContentChecked (no change)');
        } else {
          this.previousTechnology = this.contentChild.technology;
          this.logIt('AfterContentChecked');
          this.doSomething();
        }
      }

      // this surrogate for real business logic sets the comment
      private doSomething(): void {
        this.comment = this.contentChild.technology.length > 10 ? `That's a long name` : '';
      }

      private logIt(method: string): void {
        const child = this.contentChild;
        const message = `${method}: ${child ? child.technology : 'no'} child content`;
        this.loggerService.log(message);
      }

}

/**********************************************/

@Component({
  selector: 'app-after-content-host',
  template: `
        <div class="host">
            <h2>AfterContent</h2>
            <div *ngIf="show">` +
            `
              <app-after-content>
                  <app-my-child>
                  </app-my-child>
              </app-after-content>
            ` +
            `</div>
            <h4>-- AfterContentLogs --</h4>
            <p><button (click)="reset()">Reset</button></p>
            <div *ngFor="let message of logs">{{ message }}</div>
        </div>
  `,
  styles: [`.host { background: #ffb3b3; }`]
})
export class AfterContentHostComponent {
  logs: string[];
  show = true;

  constructor(private loggerService: LoggerService) {
    this.logs = loggerService.logs;
  }

  reset(): void {
    this.logs.length = 0;
    // quickly remove and reload aftercontentcomponent which recreates it
    this.show = false;
    this.loggerService.tick_then(() => this.show = true);
  }

}

CounterComponent:

import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';

import { LoggerService } from './logger.service';

@Component({
  selector: 'app-my-counter',
  template: `
      <div class="counter">
        Counter = {{ counter }}

        <h5>-- Counter Change Log --</h5>
        <div *ngFor="let change of changeLog" mySpy>{{ change }}</div>
      </div>
  `,
  styles: [`.counter { background: #ffe6e6; padding: 8px; margin-top: 8px; }`]
})
export class MyCounterComponent implements OnChanges {
  @Input() counter: number;
  changeLog: string[] = [];

  ngOnChanges(changes: SimpleChanges) {
    // empty changelog whenever counter goes to zero
    // hint: this is a way to respond programmatically to external value changes
    if (this.counter === 0) {
      this.changeLog.length = 0;
    }
    // a change to counter is the only change we care about
    const change = changes['counter'];
    const current = change.currentValue;
    const previous = JSON.stringify(change.previousValue); // first time is {}; after is integer
    this.changeLog.push(`counter: currentValue = ${current}, previousValue = ${previous}`);
  }
}

/**********************************************/

@Component({
  selector: 'app-counter-host',
  template: `
        <div class="host">
            <h2>Counter Spy</h2>
            <button (click)="updateCounter()">Update Counter</button>
            <button (click)="reset()">Reset Counter</button>

            <app-my-counter [counter]="value"></app-my-counter>

            <h4>-- Spy Lifecycle Hook Log --</h4>
            <div *ngFor="let message of spyLog">{{ message }}</div>
        </div>
  `,
  styles: [`.host { background: #ff6666; }`]
})
export class CounterHostComponent {
  value: number;
  spyLog: string[] = [];

  private loggerService: LoggerService;

  constructor(loggerService: LoggerService) {
      this.loggerService = loggerService;
      this.spyLog = loggerService.logs;
      this.reset();
  }

  updateCounter() {
    this.value += 1;
    this.loggerService.tick();
  }

  reset() {
    this.loggerService.log('-- reset --');
    this.value = 0;
    this.loggerService.tick();
  }

}

Let's view the results in our Chromium Browser:

results matching ""

    No results matching ""