Delegazione: EventEmitter o osservabile in angolare


244

Sto cercando di implementare qualcosa di simile a un modello di delega in Angolare. Quando l'utente fa clic su a nav-item, vorrei chiamare una funzione che quindi emette un evento che dovrebbe a sua volta essere gestito da qualche altro componente che ascolta l'evento.

Ecco lo scenario: ho un 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)
    }

}

Ecco il componente osservativo:

export class ObservingComponent {

  // How do I observe the event ? 
  // <----------Observe/Register Event ?-------->

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

}

La domanda chiave è: come faccio a far sì che il componente osservatore osservi l'evento in questione?


Risposte:


450

Aggiornamento 27/06/2016: invece di utilizzare Observables, utilizzare uno dei due

  • un oggetto comportamentale, come raccomandato da @Abdulrahman in un commento, oppure
  • un oggetto Replay, come raccomandato da @Jason Goemaat in un commento

Un soggetto è sia un osservabile (quindi possiamo subscribe()farlo ad esso) sia un osservatore (quindi possiamo invocarlo next()per emettere un nuovo valore). Sfruttiamo questa funzionalità. Un soggetto consente ai valori di essere multicast per molti osservatori. Non sfruttiamo questa funzione (abbiamo un solo osservatore).

BehaviorSubject è una variante di Subject. Ha la nozione di "il valore corrente". Lo sfruttiamo: ogni volta che creiamo un ObservingComponent, ottiene automaticamente il valore dell'elemento di navigazione corrente da BehaviorSubject.

Il codice seguente e il plunker usano BehaviorSubject.

ReplaySubject è un'altra variante di Subject. Se si desidera attendere fino alla produzione effettiva di un valore, utilizzare ReplaySubject(1). Mentre un BehaviorSubject richiede un valore iniziale (che verrà fornito immediatamente), ReplaySubject no. ReplaySubject fornirà sempre il valore più recente, ma poiché non ha un valore iniziale richiesto, il servizio può eseguire alcune operazioni asincrone prima di restituire il primo valore. Si attiverà comunque immediatamente sulle chiamate successive con il valore più recente. Se desideri solo un valore, utilizza first()l'abbonamento. Non è necessario annullare l'iscrizione se si utilizza 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


Risposta originale che utilizza un osservabile: (richiede più codice e logica rispetto all'utilizzo di un oggetto comportamentale, quindi non lo consiglio, ma può essere istruttivo)

Quindi, ecco un'implementazione che utilizza un osservabile anziché un EventEmitter . A differenza della mia implementazione EventEmitter, questa implementazione memorizza anche il servizio attualmente selezionato navItemnel servizio, in modo che quando viene creato un componente di osservazione, può recuperare il valore corrente tramite chiamata API navItem()e quindi essere informato delle modifiche tramite l' navChange$Osservabile.

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


Vedi anche l' esempio del ricettario di interazione dei componenti , che utilizza un Subjectoltre agli osservabili. Sebbene l'esempio sia "comunicazione padre e figlio", la stessa tecnica è applicabile per componenti non correlati.


3
È stato menzionato ripetutamente nei commenti ai problemi di Angular2 che è sconsigliato di utilizzare EventEmitterovunque tranne che per gli output. Attualmente stanno riscrivendo i tutorial (comunicazione server AFAIR) per non incoraggiare questa pratica.
Günter Zöchbauer,

2
C'è un modo per inizializzare il servizio e lanciare un primo evento dall'interno del componente Navigazione nel codice di esempio sopra? Il problema è che l' _observeroggetto del servizio non è inizializzato almeno al momento della ngOnInit()chiamata del componente di navigazione.
ComFreek,

4
Posso suggerire di usare BehaviorSubject invece di Observable. È più vicino EventEmitterperché è "caldo" nel senso che è già "condiviso", è progettato per salvare il valore corrente e infine implementa sia Osservabile che Osservatore che ti farà risparmiare almeno cinque righe di codice e due proprietà
Abdulrahman Alsoghayer

2
@PankajParkar, riguardo a "come fa il motore di rilevamento dei cambiamenti a sapere che il cambiamento è avvenuto su un oggetto osservabile" - Ho cancellato la mia precedente risposta di commento. Ho imparato di recente che Angular non corregge le scimmie subscribe(), quindi non è in grado di rilevare quando si osserva un cambiamento osservabile. Normalmente, c'è un evento asincrono che viene generato (nel mio codice di esempio, sono gli eventi clic sul pulsante) e il callback associato chiamerà next () su un osservabile. Ma il rilevamento delle modifiche viene eseguito a causa dell'evento asincrono, non a causa del cambiamento osservabile. Vedi anche i commenti Günter: stackoverflow.com/a/36846501/215945
Mark Rajcok

9
Se vuoi aspettare fino a quando non viene effettivamente prodotto un valore, puoi usarlo ReplaySubject(1). A BehaviorSubjectrichiede un valore iniziale che verrà fornito immediatamente. Il ReplaySubject(1)fornirà sempre il valore più recente, ma non ha un valore iniziale richiesto in modo che il servizio può fare qualche operazione asincrona prima di tornare di primo valore, ma ancora il fuoco immediatamente successive chiamate con l'ultimo valore. Se sei solo interessato a un valore, puoi utilizzare first()l'abbonamento e non devi annullare l'iscrizione alla fine perché sarà completo.
Jason Goemaat,

33

Ultime notizie: ho aggiunto un'altra risposta che utilizza un osservabile anziché un EventEmitter. Consiglio questa risposta. E in realtà, utilizzare un EventEmitter in un servizio è una cattiva pratica .


Risposta originale: (non farlo)

Metti l'EventEmitter in un servizio, che consente a ObservingComponent di iscriversi direttamente (e annullare l'iscrizione) all'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 provi il Plunker, ci sono alcune cose che non mi piacciono di questo approccio:

  • ObservingComponent deve annullare l'iscrizione quando viene distrutto
  • dobbiamo passare il componente in subscribe()modo che thissia impostato il corretto quando viene chiamato il callback

Aggiornamento: un'alternativa che risolve il secondo proiettile è far sì che ObservingComponent si navchangeiscriva direttamente alla proprietà EventEmitter:

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

Se ci abboniamo direttamente, non avremmo bisogno del subscribe()metodo su NavService.

Per rendere leggermente più incapsulato NavService, è possibile aggiungere un getNavChangeEmitter()metodo e utilizzare quello:

getNavChangeEmitter() { return this.navchange; }  // in NavService

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

Preferisco questa soluzione alla risposta fornita dall'onorevole Zouabi, ma non sono neanche un fan di questa soluzione, a dire il vero. Non mi interessa annullare l'iscrizione alla distruzione, ma odio il fatto che dobbiamo passare il componente per iscriverci all'evento ...
the_critic,

In realtà ci ho pensato e ho deciso di scegliere questa soluzione. Mi piacerebbe avere una soluzione leggermente più pulita, ma non sono sicuro che sia possibile (o probabilmente non sono in grado di trovare qualcosa di più elegante che dovrei dire).
the_critic il

in realtà il secondo problema di proiettile è che viene invece passato un riferimento alla funzione. da riparare: this.subscription = this.navService.subscribe(() => this.selectedNavItem());e su abbonamento:return this.navchange.subscribe(callback);
André Werlang,

1

Se si vuole seguire uno stile di programmazione orientato più reattivo, allora il concetto di "Tutto è uno stream" entra in scena e, quindi, utilizzare gli osservabili per gestire questi flussi il più spesso possibile.


1

Puoi usare:

  1. Comportamento Oggetto:

BehaviorSubject è un tipo di soggetto, un soggetto è un tipo speciale di osservabile che può agire come osservabile e osservatore è possibile iscriversi a messaggi come qualsiasi altro osservabile e al momento della sottoscrizione, restituisce l'ultimo valore del soggetto emesso dalla fonte osservabile:

Vantaggio: nessuna relazione come la relazione padre-figlio richiesta per passare i dati tra i componenti.

SERVIZIO 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 DI NAVIGAZIONE

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

COMPONENTE OSSERVANTE

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

Il secondo approccio è Event Delegation in upward direction child -> parent

  1. Utilizzo dei decoratori @Input e @Output padre che passa i dati al componente figlio e figlio che notifica al componente padre

es. Risposta data da @Ashish Sharma.


0

È necessario utilizzare il componente di navigazione nel modello di ObservingComponent (non dimenticare di aggiungere un selettore al componente di navigazione .. componente di navigazione per es.)

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

E implementare onNavGhange () in ObservingComponent

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

Ultima cosa ... non è necessario l'attributo degli eventi in @Componennt

events : ['navchange'], 

Questo aggancia solo un evento per il componente sottostante. Non è quello che sto cercando di fare. Avrei potuto semplicemente dire qualcosa del tipo (^ navchange) (il cursore è per il gorgoglio degli eventi) sul nav-itemma voglio solo emettere un evento che gli altri possano osservare.
the_critic il

puoi usare navchange.toRx (). iscriviti () .. ma dovrai avere un riferimento su navchange su ObservingComponent
Mourad Zouabi il

0

puoi usare BehaviourSubject come descritto sopra o esiste un altro modo:

puoi gestire EventEmitter in questo modo: prima aggiungi un selettore

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

}

Ora puoi gestire questo evento come supponiamo che observer.component.html sia la vista del componente Observer

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

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

 }

-2

Ho scoperto un'altra soluzione per questo caso senza utilizzare Reactivex né i servizi. In realtà adoro l'API rxjx ma penso che vada meglio quando si risolve una funzione asincrona e / o complessa. Usandolo in quel modo, mi ha superato.

Quello che penso tu stia cercando è una trasmissione. Solo quello. E ho scoperto questa soluzione:

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

Questo è un esempio di funzionamento completo: https://plnkr.co/edit/xGVuFBOpk2GP0pRBImsE


1
Guarda la risposta migliore, usa anche il metodo di iscrizione. In realtà oggi consiglierei di usare Redux o qualche altro controllo dello stato per risolvere questo problema di comunicazione tra i componenti. È infinito molto meglio di qualsiasi altra soluzione sebbene aggiunga ulteriore complessità. O usando la sintassi del gestore di eventi dei componenti Angular 2 o usando esplicitamente il metodo di iscrizione il concetto rimane lo stesso. Il mio ultimo pensiero è se vuoi una soluzione definitiva per quel problema usa Redux, altrimenti usa i servizi con l'emettitore di eventi.
Nicholas Marcaccini Augusto,

la sottoscrizione è valida purché angolare non rimuova il fatto che è osservabile. .subscribe () viene utilizzato nella risposta migliore, ma non su quel particolare oggetto.
Porschiey
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.