Delegação: EventEmitter ou Observable in Angular

243

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

Aqui está o cenário: Eu tenho um Navigationcomponente:

import {Component, Output, EventEmitter} from 'angular2/core';

@Component({
    // other properties left out for brevity
    events : ['navchange'], 
    template:`
      <div class="nav-item" (click)="selectedNavItem(1)"></div>
    `
})

export class Navigation {

    @Output() navchange: EventEmitter<number> = 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 ? 
  // <----------Observe/Register Event ?-------->

  public selectedNavItem(item: number) {
    console.log('item index changed!');
  }

}

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

the_critic
fonte
De acordo com os documentos:> A classe EventEmitter estende Observable.
halllo 18/06

Respostas:

449

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

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

Um Assunto é um Observável (para que possamos subscribe()fazê-lo) e um Observador (para que possamos chamá next()-lo para emitir um novo valor). Nós exploramos esse recurso. Um Assunto permite que os valores sejam multicast para muitos Observadores. Não exploramos esse recurso (temos apenas um observador).

BehaviorSubject é uma variante do Subject. Tem a noção de "o valor atual". Exploramos isso: sempre que criamos um ObservingComponent, ele obtém o valor atual do item de navegação 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 ele não possui um valor inicial necessário, o serviço pode executar algumas operações assíncronas antes de retornar seu primeiro valor. Ele ainda dispara imediatamente nas chamadas subsequentes com o valor mais recente. Se você deseja 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<number>(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:`
    <div class="nav-item" (click)="selectedNavItem(1)">nav 1 (click me)</div>
    <div class="nav-item" (click)="selectedNavItem(2)">nav 2 (click me)</div>`
})
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, por isso não o recomendo, mas pode ser instrutivo)

Então, aqui está uma implementação que usa um Observable em vez de um EventEmitter . Diferentemente da minha implementação EventEmitter, essa implementação também armazena o atualmente selecionado navItemno serviço, para que, quando um componente de observação for criado, ele possa recuperar o valor atual por meio de uma chamada de API navItem()e ser notificado sobre 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<number>;
  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:`
    <div class="nav-item" (click)="selectedNavItem(1)">nav 1 (click me)</div>
    <div class="nav-item" (click)="selectedNavItem(2)">nav 2 (click me)</div>
  `,
})
export class Navigation {
  item:number;
  constructor(private _navService:NavService) {}
  selectedNavItem(item: number) {
    console.log('selected nav item ' + item);
    this._navService.changeNav(item);
  }
}

Plunker


Consulte também o exemplo do Component Interaction Cookbook , que usa um Subjectalém de observáveis. Embora o exemplo seja "comunicação entre pais e filhos", a mesma técnica é aplicável a componentes não relacionados.

Mark Rajcok
fonte
3
Foi mencionado repetidamente nos comentários aos problemas do Angular2 que é desencorajado o uso em EventEmitterqualquer lugar, exceto nas saídas. No momento, eles estão reescrevendo os tutoriais (comunicação do servidor AFAIR) para não incentivar essa prática.
Günter Zöchbauer 23/02
2
Existe uma maneira de inicializar o serviço e disparar um primeiro evento de dentro do componente Navegação no código de exemplo acima? O problema é que o _observerobjeto de serviço pelo menos não é inicializado no momento ngOnInit()em que o componente Navigation é chamado.
ComFreek
4
Posso sugerir o uso de BehaviorSubject em vez de Observable. Está mais perto EventEmitterporque está "quente", o que significa que já está "compartilhado", foi projetado para salvar o valor atual e, finalmente, implementa o Observable e o Observer, o que economizará pelo menos cinco linhas de código e duas propriedades
Abdulrahman Alsoghayer
2
@PankajParkar, sobre "como o mecanismo de detecção de alterações sabe que as alterações ocorreram no objeto observável" - excluí minha resposta de comentário anterior. Aprendi recentemente que o Angular não usa patch de macaco subscribe(), portanto não pode detectar quando um observável muda. Normalmente, há algum evento assíncrono que é acionado (no meu código de exemplo, são os eventos de clique no botão), e o retorno de chamada associado chamará next () em um observável. Mas a detecção de alterações é executada por causa do evento assíncrono, não por causa da alteração observável. Veja também os comentários de Günter: stackoverflow.com/a/36846501/215945
Mark Rajcok
9
Se você quiser esperar até que um valor seja realmente produzido, você pode usar ReplaySubject(1). A BehaviorSubjectrequer um valor inicial que será fornecido imediatamente. Ele ReplaySubject(1)sempre fornecerá o valor mais recente, mas não possui um valor inicial necessário para que o serviço possa executar alguma operação assíncrona antes de retornar seu primeiro valor, mas ainda dispara imediatamente nas chamadas subseqüentes com o último valor. Se você estiver interessado apenas em um valor, poderá usar first()a assinatura e não precisar cancelar a assinatura no final, pois isso será concluído.
Jason Goemaat
33

Notícias de Última Hora: de última adicionei outra resposta que usa um Observable em vez de um EventEmitter. Eu recomendo essa resposta sobre esta. 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 ao ObservingComponent se inscrever diretamente (e cancelar a inscrição) no evento :

import {EventEmitter} from 'angular2/core';

export class NavService {
  navchange: EventEmitter<number> = 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:`
    <div class="nav-item" (click)="selectedNavItem(1)">item 1 (click me)</div>
  `,
})
export class Navigation {
  constructor(private navService:NavService) {}
  selectedNavItem(item: number) {
    console.log('selected nav item ' + item);
    this.navService.emit(item);
  }
}

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

  • O ObservingComponent precisa cancelar a assinatura quando é destruído
  • precisamos passar o componente para subscribe()que o apropriado thisseja definido quando o retorno de chamada for chamado

Atualização: Uma alternativa que resolve o segundo marcador é fazer com que o ObservingComponent assine diretamente o navchange propriedade EventEmitter:

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

Se subscrevermos diretamente, não precisaremos do subscribe() método no NavService.

Para tornar o NavService um pouco mais encapsulado, você pode adicionar um getNavChangeEmitter()método 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));
}
Mark Rajcok
fonte
Prefiro esta solução à resposta dada pelo senhor deputado Zouabi, mas também não sou fã desta solução, para ser sincero. Eu não me importo com o cancelamento da inscrição na destruição, mas odeio o fato de que precisamos passar o componente para assinar o evento ...
the_critic
Eu realmente pensei sobre isso e decidi ir com esta solução. Adoraria ter uma solução um pouco mais limpa, mas não tenho certeza de que seja possível (ou provavelmente não sou capaz de criar algo mais elegante, devo dizer).
23415 as
na verdade, o segundo problema é que uma referência à função está sendo passada. corrigir: this.subscription = this.navService.subscribe(() => this.selectedNavItem());e ao se inscrever:return this.navchange.subscribe(callback);
André Werlang 15/02
1

Se alguém quiser seguir um estilo de programação mais reativo, então definitivamente o conceito de "Tudo é um fluxo" entra em cena e, portanto, use o Observables para lidar com esses fluxos o mais rápido possível.

Krishna Ganeriwal
fonte
1

Você pode usar:

  1. Assunto do comportamento:

BehaviorSubject é um tipo de assunto, um assunto é um tipo especial de observável que pode atuar como observável e observador. Você pode assinar mensagens como qualquer outro observável e, após a assinatura, retorna o último valor do assunto emitido pela fonte observável:

Vantagem: Nenhum relacionamento, como o relacionamento pai-filho, é necessário para transmitir dados entre componentes.

SERVIÇO NAV

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

@Injectable()
export class NavService {
  private navSubject$ = new BehaviorSubject<number>(0);

  constructor() {  }

  // Event New Item Clicked
  navItemClicked(navItem: number) {
    this.navSubject$.next(number);
  }

 // Allowing Observer component to subscribe emitted data only
  getNavItemClicked$() {
   return this.navSubject$.asObservable();
  }
}

COMPONENTE DE NAVEGAÇÃO

@Component({
  selector: 'navbar-list',
  template:`
    <ul>
      <li><a (click)="navItemClicked(1)">Item-1 Clicked</a></li>
      <li><a (click)="navItemClicked(2)">Item-2 Clicked</a></li>
      <li><a (click)="navItemClicked(3)">Item-3 Clicked</a></li>
      <li><a (click)="navItemClicked(4)">Item-4 Clicked</a></li>
    </ul>
})
export class Navigation {
  constructor(private navService:NavService) {}
  navItemClicked(item: number) {
    this.navService.navItemClicked(item);
  }
}

OBSERVANDO COMPONENTE

@Component({
  selector: 'obs-comp',
  template: `obs component, item: {{item}}`
})
export class ObservingComponent {
  item: number;
  itemClickedSubcription:any

  constructor(private navService:NavService) {}
  ngOnInit() {

    this.itemClickedSubcription = this.navService
                                      .getNavItemClicked$
                                      .subscribe(
                                        item => this.selectedNavItem(item)
                                       );
  }
  selectedNavItem(item: number) {
    this.item = item;
  }

  ngOnDestroy() {
    this.itemClickedSubcription.unsubscribe();
  }
}

A segunda abordagem é Event Delegation in upward direction child -> parent

  1. Usando os pais decoradores @Input e @Output que passam dados para o componente filho e o componente pai de notificação filho

por exemplo, Respondida por @Ashish Sharma.

Khizer
fonte
0

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

<navigation-component (navchange)='onNavGhange($event)'></navigation-component>

E implemente onNavGhange () em ObservingComponent

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

Última coisa: você não precisa do atributo events em @Componennt

events : ['navchange'], 
Mourad Zouabi
fonte
Isso conecta apenas um evento ao componente subjacente. Não é isso que estou tentando fazer. Eu poderia ter dito algo como (^ navchange) (o sinal de intercalação é para eventos borbulhando) no nav-itemmas eu só quero emitir um evento que outros possam observar.
the_critic
você pode usar navchange.toRx () subscribe () .. mas você vai precisar de ter uma referência sobre navchange em ObservingComponent.
Mourad Zouabi
0

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

você pode manipular o 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:`
  <div class="nav-item" (click)="selectedNavItem(1)"></div>
`
 })

 export class Navigation {

@Output() navchange: EventEmitter<number> = new EventEmitter();

selectedNavItem(item: number) {
    console.log('selected nav item ' + item);
    this.navchange.emit(item)
}

}

Agora você pode lidar com esse evento como suponhamos que observer.component.html seja a visualização do componente Observer

<app-nav-component (navchange)="recieveIdFromNav($event)"></app-nav-component>

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

 }
Ashish Sharma
fonte
-2

Eu descobri outra solução para este caso sem usar o Reactivex nem os serviços. Na verdade, eu amo a API rxjx, no entanto, acho que ela funciona melhor ao resolver uma função assíncrona e / ou complexa. Usá-lo dessa maneira, é muito superior a mim.

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

<app>
  <app-nav (selectedTab)="onSelectedTab($event)"></app-nav>
       // This component bellow wants to know when a tab is selected
       // broadcast here is a property of app component
  <app-interested [broadcast]="broadcast"></app-interested>
</app>

 @Component class App {
   broadcast: EventEmitter<tab>;

   constructor() {
     this.broadcast = new EventEmitter<tab>();
   }

   onSelectedTab(tab) {
     this.broadcast.emit(tab)
   }    
 }

 @Component class AppInterestedComponent implements OnInit {
   broadcast: EventEmitter<Tab>();

   doSomethingWhenTab(tab){ 
      ...
    }     

   ngOnInit() {
     this.broadcast.subscribe((tab) => this.doSomethingWhenTab(tab))
   }
 }

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

Nicholas Marcaccini Augusto
fonte
1
Procure a melhor resposta, ele também usa o método de assinatura. Atualmente, eu recomendaria usar o Redux ou algum outro controle de estado para resolver esse problema de comunicação entre os componentes. É infinito muito melhor do que qualquer outra solução, embora adicione complexidade extra. Usando a sintaxe do manipulador de eventos dos componentes Angular 2 ou explicitamente usando o método de assinatura, o conceito permanece o mesmo. Meu pensamento final é se você deseja uma solução definitiva para esse problema, use Redux, caso contrário, use serviços com emissor de eventos.
Nicholas Marcaccini Augusto
A inscrição é válida desde que angular não remova o fato de ser observável. .subscribe () é usado na melhor resposta, mas não nesse objeto em particular.
Porschiey