7. Component Interaction

Input Binding:

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

export class Technology {
  name: string;
}

export const TECHNOLOGIES = [
      { name: 'Angular 4'},
      { name: 'Angular CLI'},
      { name: 'Angular Material'}
];

@Component({
  selector: 'app-technology-child',
  template: `
        <h3>{{ technology.name }} says:</h3>
        <p>I, {{ technology.name }} am at your service, {{ masterName }}.</p>
  `
})
export class TechnologyChildComponent {
  @Input() technology: Technology;
  @Input('master') masterName: string;
}

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

@Component({
  selector: 'app-technology-parent',
  template: `
      <h2>{{ master }} salutes the top {{ technologies.length }} super heroes</h2>
      <app-technology-child *ngFor="let technology of technologies"
                            [technology]="technology" [master]="master">
      </app-technology-child>
  `
})
export class TechnologyParentComponent {
  technologies = TECHNOLOGIES;
  master = 'Open Source';

}

Let's have a look at the results of the running application in Chromium:

Intercept Input Property Changes With A Setter:

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

@Component({
  selector: 'app-name-child',
  template: `
              <h3>"{{ name }}"</h3>
  `
})
export class NameChildComponent {
  private _name = '';

  @Input()
  set name(name: string) {
    this._name = (name && name.trim()) || '<future killer technology>';
  }

  get name(): string { return this._name; }

}

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

@Component({
  selector: 'app-name-parent',
  template: `
        <h2>Open Source salutes {{ names.length }} technologies</h2>
        <app-name-child *ngFor="let name of names" [name]="name"></app-name-child>
  `
})
export class NameParentComponent {
      names = ['Angular 5', '  ', ' Angular Dart '];

}

Let's have a look at the results in our Chromium Web Browser:

Intercept Input Property Changes With ngOnChanges:

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

@Component({
  selector: 'app-version-child',
  template: `
        <h3>Version {{ major }}.{{ minor }}</h3>
        <h4>Change log:</h4>
        <ul>
            <li *ngFor="let change of changeLog">{{ change }}</li>
        </ul>
  `
})
export class VersionChildComponent implements OnChanges {
              @Input() major: number;
              @Input() minor: number;
              changeLog: string[] = [];

              ngOnChanges(changes: {[propKey: string]: SimpleChange}) {
                const log: string[] = [];
                for (const propName of Object.keys(changes)) {
                  const changedProp = changes[propName];
                  const to = JSON.stringify(changedProp.currentValue);
                  if (changedProp.isFirstChange()) {
                    log.push(`Initial value of ${propName} set to ${to}`);
                  } else {
                    const from = JSON.stringify(changedProp.previousValue);
                    log.push(`${propName} changed from ${from} to ${to}`);
                  }
                }
                this.changeLog.push(log.join(', '));
              }

}

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

@Component({
  selector: 'app-version-parent',
  template: `
          <h2>Source code version</h2>
          <button (click)="newMinor()">New minor version</button>
          <button (click)="newMajor()">New major version</button>
          <app-version-child [major]="major" [minor]="minor"></app-version-child>
  `
})
export class VersionParentComponent {
        major = 5;
        minor = 1;

        newMinor() {
          this.minor++;
        }

        newMajor() {
          this.major++;
          this.minor = 0;
        }

}

Let's view the results in Chromium:

Parent Listens For Child Event:

import { Component, EventEmitter, Input, Output } from '@angular/core';

@Component({
  selector: 'app-voter',
  template: `
          <h4>{{ name }}</h4>
          <button (click)="vote(true)" [disabled]="voted">Agree</button>
          <button (click)="vote(false)" [disabled]="voted">Disagree</button>
  `
})
export class VoterComponent {
        @Input() name: string;
        @Output() onVoted = new EventEmitter<boolean>();
        voted = false;

        vote(agreed: boolean): void {
          this.onVoted.emit(agreed);
          this.voted = true;
        }

}

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

@Component({
  selector: 'app-vote-taker',
  template: `
            <h2>Should Angular 10 implement teleportation && telepathy functionality?</h2>
            <h3>Agree: {{ agreed }}, Disagree: {{ disagreed }}</h3>
            <app-voter *ngFor="let voter of voters"
                          [name]="voter"
                          (onVoted)="onVoted($event)">
            </app-voter>
  `
})
export class VoteTakerComponent {
       agreed = 0;
       disagreed = 0;
       voters = ['Superman', 'Wonderwoman', 'Spiderman'];

       onVoted(agreed: boolean): void {
         agreed ? this.agreed++ : this.disagreed++;
       }

}

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

Parent Interacts With Child Via Local Variable:

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

@Component({
  selector: 'app-countdown-timer',
  template: `<p>{{ message }}</p>`
})
export class CountdownTimerComponent implements OnInit, OnDestroy {
             message = '';
             seconds = 11;
             private intervalId = 0;

             clearTimer() { clearInterval(this.intervalId); }

             ngOnInit() {
               this.start();
             }

             ngOnDestroy() {
               this.clearTimer();
             }

             start() { this.countDown(); }

             stop() {
               this.clearTimer();
               this.message = `Holding at T-${this.seconds} seconds`;
             }

             private countDown() {
               this.clearTimer();
               this.intervalId = window.setInterval(() => {
                  this.seconds -= 1;
                  if (this.seconds === 0) {
                    this.message = 'Blast Off!';
                  } else {
                    if (this.seconds < 0) { this.seconds = 10; } // reset
                    this.message = `T-${this.seconds} seconds continuing`;
                  }
               }, 1000);
             }

  }

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

  @Component({
    selector: 'app-countdown-host-lv',
    template: `
          <h3>Countdown to Liftoff (via local variable)</h3>
          <button (click)="timer.start()">Start</button>
          <button (click)="timer.stop()">Stop</button>
          <div class="seconds">{{ timer.seconds }}</div>
          <app-countdown-timer #timer></app-countdown-timer>
    `,
    styles: [`
          .seconds {
            background-color: black;
            color: red;
            font-size: 5em;
            margin: 0.3em 0;
            text-align: center;
            width: 1.5em;
          }
      `]
  })
  export class CountdownLocalVarHostComponent {

  }

Let's view the running application in Chromium:

Parent Calls An @ViewChild():

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

import { CountdownTimerComponent } from './countdown-timer.component';

@Component({
  selector: 'app-countdown-host-vc',
  template: `
        <h3>Countdown to Liftoff (via ViewChild)</h3>
        <button (click)="start()">Start</button>
        <button (click)="stop()">Stop</button>
        <div class="seconds">{{ seconds() }}</div>
        <app-countdown-timer></app-countdown-timer>
  `,
  styles: [`
      .seconds {
        background-color: black;
        color: red;
        font-size: 5em;
        margin: 0.3em 0;
        text-align: center;
        width: 1.5em;
      }
    `]
})
export class CountdownViewChildHostComponent implements AfterViewInit {

  @ViewChild(CountdownTimerComponent) private timerComponent: CountdownTimerComponent;

  seconds() { return 0; }

  ngAfterViewInit() {
    // redefine seconds to get from the countdowntimercomponent.seconds
    // but wait a tick first to avoid one-time dev mode
    // unidirectional-data-flow-violation error
    setTimeout(() => this.seconds = () => this.timerComponent.seconds, 0);
  }

  start() { this.timerComponent.start(); }
  stop() { this.timerComponent.stop(); }

}

Parent And Children Communicate Via A Service

MissionService:

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

@Injectable()
export class MissionService {


    // observable string sources
    private missionAnnouncedSource = new Subject<string>();
    private missionConfirmedSource = new Subject<string>();

    // observable string streams
    missionAnnounced$ = this.missionAnnouncedSource.asObservable();
    missionConfirmed$ = this.missionConfirmedSource.asObservable();

    // service message commands
    announceMission(mission: string): void {
      this.missionAnnouncedSource.next(mission);
    }

    confirmMission(astronaut: string): void {
      this.missionConfirmedSource.next(astronaut);
    }

}

AstronautComponent:

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

import { MissionService } from './mission.service';

import { Subscription } from 'rxjs/Subscription';


@Component({
  selector: 'app-my-astronaut',
  template: `
            <p>
                {{ astronaut }}: <strong>{{ mission }}</strong>
                <button (click)="confirm()" [disabled]="!announced || confirmed">
                Confirm
                </button>
            </p>
  `
})
export class AstronautComponent implements OnDestroy {
            @Input() astronaut: string;
            mission = '< no mission announced >';
            confirmed = false;
            announced = false;
            private missionAnnouncedSubscription: Subscription;

            constructor(private missionService: MissionService) {
              this.missionAnnouncedSubscription = missionService.missionAnnounced$.subscribe(
                mission => {
                  this.mission = mission;
                  this.announced = true;
                  this.confirmed = false;
                });
            }

            confirm() {
              this.confirmed = true;
              this.missionService.confirmMission(this.astronaut);
            }

            ngOnDestroy() {
              this.missionAnnouncedSubscription.unsubscribe();
            }
}

MissionControlComponent:

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

import { MissionService } from './mission.service';


@Component({
  selector: 'app-mission-control',
  template: `
        <h2>Mission Control</h2>
        <button (click)="announce()">Announce Mission</button>
        <app-my-astronaut *ngFor="let astronaut of astronauts"
                          [astronaut]="astronaut">
        </app-my-astronaut>
        <h3>History</h3>
        <ul>
            <li *ngFor="let event of history">{{ event }}</li>
        </ul>
  `,
  providers: [MissionService]
})
export class MissionControlComponent {
       astronauts = ['Igor', 'Nils', 'Hans', 'Rob'];
       history: string[] = [];

       missions = ['Fly to Mars!', 'Fly to Jupiter!', 'Fly to Venus', 'Conquer the World!'];
       nextMission = 0;

       constructor(private missionService: MissionService) {
         missionService.missionConfirmed$.subscribe(
           astronaut => { this.history.push(`${astronaut} confirmed the mission`); }
         );
       }

       announce() {
         const mission = this.missions[this.nextMission++];
         this.missionService.announceMission(mission);
         this.history.push(`Mission "${mission}" announced`);
         if (this.nextMission >= this.missions.length) { this.nextMission = 0; }
       }

}

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

Sibling Component Interaction

TodoService:

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

@Injectable()
export class TodoService {

      // observable string sources
      private totalCount = new Subject<number>();
      private lastUpdate = new Subject<number>();
      private clearAll = new Subject<boolean>();

      // observable string streams
      totalCount$ = this.totalCount.asObservable();
      lastUpdate$ = this.lastUpdate.asObservable();
      clearAll$ = this.clearAll.asObservable();

      // service message commands
      publishTotalCount(count: number) {
        this.totalCount.next(count);
      }

      publishLastUpdate(date: number) {
        this.lastUpdate.next(date);
      }

      publishClearAll(clear: boolean) {
        this.clearAll.next(clear);
      }

}

Sibling Components:

import { Component, OnDestroy } from '@angular/core';
import { TodoService } from './todo.service';

import { Subscription } from 'rxjs/Subscription';

class Todo {
  title: string;
  isCompleted: boolean;
  date: number;
}

@Component({
  selector: 'app-todo',
  template: `
          <h2>Todo List</h2>
          <h3>What needs to be done?</h3>
          <input #todo>
          <button (click)="add(todo)">Add</button>
          <ul>
                <li *ngFor="let todo of todos">{{ todo.title }}
                <button (click)="remove(todo)">X</button>
                </li>
          </ul>
  `
})
export class TodoComponent implements OnDestroy {
    todos: Todo[] = [];
    totalItems = 0;
    lastUpdate = 0;
    clearSubscription: Subscription;

    constructor(private todoService: TodoService) {
      this.clearSubscription = this.todoService.clearAll$.subscribe(
        clear => {
          if (clear) {
            this.todos.length = 0;
            this.totalItems = 0;
          }
        });
    }

    add(todo): void {
      this.todos.push({title: todo.value, isCompleted: false, date: new Date().getTime()});
      this.updateCountDate(true);
      todo.value = '';
    }

    remove(todo): void {
      this.todos.splice(this.todos.indexOf(todo), 1);
      this.updateCountDate(false);
    }

    private updateCountDate(change: boolean): void {
      change ? this.totalItems += 1 : this.totalItems -= 1;
      this.todoService.publishTotalCount(this.totalItems);
      this.todoService.publishLastUpdate(new Date().getTime());
    }


    ngOnDestroy() {
      this.clearSubscription.unsubscribe();
    }
}

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

@Component({
  selector: 'app-dashboard',
  template: `
        <h2>Dashboard</h2>
        <p><b>Last Update: </b>{{ lastUpdate | date:'medium' }}</p>
        <p><b>Total Items: </b>{{ totalCount }}</p>
        <button (click)="clear()">Clear All</button>
  `
})
export class DashboardComponent implements OnDestroy {
        lastUpdate = null;
        totalCount = 0;
        totalCountSubscription: Subscription;
        lastUpdateSubscription: Subscription;

        constructor(private todoService: TodoService) {
          this.totalCountSubscription = this.todoService.totalCount$.subscribe(
            count => {
              this.totalCount = count;
            });
          this.lastUpdateSubscription = this.todoService.lastUpdate$.subscribe(
            lastUpdate => {
              this.lastUpdate = lastUpdate;
            });
        }

        clear() {
          this.lastUpdate = null;
          this.totalCount = 0;
          this.todoService.publishClearAll(true);
        }

        ngOnDestroy() {
          this.totalCountSubscription.unsubscribe();
          this.lastUpdateSubscription.unsubscribe();
        }

}

AppComponent:

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

@Component({
  selector: 'app-root',
  template: `
        <h1>{{ title }}</h1>
          <div class="left">
            <app-todo></app-todo>
            <app-dashboard></app-dashboard>
          </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:

FavoriteComponent with Input and Output API:

import { Component, Input, Output, EventEmitter } from '@angular/core';

export interface FavoriteChangedEventArgs {
  newValue: string;
}

@Component({
  selector: 'app-favorite',
  template: `
      <i class="material-icons" (click)="toggle()">{{ starStatus }}</i>
  `
})
export class FavoriteComponent {
    @Input() starStatus: string;
    @Output() change = new EventEmitter<FavoriteChangedEventArgs>();

        toggle() {
          this.starStatus =  (this.starStatus === 'star') ? 'star_border' : 'star';
          this.change.emit({ newValue: this.starStatus });
        }

}

TechnologiesComponent Host:

import { Component } from '@angular/core';
import { FavoriteChangedEventArgs } from './favorite/favorite.component';

@Component({
  selector: 'app-technologies',
  template: `
        <div class="row">
          <div class="col-6">
          <h2>{{ title }}</h2>
          <p>{{ post.title }}</p>
          <app-favorite [starStatus]="post.starStatus" (change)="onFavoriteChanged($event)"></app-favorite>
        </div>
        <div class="col">
            <img class="w-100 p-3 rounded-circle" [src]="imageUrl" />
        </div>
      </div>
  `
})
export class TechnologiesComponent {
  title = 'My Favorites';
  imageUrl = './assets/polymer1.jpg';

  post = {
    title: 'Work hard | Be kind | Do more ...',
    starStatus: 'star'
  };

  onFavoriteChanged(eventArgs: FavoriteChangedEventArgs) {
    console.log('favorite changed: ', eventArgs);
  }

}

HeartComponent:

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

@Component({
  selector: 'app-heart',
  template: `
          <i class="material-icons" (click)="toggle()">{{ heartStatus }}</i>
          <span class="like">{{ like }}</span>
  `,
  styles: [`
    .material-icons {
      color: red;
      cursor: pointer;
      font-size: 18px;
    }
    .like {
      font-size: 18px;
    }
  `]
})
export class HeartComponent {
    @Input() heartStatus: string;
    @Input() like: number;
    toggle() {
      this.heartStatus = (this.heartStatus === 'favorite') ? 'favorite_border' : 'favorite';
      this.like += (this.heartStatus === 'favorite') ? 1 : -1;
    }

}

TechnologiesComponent Host:

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

@Component({
  selector: 'app-technologies',
  template: `
        <div class="row">
          <div class="col-6">
          <h2>{{ title }}</h2>
          <p><strong>{{ tweet.message }}</strong>
          <app-heart [heartStatus]="tweet.heartStatus" [like]="tweet.likesCount"></app-heart>
          </p>
        </div>
        <div class="col">
            <img class="w-100 p-3 rounded-circle" [src]="imageUrl" />
        </div>
      </div>
  `
})
export class TechnologiesComponent {
  title = 'Technologies News';
  imageUrl = './assets/polymer1.jpg';

  tweet = {
    message: 'Version 5.0.0 of Angular Now Available, November 1 2017',
    heartStatus: 'favorite_border',
    likesCount: 100000
  };

}

Let's have a look at the running application in Chromium:

results matching ""

    No results matching ""