Buscar..


Introducción

Compartir información entre diferentes directivas y componentes.

Pase datos de padre a hijo con enlace de entrada

HeroChildComponent tiene dos propiedades de entrada, típicamente adornadas con decoraciones @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 propiedad de entrada de intercepción cambia con un setter

Use un establecedor de propiedades de entrada para interceptar y actuar sobre un valor del padre.

El definidor de la propiedad de entrada de nombre en el NameChildComponent secundario recorta el espacio en blanco de un nombre y reemplaza un valor vacío con texto predeterminado.

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

Aquí está el NameParentComponent que muestra las variaciones de nombre, incluido un nombre con todos los espacios:

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

Padre escucha para evento infantil

El componente hijo expone una propiedad EventEmitter con la que emite eventos cuando ocurre algo. El padre se une a esa propiedad de evento y reacciona a esos eventos.

La propiedad EventEmitter del niño es una propiedad de salida, típicamente adornada con una decoración @Output como se ve en este 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;
  }
}

Al hacer clic en un botón se activa la emisión de un verdadero o falso (la carga útil booleana).

El principal VoteTakerComponent vincula un controlador de eventos (onVoted) que responde a la carga útil del evento secundario ($ evento) y actualiza un contador.

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

El padre interactúa con el niño a través de la variable local

Un componente principal no puede utilizar el enlace de datos para leer propiedades secundarias o invocar métodos secundarios. Podemos hacer ambas cosas creando una variable de referencia de plantilla para el elemento secundario y luego hacer referencia a esa variable dentro de la plantilla principal como se ve en el siguiente ejemplo.

Tenemos un hijo CountdownTimerComponent que repetidamente cuenta atrás hasta cero y lanza un cohete. Tiene métodos de inicio y detención que controlan el reloj y muestra un mensaje de estado de cuenta regresiva en su propia plantilla.

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

Veamos el CountdownLocalVarParentComponent que aloja el componente del temporizador.

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

El componente principal no puede enlazar datos a los métodos de inicio y parada del niño ni a su propiedad de segundos.

Podemos colocar una variable local (#timer) en la etiqueta () que representa el componente secundario. Eso nos da una referencia al componente secundario en sí mismo y la capacidad de acceder a cualquiera de sus propiedades o métodos desde la plantilla principal.

En este ejemplo, conectamos los botones principales al inicio y al final del niño y utilizamos la interpolación para mostrar la propiedad de los segundos del niño.

Aquí vemos al padre y al niño trabajando juntos.

Padre llama a un ViewChild

El enfoque de la variable local es simple y fácil. Pero está limitado porque el cableado padre-hijo debe hacerse completamente dentro de la plantilla padre. El componente principal en sí no tiene acceso al elemento secundario.

No podemos usar la técnica de variables locales si una instancia de la clase de componente principal debe leer o escribir valores de componente secundario o debe llamar a métodos de componente secundario.

Cuando la clase de componente principal requiere ese tipo de acceso, inyectamos el componente secundario en el principal como un ViewChild.

Ilustraremos esta técnica con el mismo ejemplo de temporizador de cuenta regresiva. No cambiaremos su apariencia o comportamiento. El niño CountdownTimerComponent también es el mismo.

Estamos cambiando de la variable local a la técnica ViewChild únicamente con fines de demostración. Aquí está el padre, 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(); }
}

Se necesita un poco más de trabajo para obtener la vista secundaria en la clase de componente principal.

Importamos referencias al decorador ViewChild y al gancho del ciclo de vida de AfterViewInit.

Inyectamos el hijo CountdownTimerComponent en la propiedad privada timerComponent a través de la decoración de la propiedad @ViewChild.

La variable local #timer desaparece de los metadatos del componente. En su lugar, enlazamos los botones con los propios métodos de inicio y detención del componente principal y presentamos los segundos de tic-tac en una interpolación alrededor del método de segundos del componente principal.

Estos métodos acceden directamente al componente del temporizador inyectado.

El gancho del ciclo de vida de ngAfterViewInit es una arruga importante. El componente del temporizador no está disponible hasta que Angular muestra la vista principal. Así que mostramos 0 segundos inicialmente.

Luego Angular llama al gancho del ciclo de vida de ngAfterViewInit, momento en el cual es demasiado tarde para actualizar la visualización de la cuenta principal de los segundos de la cuenta regresiva. La regla de flujo de datos unidireccional de Angular nos impide actualizar las vistas principales en el mismo ciclo. Tenemos que esperar un turno antes de poder mostrar los segundos.

Usamos setTimeout para esperar una marca y luego revisar el método de los segundos para que tome valores futuros del componente del temporizador.

Padres e hijos se comunican a través de un servicio.

Un componente padre y sus hijos comparten un servicio cuya interfaz permite la comunicación bidireccional dentro de la familia.

El ámbito de la instancia de servicio es el componente principal y sus elementos secundarios. Los componentes fuera de este subárbol de componentes no tienen acceso al servicio ni a sus comunicaciones.

Este MissionService conecta el MissionControlComponent con varios hijos de 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 proporciona la instancia del servicio que comparte con sus hijos (a través de la matriz de metadatos del proveedor) e inyecta esa instancia en sí misma a través de su constructor:

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

El AstronautComponent también inyecta el servicio en su constructor. Cada AstronautComponent es un elemento secundario de MissionControlComponent y, por lo tanto, recibe la instancia de servicio de su padre:

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

Tenga en cuenta que capturamos la suscripción y anula la suscripción cuando se destruye el AstronautComponent. Este es un paso de guardia de fuga de memoria. No hay riesgo real en esta aplicación porque la vida útil de un AstronautComponent es la misma que la vida misma de la aplicación. Eso no siempre sería cierto en una aplicación más compleja.

No agregamos esta protección a MissionControlComponent porque, como padre, controla la vida útil de MissionService. El registro de Historial demuestra que los mensajes viajan en ambas direcciones entre el MissionControlComponent padre y el hijo AstronautComponent, facilitado por el servicio:



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow