Delegation: EventEmitter oder Observable in Angular


243

Ich versuche, so etwas wie ein Delegierungsmuster in Angular zu implementieren. Wenn der Benutzer auf a klickt nav-item, möchte ich eine Funktion aufrufen, die dann ein Ereignis ausgibt, das wiederum von einer anderen Komponente behandelt werden soll, die auf das Ereignis wartet.

Hier ist das Szenario: Ich habe eine NavigationKomponente:

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

}

Hier ist die Beobachtungskomponente:

export class ObservingComponent {

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

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

}

Die Schlüsselfrage ist, wie ich die Beobachtungskomponente dazu bringe, das betreffende Ereignis zu beobachten.


Antworten:


449

Update 27.06.2016: Verwenden Sie anstelle von Observables entweder

  • ein BehaviorSubject, wie von @Abdulrahman in einem Kommentar empfohlen, oder
  • ein ReplaySubject, wie von @Jason Goemaat in einem Kommentar empfohlen

Ein Subjekt ist sowohl ein Observable (damit wir es erreichen können subscribe()) als auch ein Observer (damit wir es aufrufen next()können, um einen neuen Wert auszugeben). Wir nutzen diese Funktion. Ein Betreff ermöglicht das Multicasting von Werten an viele Beobachter. Wir nutzen diese Funktion nicht aus (wir haben nur einen Beobachter).

BehaviorSubject ist eine Variante von Subject. Es hat den Begriff "der aktuelle Wert". Wir nutzen dies aus: Wenn wir eine ObservingComponent erstellen, wird der aktuelle Wert des Navigationselements automatisch vom BehaviorSubject abgerufen.

Der folgende Code und der Plunker verwenden BehaviorSubject.

ReplaySubject ist eine weitere Variante von Subject. Wenn Sie warten möchten, bis ein Wert tatsächlich erzeugt wird, verwenden Sie ReplaySubject(1). Während für ein BehaviorSubject ein Anfangswert erforderlich ist (der sofort bereitgestellt wird), ist dies bei ReplaySubject nicht der Fall. ReplaySubject liefert immer den neuesten Wert. Da jedoch kein erforderlicher Anfangswert vorhanden ist, kann der Dienst eine asynchrone Operation ausführen, bevor der erste Wert zurückgegeben wird. Bei nachfolgenden Aufrufen mit dem neuesten Wert wird es immer noch sofort ausgelöst. Wenn Sie nur einen Wert möchten, verwenden Sie ihn first()für das Abonnement. Sie müssen sich nicht abmelden, wenn Sie verwenden 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


Ursprüngliche Antwort, die ein Observable verwendet: (Es erfordert mehr Code und Logik als die Verwendung eines BehaviorSubject, daher empfehle ich es nicht, aber es kann lehrreich sein.)

Hier ist also eine Implementierung, die ein Observable anstelle eines EventEmitter verwendet . Im Gegensatz zu meiner EventEmitter-Implementierung speichert diese Implementierung auch die aktuell navItemim Service ausgewählten Elemente , sodass beim Erstellen einer Beobachtungskomponente der aktuelle Wert per API-Aufruf abgerufen navItem()und über Änderungen über navChange$Observable benachrichtigt werden kann .

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


Siehe auch das Beispiel des Component Interaction Cookbook , Subjectin dem zusätzlich zu Observables ein verwendet wird. Obwohl das Beispiel "Kommunikation zwischen Eltern und Kindern" lautet, gilt dieselbe Technik für nicht verwandte Komponenten.


3
In Kommentaren zu Angular2-Problemen wurde wiederholt erwähnt, dass die Verwendung EventEmitternur für Ausgaben empfohlen wird . Sie schreiben derzeit die Tutorials (Server Communication AFAIR) neu, um diese Praxis nicht zu fördern.
Günter Zöchbauer

2
Gibt es eine Möglichkeit, den Dienst zu initialisieren und ein erstes Ereignis aus der Navigationskomponente im obigen Beispielcode auszulösen? Das Problem ist, dass das _observerdes Serviceobjekts zum Zeitpunkt des ngOnInit()Aufrufs der Navigationskomponente zumindest nicht initialisiert ist.
ComFreek

4
Darf ich vorschlagen, BehaviorSubject anstelle von Observable zu verwenden. Es ist näher dran, EventEmitterweil es "heiß" ist, was bedeutet, dass es bereits "geteilt" ist, den aktuellen Wert speichert und schließlich sowohl Observable als auch Observer implementiert, wodurch Sie mindestens fünf Codezeilen und zwei Eigenschaften
sparen

2
@PankajParkar, bezüglich "Woher weiß die Änderungserkennungs-Engine, dass eine Änderung an einem beobachtbaren Objekt stattgefunden hat?" - Ich habe meine vorherige Kommentarantwort gelöscht. Ich habe kürzlich erfahren, dass Angular kein Affenfeld ist subscribe(), sodass es nicht erkennen kann, wann sich ein beobachtbares Element ändert. Normalerweise wird ein asynchrones Ereignis ausgelöst (in meinem Beispielcode sind es die Schaltflächenklickereignisse), und der zugehörige Rückruf ruft next () für ein Observable auf. Die Änderungserkennung wird jedoch aufgrund des asynchronen Ereignisses ausgeführt, nicht aufgrund der beobachtbaren Änderung. Siehe auch Günter Kommentare: stackoverflow.com/a/36846501/215945
Mark Rajcok

9
Wenn Sie warten möchten, bis ein Wert tatsächlich erzeugt wird, können Sie verwenden ReplaySubject(1). A BehaviorSubjecterfordert einen Anfangswert, der sofort bereitgestellt wird. Der ReplaySubject(1)wird immer den neuesten Wert bereitstellen, es ist jedoch kein Anfangswert erforderlich, sodass der Dienst eine asynchrone Operation ausführen kann, bevor er seinen ersten Wert zurückgibt. Bei nachfolgenden Aufrufen mit dem letzten Wert wird er jedoch sofort ausgelöst. Wenn Sie nur an einem Wert interessiert sind, können Sie ihn first()für das Abonnement verwenden und müssen ihn am Ende nicht abbestellen, da dies abgeschlossen ist.
Jason Goemaat

33

Aktuelle Nachrichten: Ich habe eine weitere Antwort hinzugefügt , die ein Observable anstelle eines EventEmitter verwendet. Ich empfehle diese Antwort über diese. Tatsächlich ist die Verwendung eines EventEmitter in einem Dienst eine schlechte Praxis .


Ursprüngliche Antwort: (Tu das nicht)

Stellen Sie den EventEmitter in einen Dienst, mit dem die ObservingComponent das Ereignis direkt abonnieren (und abbestellen) kann :

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

Wenn Sie das versuchen Plunker, gibt es ein paar Dinge, die ich an diesem Ansatz nicht mag:

  • ObservingComponent muss sich abmelden, wenn es zerstört wird
  • Wir müssen die Komponente übergeben, subscribe()damit thisbeim Aufruf des Rückrufs das richtige festgelegt wird

Update: Eine Alternative, die das zweite Aufzählungszeichen löst, besteht darin, dass die ObservingComponent die navchangeEventEmitter-Eigenschaft direkt abonniert :

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

Wenn wir uns direkt subscribe()anmelden, benötigen wir die Methode im NavService nicht.

Um den NavService etwas gekapselter zu gestalten, können Sie eine getNavChangeEmitter()Methode hinzufügen und diese verwenden:

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

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

Ich ziehe diese Lösung der Antwort von Herrn Zouabi vor, aber ich bin auch kein Fan dieser Lösung, um ehrlich zu sein. Es ist mir egal, ob ich mich bei Zerstörung abmelden möchte, aber ich hasse die Tatsache, dass wir die Komponente übergeben müssen, um das Ereignis zu abonnieren ...
the_critic

Ich habe tatsächlich darüber nachgedacht und mich für diese Lösung entschieden. Ich hätte gerne eine etwas sauberere Lösung, bin mir aber nicht sicher, ob dies möglich ist (oder ich bin wahrscheinlich nicht in der Lage, etwas Eleganteres zu finden, sollte ich sagen).
the_critic

Tatsächlich besteht das zweite Problem darin, dass stattdessen ein Verweis auf die Funktion übergeben wird. zu beheben: this.subscription = this.navService.subscribe(() => this.selectedNavItem());und auf abonnieren:return this.navchange.subscribe(callback);
André Werlang

