Angular 2
Interakcje między komponentami
Szukaj…
Wprowadzenie
Udostępniaj informacje między różnymi dyrektywami i komponentami.
Przekaż dane od rodzica do dziecka z powiązaniem wejściowym
HeroChildComponent ma dwie właściwości wejściowe, zwykle ozdobione dekoracjami @Input.
import { Component, Input } from '@angular/core';
import { Hero } from './hero';
@Component({
selector: 'hero-child',
template: `
<h3>{{hero.name}} says:</h3>
<p>I, {{hero.name}}, am at your service, {{masterName}}.</p>
`
})
export class HeroChildComponent {
@Input() hero: Hero;
@Input('master') masterName: string;
}
Przechwyć zmiany właściwości wejściowych za pomocą settera
Użyj narzędzia do ustawiania właściwości wejściowych, aby przechwytywać wartości nadrzędne i działać na ich podstawie.
Seter właściwości input name w potomnym NameChildComponent przycina białe znaki z nazwy i zastępuje pustą wartość domyślnym tekstem.
import { Component, Input } from '@angular/core';
@Component({
selector: 'name-child',
template: '<h3>"{{name}}"</h3>'
})
export class NameChildComponent {
private _name = '';
@Input()
set name(name: string) {
this._name = (name && name.trim()) || '<no name set>';
}
get name(): string { return this._name; }
}
Oto NameParentComponent demonstrujący różne nazwy, w tym nazwę ze wszystkimi spacjami:
import { Component } from '@angular/core';
@Component({
selector: 'name-parent',
template: `
<h2>Master controls {{names.length}} names</h2>
<name-child *ngFor="let name of names" [name]="name"></name-child>
`
})
export class NameParentComponent {
// Displays 'Mr. IQ', '<no name set>', 'Bombasto'
names = ['Mr. IQ', ' ', ' Bombasto '];
}
Rodzic nasłuchuje na wydarzenie dziecka
Komponent potomny udostępnia właściwość EventEmitter, z którą emituje zdarzenia, gdy coś się dzieje. Element nadrzędny wiąże się z właściwością tego zdarzenia i reaguje na te zdarzenia.
Właściwość EventEmitter dziecka jest właściwością wyjściową, zazwyczaj ozdobioną dekoracją @Output, jak widać w tym VoterComponent:
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'my-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) {
this.onVoted.emit(agreed);
this.voted = true;
}
}
Kliknięcie przycisku uruchamia emisję wartości true lub false (ładunek logiczny).
Nadrzędny element VoteTakerComponent wiąże moduł obsługi zdarzeń (onVoted), który odpowiada na ładunek zdarzenia podrzędnego ($ event) i aktualizuje licznik.
import { Component } from '@angular/core';
@Component({
selector: 'vote-taker',
template: `
<h2>Should mankind colonize the Universe?</h2>
<h3>Agree: {{agreed}}, Disagree: {{disagreed}}</h3>
<my-voter *ngFor="let voter of voters"
[name]="voter"
(onVoted)="onVoted($event)">
</my-voter>
`
})
export class VoteTakerComponent {
agreed = 0;
disagreed = 0;
voters = ['Mr. IQ', 'Ms. Universe', 'Bombasto'];
onVoted(agreed: boolean) {
agreed ? this.agreed++ : this.disagreed++;
}
}
Rodzic współdziała z dzieckiem za pośrednictwem zmiennej lokalnej
Komponent nadrzędny nie może używać powiązania danych do odczytywania właściwości potomnych ani wywoływania metod potomnych. Możemy to zrobić, tworząc zmienną odniesienia szablonu dla elementu potomnego, a następnie odwołując się do tej zmiennej w szablonie nadrzędnym, jak pokazano w poniższym przykładzie.
Mamy potomny CountdownTimerComponent, który wielokrotnie odlicza do zera i wystrzeliwuje rakietę. Ma metody uruchamiania i zatrzymywania, które kontrolują zegar, i wyświetla komunikat o stanie odliczania we własnym szablonie.
import { Component, OnDestroy, OnInit } from '@angular/core';
@Component({
selector: 'countdown-timer',
template: '<p>{{message}}</p>'
})
export class CountdownTimerComponent implements OnInit, OnDestroy {
intervalId = 0;
message = '';
seconds = 11;
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 and counting`;
}
}, 1000);
}
}
Zobaczmy CountdownLocalVarParentComponent, który obsługuje komponent timera.
import { Component } from '@angular/core';
import { CountdownTimerComponent } from './countdown-timer.component';
@Component({
selector: 'countdown-parent-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>
<countdown-timer #timer></countdown-timer>
`,
styleUrls: ['demo.css']
})
export class CountdownLocalVarParentComponent { }
Składnik nadrzędny nie może powiązać danych z metodami uruchamiania i zatrzymywania dziecka ani z jego właściwością sekund.
Możemy umieścić zmienną lokalną (#timer) na tag () reprezentującą komponent potomny. To daje nam odniesienie do samego komponentu potomnego i możliwość dostępu do dowolnej jego właściwości lub metody z szablonu nadrzędnego.
W tym przykładzie podłączamy przyciski rodzica do startu i zatrzymania dziecka i używamy interpolacji do wyświetlenia właściwości sekundy dziecka.
Tutaj widzimy, jak rodzic i dziecko pracują razem.
Rodzic nazywa ViewChild
Podejście zmiennej lokalnej jest proste i łatwe. Jest to jednak ograniczone, ponieważ okablowanie rodzic-dziecko musi być wykonane całkowicie w szablonie rodzica. Sam komponent nadrzędny nie ma dostępu do elementu podrzędnego.
Nie możemy zastosować techniki zmiennej lokalnej, jeśli instancja klasy komponentu nadrzędnego musi czytać lub zapisywać wartości komponentów podrzędnych lub musi wywoływać metody komponentów podrzędnych.
Gdy klasa komponentu nadrzędnego wymaga tego rodzaju dostępu, wstrzykujemy komponent podrzędny do elementu nadrzędnego jako ViewChild.
Zilustrujemy tę technikę tym samym przykładem licznika czasu. Nie zmienimy jego wyglądu ani zachowania. Podrzędny CountdownTimerComponent jest również taki sam.
Przechodzimy ze zmiennej lokalnej na technikę ViewChild wyłącznie w celu demonstracji. Oto element nadrzędny, CountdownViewChildParentComponent:
import { AfterViewInit, ViewChild } from '@angular/core';
import { Component } from '@angular/core';
import { CountdownTimerComponent } from './countdown-timer.component';
@Component({
selector: 'countdown-parent-vc',
template: `
<h3>Countdown to Liftoff (via ViewChild)</h3>
<button (click)="start()">Start</button>
<button (click)="stop()">Stop</button>
<div class="seconds">{{ seconds() }}</div>
<countdown-timer></countdown-timer>
`,
styleUrls: ['demo.css']
})
export class CountdownViewChildParentComponent 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 devMode
// unidirectional-data-flow-violation error
setTimeout(() => this.seconds = () => this.timerComponent.seconds, 0);
}
start() { this.timerComponent.start(); }
stop() { this.timerComponent.stop(); }
}
Zajmuje trochę więcej pracy, aby uzyskać widok potomny do nadrzędnej klasy komponentów.
Importujemy odniesienia do dekoratora ViewChild i haka cyklu życia AfterViewInit.
Wstrzykujemy podrzędny CountdownTimerComponent do prywatnej właściwości timerComponent poprzez dekorację właściwości @ViewChild.
Zmienna lokalna #timer zniknęła z metadanych komponentu. Zamiast tego łączymy przyciski z własnymi metodami uruchamiania i zatrzymywania komponentu nadrzędnego i prezentujemy tykające sekundy w interpolacji wokół metody sekund komponentu nadrzędnego.
Te metody mają bezpośredni dostęp do wstrzykiwanego komponentu timera.
Hak cyklu życia ngAfterViewInit jest ważną zmarszczką. Komponent timera nie jest dostępny, dopóki Angular nie wyświetli widoku rodzica. Na początku wyświetlamy 0 sekund.
Następnie Angular wywołuje hak cyklu życia ngAfterViewInit, kiedy jest już za późno, aby zaktualizować wyświetlanie w widoku rodzica sekund odliczania. Jednokierunkowa reguła przepływu danych Angulara uniemożliwia nam aktualizację widoku rodzica w tym samym cyklu. Musimy poczekać jedną turę, zanim będziemy mogli wyświetlić sekundy.
Używamy setTimeout, aby czekać jeden tik, a następnie poprawiamy metodę sekund, aby pobierała przyszłe wartości ze składnika timera.
Rodzice i dzieci komunikują się za pośrednictwem usługi
Element nadrzędny i jego dzieci korzystają z usługi, której interfejs umożliwia dwukierunkową komunikację w rodzinie.
Zakres wystąpienia usługi to komponent nadrzędny i jego elementy podrzędne. Komponenty poza tym poddrzewem komponentów nie mają dostępu do usługi ani do komunikacji.
Ta usługa MissionService łączy MissionControlComponent z wieloma dziećmi potomnymi AstronautComponent.
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) {
this.missionAnnouncedSource.next(mission);
}
confirmMission(astronaut: string) {
this.missionConfirmedSource.next(astronaut);
}
}
MissionControlComponent zapewnia zarówno instancję usługi, którą udostępnia swoim dzieciom (za pośrednictwem tablicy metadanych dostawców), jak i wstrzykuje tę instancję do siebie za pośrednictwem swojego konstruktora:
import { Component } from '@angular/core';
import { MissionService } from './mission.service';
@Component({
selector: 'mission-control',
template: `
<h2>Mission Control</h2>
<button (click)="announce()">Announce mission</button>
<my-astronaut *ngFor="let astronaut of astronauts"
[astronaut]="astronaut">
</my-astronaut>
<h3>History</h3>
<ul>
<li *ngFor="let event of history">{{event}}</li>
</ul>
`,
providers: [MissionService]
})
export class MissionControlComponent {
astronauts = ['Lovell', 'Swigert', 'Haise'];
history: string[] = [];
missions = ['Fly to the moon!',
'Fly to mars!',
'Fly to Vegas!'];
nextMission = 0;
constructor(private missionService: MissionService) {
missionService.missionConfirmed$.subscribe(
astronaut => {
this.history.push(`${astronaut} confirmed the mission`);
});
}
announce() {
let mission = this.missions[this.nextMission++];
this.missionService.announceMission(mission);
this.history.push(`Mission "${mission}" announced`);
if (this.nextMission >= this.missions.length) { this.nextMission = 0; }
}
}
AstronautComponent wstrzykuje również usługę do swojego konstruktora. Każdy AstronautComponent jest potomkiem MissionControlComponent i dlatego otrzymuje instancję usługi rodzica:
import { Component, Input, OnDestroy } from '@angular/core';
import { MissionService } from './mission.service';
import { Subscription } from 'rxjs/Subscription';
@Component({
selector: '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;
subscription: Subscription;
constructor(private missionService: MissionService) {
this.subscription = missionService.missionAnnounced$.subscribe(
mission => {
this.mission = mission;
this.announced = true;
this.confirmed = false;
});
}
confirm() {
this.confirmed = true;
this.missionService.confirmMission(this.astronaut);
}
ngOnDestroy() {
// prevent memory leak when component destroyed
this.subscription.unsubscribe();
}
}
Zauważ, że przechwytujemy subskrypcję i anulujemy subskrypcję, gdy AstronautComponent zostanie zniszczony. Jest to krok zabezpieczenia przed wyciekiem pamięci. W tej aplikacji nie ma rzeczywistego ryzyka, ponieważ okres istnienia AstronautComponent jest taki sam, jak czas życia samej aplikacji. Nie zawsze tak jest w przypadku bardziej złożonych aplikacji.
Nie dodajemy tej osłony do MissionControlComponent, ponieważ jako rodzic kontroluje żywotność usługi MissionService. Dziennik historii pokazuje, że wiadomości przesyłane są w obu kierunkach między nadrzędną MissionControlComponent a dziećmi AstronautComponent, co ułatwia usługa: