Comunicação de componente do irmão Angular 2

118

Eu tenho um ListComponent. Quando um item é clicado em ListComponent, os detalhes desse item devem ser mostrados em DetailComponent. Ambos estão na tela ao mesmo tempo, então não há roteamento envolvido.

Como posso informar a DetailComponent qual item em ListComponent foi clicado?

Considerei emitir um evento para o pai (AppComponent) e fazer com que o pai definisse selectedItem.id em DetailComponent com um @Input. Ou eu poderia usar um serviço compartilhado com assinaturas observáveis.


EDIT: Definir o item selecionado via evento + @Input não aciona o DetailComponent, no caso de eu precisar executar código adicional. Portanto, não tenho certeza se essa é uma solução aceitável.


Mas ambos os métodos parecem muito mais complexos do que a maneira Angular 1 de fazer as coisas, que era por meio de $ rootScope. $ Broadcast ou $ scope. $ Parent. $ Broadcast.

Como tudo no Angular 2 é um componente, estou surpreso que não haja mais informações disponíveis sobre a comunicação de componentes.

Existe outra maneira / mais simples de fazer isso?

dennis.sheppard
fonte
Você encontrou alguma maneira de compartilhar dados entre irmãos? Eu preciso disso como observável ..
Ser Humano

Respostas:

65

Atualizado para rc.4: ao tentar passar dados entre componentes irmãos no angular 2, a maneira mais simples agora (angular.rc.4) é aproveitar a injeção de dependência hierárquica do angular2 e criar um serviço compartilhado.

Aqui estaria o serviço:

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

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

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

Agora, aqui estaria o componente PAI

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{

} 

e seus dois filhos

criança 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;
    }
}

criança 2 (é irmão)

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

AGORA: Coisas a serem observadas ao usar este método.

  1. Inclui apenas o provedor de serviços para o serviço compartilhado no componente PAI e NÃO os filhos.
  2. Você ainda tem que incluir construtores e importar o serviço nos filhos
  3. Esta resposta foi respondida originalmente para uma versão beta inicial do Angulo 2. No entanto, tudo o que mudou são as instruções de importação, de modo que é tudo que você precisa atualizar se usou a versão original por acaso.
Alex J
fonte
2
Isso ainda é válido para angular-rc1?
Sergio
4
Não acredito que isso notifique o irmão de que algo foi atualizado no serviço compartilhado. Se child-component1 fizer algo que o child-component2 precise responder, este método não cuidará disso. Eu acredito que a maneira de contornar isso é com os observáveis?
dennis.sheppard
1
@Sufyan: Suponho que adicionar os campos do provedor aos filhos faz com que o Angular crie novas instâncias privadas para cada um. Quando você não os adiciona, eles usam a instância "singleton" do pai.
Ralph de
1
Parece que isso não está mais funcionando com as atualizações mais recentes
Sufyan Jabr
3
Isso está desatualizado. directivesnão são mais declarados no componente.
Nate Gardner
26

No caso de 2 componentes diferentes (componentes não aninhados, pai \ filho \ neto), sugiro o seguinte:

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

}

AstronautComponent:

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

Fonte: pais e filhos se comunicam por meio de um serviço

Dudi
fonte
2
Gostaria que você adicionasse alguma terminologia a esta resposta. Acredito que esteja de acordo com RxJS, padrão observável, etc., não totalmente certo, mas adicionar descrições a alguns deles seria benéfico para as pessoas (como eu).
karns
13

Uma maneira de fazer isso é usar um serviço compartilhado .

No entanto, acho a solução a seguir muito mais simples, ela permite compartilhar dados entre 2 irmãos. (Eu testei isso apenas no Angular 5 )

Em seu modelo de componente pai:

<!-- 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
fonte
Isso não se opõe à ideia de acoplamento fraco e, portanto, de componentes?
Robin de
Alguém tem ideia se essa é uma forma limpa ou suja de fazer isso? Parece muito mais simples compartilhar dados em uma direção, por exemplo, apenas de sibiling1 para sibiling2, mas não o contrário
Sarah
7

Uma diretiva pode fazer sentido em certas situações para 'conectar' componentes. Na verdade, as coisas que estão sendo conectadas nem precisam ser componentes completos e, às vezes, é mais leve e, na verdade, mais simples se não forem.

Por exemplo, eu tenho um Youtube Playercomponente (envolvendo a API do Youtube) e queria alguns botões de controle para ele. A única razão pela qual os botões não fazem parte do meu componente principal é que eles estão localizados em outro lugar no DOM.

Nesse caso, é realmente apenas um componente de 'extensão' que só poderá ser usado com o componente 'pai'. Eu digo 'pai', mas no DOM é um irmão - então chame do que quiser.

Como eu disse, ele nem precisa ser um componente completo, no meu caso é apenas um <button>(mas pode ser um 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
   }
}

No HTML para ProductPage.component, onde youtube-playerestá obviamente meu componente que envolve a API do Youtube.

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

... lots more DOM ...

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

A diretiva conecta tudo para mim, e não preciso declarar o evento (clique) no HTML.

Portanto, a diretiva pode se conectar perfeitamente ao reprodutor de vídeo sem ter que ProductPageser um mediador.

Esta é a primeira vez que realmente faço isso, então ainda não tenho certeza de como isso pode ser escalonável para situações muito mais complexas. Por isso estou feliz e deixa meu HTML simples e com responsabilidades de tudo distintas.

Simon_Weaver
fonte
Um dos conceitos angulares mais importantes para entender é que um componente é apenas uma diretiva com um modelo. Uma vez que você realmente aprecie o que isso significa, as diretivas não serão tão assustadoras - e você perceberá que pode aplicá-las a qualquer elemento para anexar comportamento a ele.
Simon_Weaver
Eu tentei isso, mas recebo um erro de identificador duplicado para o equivalente a player. Se eu deixar de fora a primeira menção de jogador, recebo um rangeError. Estou perplexo sobre como isso deve funcionar.
Katharine Osborne
@KatharineOsborne Parece que no meu código real eu uso _playerpara o campo privado que representa o jogador, então sim, você obteria um erro se copiasse isso exatamente. Atualizará. Desculpe!
Simon_Weaver
4

Aqui está uma explicação prática simples: Simplesmente explicado aqui

Em 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 de onde você deseja chamar observável para informar outro componente que o botão foi clicado

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 onde você deseja executar a ação no botão clicado em outro 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

});

}

Meu entendimento claro sobre a comunicação de componentes lendo isto: http://musttoknow.com/angular-4-angular-5-communicate-two-components-using-observable-subject/

Prashant M Bhavsar
fonte
Ei, muito obrigado pela solução simples> eu tentei no stackblitz que funcionou bem. Mas meu aplicativo tem rotas lentas carregadas (usei o fornecido em: 'root') e chamadas HTTP para definir e obter. Você poderia me ajudar com chamadas HTTP. tentei muito, mas não funcionou:
Kshri
4

O serviço compartilhado é uma boa solução para esse problema. Se você deseja armazenar algumas informações de atividade também, pode adicionar o serviço compartilhado à sua lista de provedores de módulos principais (app.module).

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

Depois, você pode fornecê-lo diretamente aos seus componentes,

constructor(private sharedService: SharedService)

Com o serviço compartilhado, você pode usar funções ou criar um assunto para atualizar vários locais de uma vez.

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

Em seu componente de lista, você pode publicar informações do item clicado,

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

e você pode buscar essas informações em seu componente de detalhe:

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

Obviamente, os dados que listam os compartilhamentos de componentes podem ser qualquer coisa. Espero que isto ajude.

Nick Greaves
fonte
Este é o exemplo mais direto (também conhecido como conciso) desse conceito de serviço compartilhado, e realmente deveria ser votado para aumentar sua visibilidade, uma vez que não há uma resposta aceita.
iGanja
3

Você precisa configurar a relação pai-filho entre seus componentes. O problema é que você pode simplesmente injetar os componentes filhos no construtor do componente pai e armazená-los em uma variável local. Em vez disso, você deve declarar os componentes filho em seu componente pai usando o @ViewChilddeclarador de propriedade. Esta é a aparência de seu componente pai:

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

Cuidado, o componente filho não estará disponível no construtor do componente pai, logo após o ngAfterViewInitgancho do ciclo de vida ser chamado. Para pegar este gancho AfterViewInit, basta implementar a interface em sua classe pai da mesma maneira que você faria comOnInit .

Mas, existem outros declaradores de propriedade, conforme explicado nesta nota de blog: http://blog.mgechev.com/2016/01/23/angular2-viewchildren-contentchildren-difference-viewproviders/

Vereb
fonte
2

Assuntos de comportamento. Eu escrevi um blog sobre isso.

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

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

Neste exemplo, estou declarando um assunto de comportamento noid do tipo número. Também é um observável. E se "algo aconteceu", isso mudará com a função new () {}.

Então, nos componentes do irmão, um vai chamar a função, para fazer a mudança, e o outro será afetado por essa mudança, ou vice-versa.

Por exemplo, eu obtenho o id do URL e atualizo o noid do assunto de comportamento.

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

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

E por outro lado, posso perguntar se esse ID é "o que quer que eu queira" e fazer uma escolha depois disso, no meu caso se eu quero delte uma tarefa, e essa tarefa é o url atual, ela tem que me redirecionar para a 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
fonte
1

Isso não é exatamente o que você quer, mas com certeza irá ajudá-lo

Estou surpreso que não haja mais informações por aí sobre comunicação de componentes <=> considere este tutorial de angualr2

Para comunicação de componentes irmãos, sugiro ir com sharedService. No entanto, também existem outras opções disponíveis.

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 o link no topo para obter mais códigos.

Edit: Esta é uma pequena demonstração. Você já mencionou que já tentou sharedService. Portanto, considere este tutorial de angualr2 para obter mais informações.

micronyks
fonte
0

Tenho passado métodos setter do pai para um de seus filhos por meio de uma vinculação, chamando esse método com os dados do componente filho, o que significa que o componente pai é atualizado e pode então atualizar seu segundo componente filho com os novos dados. Requer vincular 'this' ou usar uma função de seta.

Isso tem a vantagem de que as crianças não estão tão acopladas umas às outras, pois não precisam de um serviço compartilhado específico.

Não estou totalmente certo de que esta seja a melhor prática, seria interessante ouvir a opinião de outros sobre isso.

Thingamajig
fonte