Ricerca…


introduzione

Condividi le informazioni tra diverse direttive e componenti.

Passa i dati da genitore a figlio con il bind di input

HeroChildComponent ha due proprietà di input, tipicamente adornate con decorazioni @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;
}

Intercetta le proprietà di input con un setter

Utilizzare un setter di proprietà di input per intercettare e agire su un valore dal genitore.

Il setter della proprietà di input del nome nel figlio NameChildComponent taglia lo spazio bianco da un nome e sostituisce un valore vuoto con il testo predefinito.

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; }
}

Ecco il NameParentComponent che mostra le varianti del nome incluso un nome con tutti gli spazi:

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  '];
}

Il genitore ascolta l'evento figlio

Il componente figlio espone una proprietà EventEmitter con la quale emette eventi quando succede qualcosa. Il genitore si collega a quella proprietà dell'evento e reagisce a quegli eventi.

La proprietà EventEmitter del child è una proprietà di output, tipicamente decorata con una decorazione @Output come vista in questo 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;
  }
}

Facendo clic su un pulsante si attiva l'emissione di un vero o falso (il payload booleano).

Il genitore VoteTakerComponent associa un gestore di eventi (onVoted) che risponde al payload dell'evento figlio ($ event) e aggiorna un contatore.

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++;
  }
}

Il genitore interagisce con il figlio tramite la variabile locale

Un componente padre non può utilizzare l'associazione dati per leggere le proprietà figlio o richiamare i metodi figlio. Possiamo fare entrambe le cose creando una variabile di riferimento modello per l'elemento figlio e quindi facendo riferimento a tale variabile all'interno del modello principale come mostrato nell'esempio seguente.

Abbiamo un figlio CountdownTimerComponent che conta più volte fino a zero e lancia un razzo. Ha i metodi di avvio e arresto che controllano l'orologio e visualizza un messaggio di stato del conto alla rovescia nel proprio modello.

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);
  }
}

Vediamo CountdownLocalVarParentComponent che ospita il componente timer.

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 { }

Il componente principale non può eseguire il bind dei dati sui metodi di avvio e arresto del figlio né sulla sua proprietà secondi.

Possiamo posizionare una variabile locale (#timer) sul tag () che rappresenta il componente figlio. Questo ci dà un riferimento al componente figlio stesso e alla possibilità di accedere a qualsiasi sua proprietà o metodo dal modello principale.

In questo esempio, colleghiamo i pulsanti padre all'avvio e all'arresto del figlio e utilizziamo l'interpolazione per visualizzare la proprietà secondi del figlio.

Qui vediamo il genitore e il bambino che lavorano insieme.

Il genitore chiama un ViewChild

L'approccio variabile locale è semplice e facile. Ma è limitato perché il cablaggio genitore-figlio deve essere fatto interamente all'interno del modello principale. Il componente principale non ha accesso al bambino.

Non è possibile utilizzare la tecnica della variabile locale se un'istanza della classe del componente padre deve leggere o scrivere valori del componente figlio o deve chiamare metodi del componente figlio.

Quando la classe del componente genitore richiede quel tipo di accesso, iniettiamo il componente figlio nel genitore come ViewChild.

Illustreremo questa tecnica con lo stesso esempio di Countdown Timer. Non cambieremo il suo aspetto o comportamento. Il figlio CountdownTimerComponent è lo stesso.

Stiamo passando dalla variabile locale alla tecnica ViewChild esclusivamente a scopo dimostrativo. Ecco il genitore, 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(); }
}

Ci vuole un po 'più di lavoro per ottenere la vista figlio nella classe del componente genitore.

Importiamo i riferimenti al decoratore ViewChild e al gancio del ciclo di vita AfterViewInit.

Iniettiamo il figlio CountdownTimerComponent nella proprietà timerComponent privata tramite la decorazione della proprietà @ViewChild.

La variabile locale #timer è scomparsa dai metadati del componente. Invece leghiamo i pulsanti ai metodi di avvio e arresto del componente principale e presentiamo i secondi di spunta in un'interpolazione attorno al metodo dei secondi del componente genitore.

Questi metodi accedono direttamente al componente timer iniettato.

L'hook del ciclo di vita ngAfterViewInit è una ruga importante. Il componente del timer non è disponibile fino a quando Angular non visualizza la vista genitore. Quindi visualizziamo inizialmente 0 secondi.

Quindi Angular chiama l'hook del ciclo di vita ngAfterViewInit in cui è troppo tardi per aggiornare la visualizzazione della visualizzazione genitore dei secondi del conto alla rovescia. La regola del flusso di dati unidirezionale di Angular ci impedisce di aggiornare la vista genitore nello stesso ciclo. Dobbiamo aspettare un turno prima di poter visualizzare i secondi.

Usiamo setTimeout per aspettare un segno di spunta e quindi rivedere il metodo dei secondi in modo che prenda i valori futuri dal componente timer.

Genitori e figli comunicano tramite un servizio

Un componente principale e i suoi figli condividono un servizio la cui interfaccia consente la comunicazione bidirezionale all'interno della famiglia.

L'ambito dell'istanza del servizio è il componente principale e i relativi elementi secondari. I componenti esterni alla sottostruttura di questo componente non hanno accesso al servizio o alle loro comunicazioni.

Questo MissionService collega MissionControlComponent a più bambini 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);
  }
}

Il MissionControlComponent fornisce sia l'istanza del servizio che condivide con i suoi figli (attraverso l'array di metadati del provider) e inietta tale istanza in se stessa attraverso il suo costruttore:

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; }
  }
}

Anche l'AstronautComponent inietta il servizio nel suo costruttore. Ogni AstronautComponent è figlio di MissionControlComponent e pertanto riceve l'istanza di servizio del genitore:

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();
  }
}

Si noti che acquisiamo la sottoscrizione e annulliamo l'iscrizione quando DistronautComponent viene distrutto. Questo è un passo per la perdita di memoria. Non ci sono rischi reali in questa app perché la durata di un AstronautComponent è uguale alla durata della stessa app. Ciò non sarebbe sempre vero in un'applicazione più complessa.

Non aggiungiamo questa guardia a MissionControlComponent perché, in quanto genitore, controlla la durata del MissionService. Il registro cronologia dimostra che i messaggi viaggiano in entrambe le direzioni tra i genitori MissionControlComponent e i bambini AstronautComponent, facilitati dal servizio:



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow