Delegação: EventEmitter ou Observable in Angular

Eu estou tentando implementar algo como um padrão de delegação em Angular. Quando o usuário clica em um nav-item , eu gostaria de chamar uma function que, em seguida, emite um evento que por sua vez deve ser tratado por algum outro componente que esteja ouvindo o evento.

Aqui está o cenário: Eu tenho um componente de Navigation :

 import {Component, Output, EventEmitter} from 'angular2/core'; @Component({ // other properties left out for brevity events : ['navchange'], template:`  ` }) export class Navigation { @Output() navchange: EventEmitter = new EventEmitter(); selectedNavItem(item: number) { console.log('selected nav item ' + item); this.navchange.emit(item) } } 

Aqui está o componente de observação:

 export class ObservingComponent { // How do I observe the event ? //  public selectedNavItem(item: number) { console.log('item index changed!'); } } 

A questão chave é: como faço o componente de observação observar o evento em questão?

Atualização 2016-06-27: em vez de usar Observables, use

  • um BehaviorSubject, como recomendado por @Abdulrahman em um comentário, ou
  • um ReplaySubject, como recomendado pelo @Jason Goemaat em um comentário

Um Assunto é um Observável (assim podemos nos subscribe() para ele) e um Observador (assim podemos chamar o next() para emitir um novo valor). Nós exploramos esse recurso. Um Assunto permite que os valores sejam multicast para muitos Observadores. Nós não exploramos esse recurso (só temos um Observer).

BehaviorSubject é uma variante do Subject. Tem a noção de “o valor atual”. Nós exploramos isso: sempre que criamos um ObservingComponent, ele obtém o valor do item de navegação atual do BehaviorSubject automaticamente.

O código abaixo e o plunker usam BehaviorSubject.

ReplaySubject é outra variante do Subject. Se você quiser esperar até que um valor seja realmente produzido, use ReplaySubject(1) . Enquanto um BehaviorSubject requer um valor inicial (que será fornecido imediatamente), ReplaySubject não. O ReplaySubject sempre fornecerá o valor mais recente, mas, como não possui um valor inicial obrigatório, o serviço pode executar uma operação assíncrona antes de retornar seu primeiro valor. Ele ainda será triggersdo imediatamente nas chamadas subsequentes com o valor mais recente. Se você quiser apenas um valor, use first() na assinatura. Você não precisa cancelar a inscrição se usar first() .

 import {Injectable} from '@angular/core' import {BehaviorSubject} from 'rxjs/BehaviorSubject'; @Injectable() export class NavService { // Observable navItem source private _navItemSource = new BehaviorSubject(0); // Observable navItem stream navItem$ = this._navItemSource.asObservable(); // service command changeNav(number) { this._navItemSource.next(number); } } 
 import {Component} from '@angular/core'; import {NavService} from './nav.service'; import {Subscription} from 'rxjs/Subscription'; @Component({ selector: 'obs-comp', template: `obs component, item: {{item}}` }) export class ObservingComponent { item: number; subscription:Subscription; constructor(private _navService:NavService) {} ngOnInit() { this.subscription = this._navService.navItem$ .subscribe(item => this.item = item) } ngOnDestroy() { // prevent memory leak when component is destroyed this.subscription.unsubscribe(); } } 
 @Component({ selector: 'my-nav', template:`  ` }) export class Navigation { item = 1; constructor(private _navService:NavService) {} selectedNavItem(item: number) { console.log('selected nav item ' + item); this._navService.changeNav(item); } } 

Plunker


Resposta original que usa um Observable: (requer mais código e lógica do que usar um BehaviorSubject, então eu não recomendo, mas pode ser instrutivo)

Então, aqui está uma implementação que usa um Observable em vez de um EventEmitter . Ao contrário da implementação do EventEmitter, essa implementação também armazena o navItem atualmente selecionado no serviço, de modo que quando um componente de observação é criado, ele pode recuperar o valor atual por meio da API chamada navItem() e ser notificado de alterações por meio do navChange$ Observable .

 import {Observable} from 'rxjs/Observable'; import 'rxjs/add/operator/share'; import {Observer} from 'rxjs/Observer'; export class NavService { private _navItem = 0; navChange$: Observable; private _observer: Observer; constructor() { this.navChange$ = new Observable(observer => this._observer = observer).share(); // share() allows multiple subscribers } changeNav(number) { this._navItem = number; this._observer.next(number); } navItem() { return this._navItem; } } @Component({ selector: 'obs-comp', template: `obs component, item: {{item}}` }) export class ObservingComponent { item: number; subscription: any; constructor(private _navService:NavService) {} ngOnInit() { this.item = this._navService.navItem(); this.subscription = this._navService.navChange$.subscribe( item => this.selectedNavItem(item)); } selectedNavItem(item: number) { this.item = item; } ngOnDestroy() { this.subscription.unsubscribe(); } } @Component({ selector: 'my-nav', template:`   `, }) export class Navigation { item:number; constructor(private _navService:NavService) {} selectedNavItem(item: number) { console.log('selected nav item ' + item); this._navService.changeNav(item); } } 

Plunker


Veja também o exemplo Cookbook de Interação de Componentes , que usa um Subject além de observáveis. Embora o exemplo seja “comunicação pai e filho”, a mesma técnica é aplicável a componentes não relacionados.

Últimas notícias: adicionei outra resposta que usa um Observável em vez de um EventEmitter. Eu recomendo que responda sobre este. E, na verdade, usar um EventEmitter em um serviço é uma prática ruim .


Resposta original: (não faça isso)

Coloque o EventEmitter em um serviço, que permite que o ObservingComponent assine diretamente (e cancele a inscrição) no evento :

 import {EventEmitter} from 'angular2/core'; export class NavService { navchange: EventEmitter = new EventEmitter(); constructor() {} emit(number) { this.navchange.emit(number); } subscribe(component, callback) { // set 'this' to component when callback is called return this.navchange.subscribe(data => call.callback(component, data)); } } @Component({ selector: 'obs-comp', template: 'obs component, index: {{index}}' }) export class ObservingComponent { item: number; subscription: any; constructor(private navService:NavService) { this.subscription = this.navService.subscribe(this, this.selectedNavItem); } selectedNavItem(item: number) { console.log('item index changed!', item); this.item = item; } ngOnDestroy() { this.subscription.unsubscribe(); } } @Component({ selector: 'my-nav', template:`  `, }) export class Navigation { constructor(private navService:NavService) {} selectedNavItem(item: number) { console.log('selected nav item ' + item); this.navService.emit(item); } } 

Se você experimentar o Plunker , há algumas coisas que eu não gosto nessa abordagem:

  • O ObservingComponent precisa cancelar a inscrição quando for destruído
  • temos que passar o componente para subscribe() para que o próprio seja definido quando o callback é chamado

Atualização: Uma alternativa que soluciona o segundo item é fazer com que o ObservingComponent assine diretamente a propriedade navchange navchange:

 constructor(private navService:NavService) { this.subscription = this.navService.navchange.subscribe(data => this.selectedNavItem(data)); } 

Se nos inscrevermos diretamente, não precisaríamos do método subscribe() no NavService.

Para tornar o NavService um pouco mais encapsulado, você pode adicionar um método getNavChangeEmitter() e usá-lo:

 getNavChangeEmitter() { return this.navchange; } // in NavService constructor(private navService:NavService) { // in ObservingComponent this.subscription = this.navService.getNavChangeEmitter().subscribe(data => this.selectedNavItem(data)); } 

Se alguém quiser seguir um estilo de programação mais reativo, então definitivamente o conceito de “Tudo é um stream” entra em cena e, portanto, use Observables para lidar com esses streams sempre que possível.

você pode usar BehaviourSubject como descrito acima ou há mais uma maneira:

você pode manipular EventEmitter assim: primeiro adicione um seletor

 import {Component, Output, EventEmitter} from 'angular2/core'; @Component({ // other properties left out for brevity selector: 'app-nav-component', //declaring selector template:`  ` }) export class Navigation { @Output() navchange: EventEmitter = new EventEmitter(); selectedNavItem(item: number) { console.log('selected nav item ' + item); this.navchange.emit(item) } } 

Agora você pode manipular este evento como vamos supor observer.component.html é a visão do componente Observer

  

então no ObservingComponent.ts

 export class ObservingComponent { //method to recieve the value from nav component public recieveIdFromNav(id: number) { console.log('here is the id sent from nav component ', id); } } 

Você precisa usar o componente de navegação no modelo de ObservingComponent (não se esqueça de adicionar um seletor ao componente de navegação. Navigation-component for ex)

  

E implementar onNavGhange () em ObservingComponent

 onNavGhange(event) { console.log(event); } 

Última coisa .. você não precisa do atributo events no @Componennt

 events : ['navchange'], 

Descobri outra solução para este caso sem usar o Reactivex nem os serviços. Eu realmente amo a API do rxjx, mas acho que é melhor quando resolvo uma function assíncrona e / ou complexa. Usando isso dessa maneira, é bem superado para mim.

O que eu acho que você está procurando é para uma transmissão. Só isso. E eu descobri essa solução:

   // This component bellow wants to know when a tab is selected // broadcast here is a property of app component   @Component class App { broadcast: EventEmitter; constructor() { this.broadcast = new EventEmitter(); } onSelectedTab(tab) { this.broadcast.emit(tab) } } @Component class AppInterestedComponent implements OnInit { broadcast: EventEmitter(); doSomethingWhenTab(tab){ ... } ngOnInit() { this.broadcast.subscribe((tab) => this.doSomethingWhenTab(tab)) } } 

Este é um exemplo completo de trabalho: https://plnkr.co/edit/xGVuFBOpk2GP0pRBImsE