1

Wenn man einem reaktiveren Programmierstil folgen möchte, kommt definitiv das Konzept "Alles ist ein Stream" ins Spiel und verwendet daher Observables, um diese Streams so oft wie möglich zu verarbeiten.


1

Sie können entweder verwenden:

  1. Verhalten Betreff:

BehaviourSubject ist eine Art von Betreff, ein Betreff ist eine spezielle Art von Observable, die als Observable fungieren kann, und Beobachter, den Sie wie jedes andere Observable abonnieren können, und beim Abonnieren den letzten Wert des Betreffs zurückgeben, der von der beobachtbaren Quelle ausgegeben wird:

Vorteil: Keine Beziehung wie Eltern-Kind-Beziehung erforderlich, um Daten zwischen Komponenten zu übertragen.

NAV SERVICE

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

NAVIGATIONSKOMPONENTE

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

BEOBACHTUNG DER KOMPONENTE

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

Zweiter Ansatz ist Event Delegation in upward direction child -> parent

  1. Verwenden von übergeordneten @ Input- und @ Output-Dekoratoren, die Daten an die untergeordnete Komponente übergeben und die übergeordnete Komponente untergeordnete Benachrichtigung

zB Beantwortet von @Ashish Sharma.


0

Sie müssen die Navigationskomponente in der Vorlage von ObservingComponent verwenden (vergessen Sie nicht, der Navigationskomponente einen Selektor hinzuzufügen . Navigationskomponente zum Beispiel)

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

Und implementieren Sie onNavGhange () in ObservingComponent

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

Als letztes benötigen Sie das Ereignisattribut in @Componennt nicht

events : ['navchange'], 

Dadurch wird nur ein Ereignis für die zugrunde liegende Komponente verbunden. Das versuche ich nicht. Ich hätte einfach so etwas wie (^ navchange) (das Caret ist für das Sprudeln von Ereignissen) auf dem sagen können, nav-itemaber ich möchte nur ein Ereignis ausgeben, das andere beobachten können.
the_critic

Sie können navchange.toRx (). subscribe () .. verwenden, aber Sie benötigen einen Verweis auf navchange auf ObservingComponent
Mourad Zouabi

0

Sie können BehaviourSubject wie oben beschrieben verwenden oder es gibt noch einen weiteren Weg:

Sie können mit EventEmitter folgendermaßen umgehen: Fügen Sie zuerst einen Selektor hinzu

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

}

Jetzt können Sie dieses Ereignis wie folgt behandeln: Angenommen, Observer.Component.html ist die Ansicht der Observer-Komponente

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

dann in der 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

Ich habe eine andere Lösung für diesen Fall gefunden, ohne Reactivex oder beide Dienste zu verwenden. Ich liebe die rxjx-API, aber ich denke, sie eignet sich am besten zum Auflösen einer asynchronen und / oder komplexen Funktion. Wenn ich es auf diese Weise benutze, ist es für mich ziemlich übertroffen.

Ich denke, Sie suchen nach einer Sendung. Nur das. Und ich fand diese Lösung heraus:

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

Dies ist ein voll funktionsfähiges Beispiel: https://plnkr.co/edit/xGVuFBOpk2GP0pRBImsE


1
Schauen Sie sich die beste Antwort an, sie verwendet auch die Abonnementmethode. Eigentlich würde ich heutzutage die Verwendung von Redux oder einer anderen Statussteuerung zur Lösung dieses Kommunikationsproblems zwischen Komponenten empfehlen. Es ist unendlich viel besser als jede andere Lösung, obwohl es zusätzliche Komplexität hinzufügt. Entweder mit der Event-Handler-Sintax von Angular 2-Komponenten oder explizit mit der Subscribe-Methode bleibt das Konzept unverändert. Meine letzten Gedanken sind, wenn Sie eine endgültige Lösung für dieses Problem wünschen, verwenden Sie Redux, andernfalls verwenden Sie Dienste mit Ereignisemitter.
Nicholas Marcaccini Augusto

subscribe ist gültig, solange eckig nicht die Tatsache beseitigt, dass es beobachtbar ist. .subscribe () wird in der besten Antwort verwendet, jedoch nicht für dieses bestimmte Objekt.
Porschiey
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.