Comunicación de componentes hermanos de Angular 2

118

Tengo un ListComponent. Cuando se hace clic en un elemento en ListComponent, los detalles de ese elemento deben mostrarse en DetailComponent. Ambos están en la pantalla al mismo tiempo, por lo que no hay enrutamiento involucrado.

¿Cómo le digo a DetailComponent en qué elemento de ListComponent se hizo clic?

He considerado emitir un evento hasta el elemento principal (AppComponent) y hacer que el elemento principal establezca selectedItem.id en DetailComponent con un @Input. O podría usar un servicio compartido con suscripciones observables.


EDITAR: Sin embargo, configurar el elemento seleccionado a través de event + @Input no activa DetailComponent, en caso de que necesite ejecutar código adicional. Así que no estoy seguro de que sea una solución aceptable.


Pero ambos métodos parecen mucho más complejos que la forma de hacer las cosas de Angular 1, que era a través de $ rootScope. $ Broadcast o $ scope. $ Parent. $ Broadcast.

Como todo en Angular 2 es un componente, me sorprende que no haya más información sobre la comunicación de componentes.

¿Hay otra forma más sencilla de lograr esto?

dennis.sheppard
fuente
¿Encontraste alguna forma de compartir datos entre hermanos? Lo necesito como observable ..
Ser humano

Respuestas:

65

Actualizado a rc.4: al intentar que los datos pasen entre componentes hermanos en angular 2, la forma más sencilla en este momento (angular.rc.4) es aprovechar la inyección de dependencia jerárquica de angular2 y crear un servicio compartido.

Aquí estaría el servicio:

import {Injectable} from '@angular/core';

@Injectable()
export class SharedService {
    dataArray: string[] = [];

    insertData(data: string){
        this.dataArray.unshift(data);
    }
}

Ahora, aquí estaría el componente PARENT

import {Component} from '@angular/core';
import {SharedService} from './shared.service';
import {ChildComponent} from './child.component';
import {ChildSiblingComponent} from './child-sibling.component';
@Component({
    selector: 'parent-component',
    template: `
        <h1>Parent</h1>
        <div>
            <child-component></child-component>
            <child-sibling-component></child-sibling-component>
        </div>
    `,
    providers: [SharedService],
    directives: [ChildComponent, ChildSiblingComponent]
})
export class parentComponent{

} 

y sus dos hijos

niño 1

import {Component, OnInit} from '@angular/core';
import {SharedService} from './shared.service'

@Component({
    selector: 'child-component',
    template: `
        <h1>I am a child</h1>
        <div>
            <ul *ngFor="#data in data">
                <li>{{data}}</li>
            </ul>
        </div>
    `
})
export class ChildComponent implements OnInit{
    data: string[] = [];
    constructor(
        private _sharedService: SharedService) { }
    ngOnInit():any {
        this.data = this._sharedService.dataArray;
    }
}

niño 2 (es hermano)

import {Component} from 'angular2/core';
import {SharedService} from './shared.service'

@Component({
    selector: 'child-sibling-component',
    template: `
        <h1>I am a child</h1>
        <input type="text" [(ngModel)]="data"/>
        <button (click)="addData()"></button>
    `
})
export class ChildSiblingComponent{
    data: string = 'Testing data';
    constructor(
        private _sharedService: SharedService){}
    addData(){
        this._sharedService.insertData(this.data);
        this.data = '';
    }
}

AHORA: Cosas a tener en cuenta al usar este método.

  1. Solo incluya al proveedor de servicios para el servicio compartido en el componente PADRE y NO a los niños.
  2. Todavía tiene que incluir constructores e importar el servicio en los hijos
  3. Esta respuesta fue respondida originalmente para una versión beta temprana de angular 2. Sin embargo, todo lo que ha cambiado son las declaraciones de importación, por lo que es todo lo que necesita actualizar si utilizó la versión original por casualidad.
Alex J
fuente
2
¿Sigue siendo válido para angular-rc1?
Sergio
4
Sin embargo, no creo que esto notifique al hermano que algo se ha actualizado en el servicio compartido. Si child-component1 hace algo a lo que child-component2 necesita responder, este método no maneja eso. Creo que la forma de evitarlo es con observables.
dennis.sheppard
1
@Sufyan: Supongo que agregar los campos del proveedor a los niños hace que Angular cree nuevas instancias privadas para cada uno. Cuando no los agrega, usan la instancia "singleton" del padre.
Ralph
1
Parece que esto ya no funciona con las últimas actualizaciones
Sufyan Jabr
3
Esto está desactualizado. directivesya no se declaran en el componente.
Nate Gardner
26

En el caso de 2 componentes diferentes (componentes no anidados, padre / hijo / nieto) le sugiero esto:

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) {
    this.missionAnnouncedSource.next(mission);
  }
  confirmMission(astronaut: string) {
    this.missionConfirmedSource.next(astronaut);
  }

}

Astronauta Componente:

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

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

Dudi
fuente
2
Le gustaría agregar algo de terminología a esta respuesta. Creo que se ajusta a RxJS, patrón observable, etc. No estoy del todo seguro, pero agregar descripciones a algo de esto sería beneficioso para las personas (como yo).
karns
13

Una forma de hacerlo es utilizar un servicio compartido .

Sin embargo, encuentro que la siguiente solución es mucho más simple, permite compartir datos entre 2 hermanos (probé esto solo en Angular 5 )

En su plantilla de componente principal:

<!-- Assigns "AppSibling1Component" instance to variable "data" -->
<app-sibling1 #data></app-sibling1>
<!-- Passes the variable "data" to AppSibling2Component instance -->
<app-sibling2 [data]="data"></app-sibling2> 

app-sibling2.component.ts

import { AppSibling1Component } from '../app-sibling1/app-sibling1.component';
...

export class AppSibling2Component {
   ...
   @Input() data: AppSibling1Component;
   ...
}
Caner
fuente
¿No se opone esto a la idea de un acoplamiento flexible y, por tanto, de componentes?
Robin
¿Alguien tiene alguna idea de si esta es una forma limpia o sucia? Parece mucho más simple compartir datos en una dirección, por ejemplo, solo de sibiling1 a sibiling2, pero no al revés
Sarah
7

Una directiva puede tener sentido en determinadas situaciones para "conectar" componentes. De hecho, las cosas que se conectan ni siquiera necesitan ser componentes completos y, a veces, es más liviano y en realidad más simple si no lo son.

Por ejemplo, tengo un Youtube Playercomponente (envoltura de la API de Youtube) y quería algunos botones de control para él. La única razón por la que los botones no son parte de mi componente principal es que están ubicados en otra parte del DOM.

En este caso, en realidad es solo un componente de 'extensión' que solo será útil con el componente 'principal'. Digo 'padre', pero en DOM es un hermano, así que llámalo como quieras.

Como dije, ni siquiera necesita ser un componente completo, en mi caso es solo un <button>(pero podría ser un componente).

@Directive({
    selector: '[ytPlayerPlayButton]'
})
export class YoutubePlayerPlayButtonDirective {

    _player: YoutubePlayerComponent; 

    @Input('ytPlayerVideo')
    private set player(value: YoutubePlayerComponent) {
       this._player = value;    
    }

    @HostListener('click') click() {
        this._player.play();
    }

   constructor(private elementRef: ElementRef) {
       // the button itself
   }
}

En el HTML para ProductPage.component , ¿dónde youtube-playerestá obviamente mi componente que envuelve la API de Youtube?

<youtube-player #technologyVideo videoId='NuU74nesR5A'></youtube-player>

... lots more DOM ...

<button class="play-button"        
        ytPlayerPlayButton
        [ytPlayerVideo]="technologyVideo">Play</button>

La directiva conecta todo por mí, y no tengo que declarar el evento (click) en el HTML.

Por lo tanto, la directiva puede conectarse muy bien al reproductor de video sin tener que participar ProductPagecomo mediador.

Esta es la primera vez que hago esto, por lo que aún no estoy seguro de cuán escalable podría ser para situaciones mucho más complejas. Por esto aunque estoy contento y deja mi HTML simple y las responsabilidades de todo distinto.

Simon_Weaver
fuente
Uno de los conceptos angulares más importantes que hay que comprender es que un componente es solo una directiva con una plantilla. Una vez que realmente aprecie lo que esto significa, las directivas no serán tan aterradoras, y se dará cuenta de que puede aplicarlas a cualquier elemento para adjuntarle comportamiento.
Simon_Weaver
Intenté esto pero obtengo un error de identificador duplicado para el equivalente de player. Si dejo fuera la primera mención del jugador, obtengo un rangeError. Estoy perplejo en cuanto a cómo se supone que funciona esto.
Katharine Osborne
@KatharineOsborne Parece que en mi código real utilizo _playerpara el campo privado que representa al jugador, así que sí, obtendría un error si copiara esto exactamente. Actualizará. ¡Lo siento!
Simon_Weaver
4

Aquí hay una explicación práctica simple: Simplemente explicado aquí

En call.service.ts

import { Observable } from 'rxjs';
import { Subject } from 'rxjs/Subject';

@Injectable()
export class CallService {
 private subject = new Subject<any>();

 sendClickCall(message: string) {
    this.subject.next({ text: message });
 }

 getClickCall(): Observable<any> {
    return this.subject.asObservable();
 }
}

Componente desde donde desea llamar a observable para informar a otro componente que se hace clic en el botón

import { CallService } from "../../../services/call.service";

export class MarketplaceComponent implements OnInit, OnDestroy {
  constructor(public Util: CallService) {

  }

  buttonClickedToCallObservable() {
   this.Util.sendClickCall('Sending message to another comp that button is clicked');
  }
}

Componente en el que desea realizar una acción en el botón que hizo clic en otro componente

import { Subscription } from 'rxjs/Subscription';
import { CallService } from "../../../services/call.service";


ngOnInit() {

 this.subscription = this.Util.getClickCall().subscribe(message => {

 this.message = message;

 console.log('---button clicked at another component---');

 //call you action which need to execute in this component on button clicked

 });

}

import { Subscription } from 'rxjs/Subscription';
import { CallService } from "../../../services/call.service";


ngOnInit() {

 this.subscription = this.Util.getClickCall().subscribe(message => {

 this.message = message;

 console.log('---button clicked at another component---');

 //call you action which need to execute in this component on button clicked

});

}

Mi comprensión clara sobre la comunicación de componentes al leer esto: http://musttoknow.com/angular-4-angular-5-communicate-two-components-using-observable-subject/

Prashant M Bhavsar
fuente
Hola, muchas gracias por la solución simple> lo probé en stackblitz que funcionó bien. Pero mi aplicación tiene rutas de carga diferida (se han utilizado en: 'root') y llamadas HTTP para establecer y obtener. ¿Podrías ayudarme con las llamadas HTTP? Intenté mucho pero no
funcionó
4

El servicio compartido es una buena solución para este problema. Si también desea almacenar información sobre la actividad, puede agregar el servicio compartido a la lista de proveedores de sus módulos principales (módulo de aplicación).

@NgModule({
    imports: [
        ...
    ],
    bootstrap: [
        AppComponent
    ],
    declarations: [
        AppComponent,
    ],
    providers: [
        SharedService,
        ...
    ]
});

Entonces puede proporcionarlo directamente a sus componentes,

constructor(private sharedService: SharedService)

Con el Servicio Compartido puede usar funciones o puede crear un Asunto para actualizar varios lugares a la vez.

@Injectable()
export class FolderTagService {
    public clickedItemInformation: Subject<string> = new Subject(); 
}

En su componente de lista, puede publicar la información del elemento en el que se hizo clic,

this.sharedService.clikedItemInformation.next("something");

y luego puede obtener esta información en su componente de detalle:

this.sharedService.clikedItemInformation.subscribe((information) => {
    // do something
});

Obviamente, los datos que enumeran los componentes compartidos pueden ser cualquier cosa. Espero que esto ayude.

Nick Greaves
fuente
Este es el ejemplo más sencillo (también conocido como conciso) de este concepto de servicio compartido, y realmente debería votarse a favor para aumentar su visibilidad, ya que no hay una respuesta aceptada.
iGanja
3

Debe configurar la relación padre-hijo entre sus componentes. El problema es que puede simplemente inyectar los componentes secundarios en el constructor del componente principal y almacenarlo en una variable local. En su lugar, debe declarar los componentes secundarios en su componente principal mediante el @ViewChilddeclarador de propiedades. Así es como debería verse su componente principal:

import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { ListComponent } from './list.component';
import { DetailComponent } from './detail.component';

@Component({
  selector: 'app-component',
  template: '<list-component></list-component><detail-component></detail-component>',
  directives: [ListComponent, DetailComponent]
})
class AppComponent implements AfterViewInit {
  @ViewChild(ListComponent) listComponent:ListComponent;
  @ViewChild(DetailComponent) detailComponent: DetailComponent;

  ngAfterViewInit() {
    // afther this point the children are set, so you can use them
    this.detailComponent.doSomething();
  }
}

https://angular.io/docs/ts/latest/api/core/index/ViewChild-var.html

https://angular.io/docs/ts/latest/cookbook/component-communication.html#parent-to-view-child

Tenga cuidado, el componente hijo no estará disponible en el constructor del componente padre, justo después de ngAfterViewInitllamar al gancho del ciclo de vida. Para atrapar este gancho, simplemente implemente la AfterViewInitinterfaz en su clase principal de la misma manera que lo haría con OnInit.

Pero, hay otros declaradores de propiedades como se explica en esta nota de blog: http://blog.mgechev.com/2016/01/23/angular2-viewchildren-contentchildren-difference-viewproviders/

Vereb
fuente
2

Sujetos de conducta.Escribí un blog sobre eso.

import { BehaviorSubject } from 'rxjs/BehaviorSubject';
private noId = new BehaviorSubject<number>(0); 
  defaultId = this.noId.asObservable();

newId(urlId) {
 this.noId.next(urlId); 
 }

En este ejemplo, estoy declarando un sujeto de comportamiento noid de tipo número. También es un observable. Y si "sucedió algo", esto cambiará con la función new () {}.

Entonces, en los componentes del hermano, uno llamará a la función, para hacer el cambio, y el otro se verá afectado por ese cambio, o viceversa.

Por ejemplo, obtengo la identificación de la URL y actualizo el noid del asunto del comportamiento.

public getId () {
  const id = +this.route.snapshot.paramMap.get('id'); 
  return id; 
}

ngOnInit(): void { 
 const id = +this.getId ();
 this.taskService.newId(id) 
}

Y desde el otro lado, puedo preguntar si ese ID es "lo que quiera" y hacer una elección después de eso, en mi caso, si quiero eliminar una tarea, y esa tarea es la URL actual, tiene que redirigirme. a la casa:

delete(task: Task): void { 
  //we save the id , cuz after the delete function, we  gonna lose it 
  const oldId = task.id; 
  this.taskService.deleteTask(task) 
      .subscribe(task => { //we call the defaultId function from task.service.
        this.taskService.defaultId //here we are subscribed to the urlId, which give us the id from the view task 
                 .subscribe(urlId => {
            this.urlId = urlId ;
                  if (oldId == urlId ) { 
                // Location.call('/home'); 
                this.router.navigate(['/home']); 
              } 
          }) 
    }) 
}
ValRob
fuente
1

Esto no es exactamente lo que quieres, pero seguro que te ayudará.

Me sorprende que no haya más información sobre la comunicación de componentes <=> considere este tutorial de angualr2

Para la comunicación de componentes hermanos, sugeriría ir con sharedService. Sin embargo, también hay otras opciones disponibles.

import {Component,bind} from 'angular2/core';
import {bootstrap} from 'angular2/platform/browser';
import {HTTP_PROVIDERS} from 'angular2/http';
import {NameService} from 'src/nameService';


import {TheContent} from 'src/content';
import {Navbar} from 'src/nav';


@Component({
  selector: 'app',
  directives: [TheContent,Navbar],
  providers: [NameService],
  template: '<navbar></navbar><thecontent></thecontent>'
})


export class App {
  constructor() {
    console.log('App started');
  }
}

bootstrap(App,[]);

Consulte el enlace en la parte superior para obtener más código.

Editar: esta es una demostración muy pequeña. Ya has mencionado que ya lo has probado sharedService. Así que considere este tutorial de angualr2 para obtener más información.

micronyks
fuente
0

He estado transmitiendo métodos de establecimiento del padre a uno de sus hijos a través de un enlace, llamando a ese método con los datos del componente hijo, lo que significa que el componente padre se actualiza y luego puede actualizar su segundo componente hijo con los nuevos datos. Sin embargo, requiere vincular 'esto' o usar una función de flecha.

Esto tiene la ventaja de que los niños no están tan acoplados entre sí, ya que no necesitan un servicio compartido específico.

No estoy del todo seguro de que esta sea la mejor práctica, sería interesante escuchar las opiniones de otros al respecto.

Thingamajig
fuente