7. Component Interaction
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:

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:

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:

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:
