Recherche…


Introduction

Partager des informations entre différentes directives et composants.

Passer des données de parent à enfant avec une liaison d'entrée

HeroChildComponent a deux propriétés d'entrée, généralement ornées de décorations @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;
}

La propriété d'interception des entrées d'entrée avec un setter

Utilisez un paramètre de propriété en entrée pour intercepter et agir sur une valeur du parent.

Le setter de la propriété input de nom dans l'enfant NameChildComponent ajuste les espaces à partir d'un nom et remplace une valeur vide par du texte par défaut.

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

Voici le NameParentComponent démontrant des variantes de nom, y compris un nom avec tous les espaces:

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

Parent écoute un événement enfant

Le composant enfant expose une propriété EventEmitter avec laquelle il émet des événements lorsque quelque chose se produit. Le parent se lie à cette propriété d'événement et réagit à ces événements.

La propriété EventEmitter de l'enfant est une propriété de sortie, généralement ornée d'une décoration @Output, telle qu'elle apparaît dans ce composant 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;
  }
}

En cliquant sur un bouton, vous déclenchez l'émission d'une valeur vraie ou fausse (la charge utile booléenne).

Le parent VoteTakerComponent lie un gestionnaire d'événement (onVoted) qui répond à la charge utile de l'événement enfant ($ event) et met à jour un compteur.

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

Le parent interagit avec l'enfant via une variable locale

Un composant parent ne peut pas utiliser la liaison de données pour lire les propriétés enfant ou appeler des méthodes enfants. Nous pouvons faire les deux en créant une variable de référence de modèle pour l'élément enfant, puis en référençant cette variable dans le modèle parent, comme indiqué dans l'exemple suivant.

Nous avons un enfant CountdownTimerComponent qui compte à rebours jusqu'à zéro et lance une fusée. Il a des méthodes de démarrage et d'arrêt qui contrôlent l'horloge et il affiche un message d'état de compte à rebours dans son propre modèle.

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

Voyons le CountdownLocalVarParentComponent qui héberge le composant 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 { }

Le composant parent ne peut pas lier de données aux méthodes de démarrage et d'arrêt de l'enfant ni à sa propriété seconds.

Nous pouvons placer une variable locale (#timer) sur le tag () représentant le composant enfant. Cela nous donne une référence au composant enfant lui-même et à la possibilité d'accéder à l'une de ses propriétés ou méthodes depuis le modèle parent.

Dans cet exemple, nous connectons les boutons parents au démarrage et à l'arrêt de l'enfant et utilisons l'interpolation pour afficher la propriété secondes de l'enfant.

Ici, nous voyons le parent et l'enfant travailler ensemble.

Le parent appelle un ViewChild

L'approche des variables locales est simple et facile. Mais il est limité car le câblage parent-enfant doit être effectué entièrement dans le modèle parent. Le composant parent lui-même n'a pas accès à l'enfant.

Nous ne pouvons pas utiliser la technique de la variable locale si une instance de la classe du composant parent doit lire ou écrire des valeurs de composant enfant ou doit appeler des méthodes de composant enfant.

Lorsque la classe du composant parent requiert ce type d'accès, nous injectons le composant enfant dans le parent en tant que ViewChild.

Nous allons illustrer cette technique avec le même exemple de compte à rebours. Nous ne changerons pas d'apparence ou de comportement. L'enfant CountdownTimerComponent est également identique.

Nous passons de la variable locale à la technique ViewChild uniquement à des fins de démonstration. Voici le parent, 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(); }
}

Il faut un peu plus de travail pour placer la vue enfant dans la classe du composant parent.

Nous importons des références au décorateur ViewChild et au hook du cycle de vie AfterViewInit.

Nous injectons l'enfant CountdownTimerComponent dans la propriété private timerComponent via la décoration de la propriété @ViewChild.

La variable locale #timer a disparu des métadonnées du composant. Au lieu de cela, nous lions les boutons aux propres méthodes de démarrage et d'arrêt du composant parent et présentons les secondes de coche dans une interpolation autour de la méthode des secondes du composant parent.

Ces méthodes accèdent directement au composant temporisateur injecté.

Le crochet du cycle de vie ngAfterViewInit est une ride importante. Le composant du minuteur n'est disponible qu'après l'affichage de la vue parent par Angular. Nous affichons donc 0 seconde au départ.

Angular appelle ensuite le hook du cycle de vie ngAfterViewInit. Il est alors trop tard pour mettre à jour l'affichage des secondes du compte à rebours. La règle de flux de données unidirectionnelle d'Angular nous empêche de mettre à jour la vue parente dans le même cycle. Nous devons attendre un tour avant de pouvoir afficher les secondes.

Nous utilisons setTimeout pour attendre une coche, puis révisons la méthode des secondes afin qu'elle prenne les valeurs futures du composant timer.

Parent et enfants communiquent via un service

Un composant parent et ses enfants partagent un service dont l'interface permet une communication bidirectionnelle au sein de la famille.

La portée de l'instance de service est le composant parent et ses enfants. Les composants en dehors de ce sous-arbre de composant n'ont pas accès au service ou à leurs communications.

Ce MissionService connecte MissionControlComponent à plusieurs enfants 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 fournit à la fois l'instance du service qu'il partage avec ses enfants (via le tableau de métadonnées des fournisseurs) et injecte cette instance dans son constructeur via son constructeur:

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

L'AstronautComponent injecte également le service dans son constructeur. Chaque AstronautComponent est un enfant de MissionControlComponent et reçoit donc l'instance de service de son parent:

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

Notez que nous capturons l'abonnement et que nous nous désabonnons lorsque l'AstronautComponent est détruit. Ceci est une étape de protection contre la fuite de mémoire. Il n'y a pas de risque réel dans cette application car la durée de vie d'un AstronautComponent est la même que celle de l'application elle-même. Cela ne serait pas toujours vrai dans une application plus complexe.

Nous n'ajoutons pas cette protection à MissionControlComponent car, en tant que parent, il contrôle la durée de vie de MissionService. Le journal de l'historique montre que les messages circulent dans les deux sens entre le parent MissionControlComponent et les enfants AstronautComponent, facilité par le service:



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow