Delegacja: EventEmitter lub Observable in Angular


244

Usiłuję zaimplementować coś w rodzaju wzoru delegowania w Angular. Gdy użytkownik kliknie „a” nav-item, chciałbym wywołać funkcję, która następnie emituje zdarzenie, które z kolei powinno być obsługiwane przez inny komponent nasłuchujący zdarzenia.

Oto scenariusz: Mam Navigationkomponent:

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

}

Oto element obserwacyjny:

export class ObservingComponent {

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

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

}

Kluczowe pytanie brzmi: jak sprawić, by obserwujący obserwował obserwowane zdarzenie?


Odpowiedzi:


450

Aktualizacja 27.06.2016: zamiast korzystać z Obserwowalnych, użyj jednego z nich

  • BehaviorSubject, zgodnie z zaleceniem @Abdulrahman w komentarzu, lub
  • a ReplaySubject, zgodnie z zaleceniem @Jason Goemaat w komentarzu

Dotyczy to zarówno Obserwowalne (tak, możemy subscribe()do niego) i obserwatora (tak możemy nazwać next()go, aby emitować nową wartość). Wykorzystujemy tę funkcję. Temat pozwala wartościom być multiemisją dla wielu obserwatorów. Nie wykorzystujemy tej funkcji (mamy tylko jednego Obserwatora).

BehaviorSubject to odmiana podmiotu. Ma pojęcie „bieżącej wartości”. Wykorzystujemy to: za każdym razem, gdy tworzymy ObservingComponent, automatycznie pobiera bieżącą wartość elementu nawigacyjnego z BehaviorSubject.

Poniższy kod i plunker używają BehaviorSubject.

ReplaySubject to kolejny wariant podmiotu. Jeśli chcesz poczekać, aż wartość zostanie faktycznie wygenerowana, użyj ReplaySubject(1). Podczas gdy BehaviorSubject wymaga wartości początkowej (która zostanie podana natychmiast), ReplaySubject nie. ReplaySubject zawsze zapewnia najnowszą wartość, ale ponieważ nie ma wymaganej wartości początkowej, usługa może wykonać operację asynchroniczną przed zwróceniem swojej pierwszej wartości. Nadal będzie uruchamiany natychmiast po kolejnych połączeniach o najnowszej wartości. Jeśli chcesz tylko jedną wartość, skorzystaj first()z subskrypcji. Nie musisz rezygnować z subskrypcji, jeśli korzystasz 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


Oryginalna odpowiedź, która wykorzystuje Observable: (wymaga więcej kodu i logiki niż użycie BehaviorSubject, więc nie polecam, ale może być pouczająca)

Oto implementacja korzystająca z Observable zamiast EventEmitter . W przeciwieństwie do mojej implementacji EventEmitter, ta implementacja przechowuje również aktualnie wybrany navItemw usłudze, dzięki czemu, gdy tworzony jest komponent obserwacyjny, może on pobierać bieżącą wartość za pośrednictwem wywołania API navItem(), a następnie być powiadamiany o zmianach za pośrednictwem navChange$Obserwowalnego.

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


Zobacz także przykład książki kucharskiej interakcji między komponentami , w której Subjectoprócz obserwowalności używa się dodatkowo. Chociaż przykładem jest „komunikacja rodziców i dzieci”, ta sama technika ma zastosowanie do niepowiązanych komponentów.


3
Wielokrotnie wspomniano w komentarzach do zagadnień Angular2, których odradza się używać w EventEmitterdowolnym miejscu, z wyjątkiem wyników. Obecnie przepisują samouczki (komunikacja AFAIR z serwerem), aby nie zachęcać do tej praktyki.
Günter Zöchbauer

2
Czy istnieje sposób na zainicjowanie usługi i uruchomienie pierwszego zdarzenia z poziomu komponentu Nawigacja w powyższym przykładowym kodzie? Problem polega na tym, że _observerobiekt usługi nie jest przynajmniej inicjowany w momencie ngOnInit()wywołania komponentu Nawigacji.
ComFreek

4
Czy mogę zasugerować użycie BehaviorSubject zamiast Observable. Jest bliżej, EventEmitterponieważ jest „gorący”, co oznacza, że ​​jest już „udostępniony”, ma na celu zapisanie bieżącej wartości, a na koniec implementuje zarówno Observable, jak i Observer, co pozwoli Ci zaoszczędzić co najmniej pięć wierszy kodu i dwie właściwości
Abdulrahman Alsoghayer

2
@PankajParkar, dotyczący „skąd silnik wykrywania zmian wie, że nastąpiła zmiana na obserwowalnym obiekcie” - usunąłem moją poprzednią odpowiedź na komentarz. Niedawno dowiedziałem się, że Angular nie łata małp subscribe(), więc nie może wykryć zmian, które można zaobserwować. Zwykle jest uruchamiane jakieś zdarzenie asynchroniczne (w moim przykładowym kodzie są to zdarzenia kliknięcia przycisku), a powiązane wywołanie zwrotne wywoła next () na obserwowalnym. Wykrywanie zmian działa jednak z powodu zdarzenia asynchronicznego, a nie z powodu zauważalnej zmiany. Zobacz także komentarze Güntera: stackoverflow.com/a/36846501/215945
Mark Rajcok

9
Jeśli chcesz poczekać, aż wartość zostanie faktycznie wygenerowana, możesz użyć ReplaySubject(1). BehaviorSubjectWymaga początkowej wartości, które zostaną dostarczone natychmiast. ReplaySubject(1)Zawsze dostarczy najnowszej wartości, ale nie ma początkową wartość wymagana więc usługa może zrobić kilka operacji asynchronicznej przed wpuszczeniem go na pierwszą wartość, ale natychmiast ogień na kolejnych połączeń z ostatniej wartości. Jeśli interesuje Cię tylko jedna wartość, której możesz użyć first()w subskrypcji i nie musisz rezygnować z subskrypcji na końcu, ponieważ to się skończy.
Jason Goemaat

33

Najświeższe informacje: Dodałem inną odpowiedź, która używa Observable, a nie EventEmitter. Polecam tę odpowiedź zamiast tej. W rzeczywistości używanie EventEmittera w usłudze jest złą praktyką .


Oryginalna odpowiedź: (nie rób tego)

Umieść EventEmitter w usłudze, która pozwala ObservingComponent na bezpośrednie subskrybowanie (i anulowanie subskrypcji) zdarzenia :

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

Jeśli spróbujesz Plunker, w tym podejściu nie podoba mi się kilka rzeczy:

  • ObservingComponent musi zrezygnować z subskrypcji po zniszczeniu
  • musimy przekazać komponent, aby subscribe()właściwość thiszostała ustawiona po wywołaniu wywołania zwrotnego

Aktualizacja: Alternatywą, która rozwiązuje 2. punkt, jest navchangeobservingComponent bezpośrednio subskrybujący właściwość EventEmitter:

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

Jeśli subskrybujemy bezpośrednio, nie potrzebowalibyśmy tej subscribe()metody na NavService.

Aby NavService było nieco bardziej enkapsulowane, możesz dodać getNavChangeEmitter()metodę i użyć jej:

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

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

Wolę to rozwiązanie od odpowiedzi udzielonej przez pana posła Zouabiego, ale szczerze mówiąc, nie jestem też fanem tego rozwiązania. Nie obchodzi mnie rezygnacja z subskrypcji destrukcji, ale nienawidzę faktu, że musimy przekazać komponent, aby zasubskrybować to wydarzenie ...
the_critic

Pomyślałem o tym i zdecydowałem się na to rozwiązanie. Chciałbym mieć nieco czystsze rozwiązanie, ale nie jestem pewien, czy to możliwe (lub prawdopodobnie nie jestem w stanie wymyślić czegoś bardziej eleganckiego, co powinienem powiedzieć).
the_critic

tak naprawdę drugim problemem jest to, że zamiast tego przekazywane jest odwołanie do funkcji. naprawić: this.subscription = this.navService.subscribe(() => this.selectedNavItem());i po zasubskrybowaniu:return this.navchange.subscribe(callback);
André Werlang

1

Jeśli ktoś chce zastosować programowanie bardziej zorientowane na reakcję, zdecydowanie pojawia się koncepcja „Wszystko jest strumieniem”, a zatem należy używać Observables do radzenia sobie z tymi strumieniami tak często, jak to możliwe.


1

Możesz użyć:

  1. Przedmiot zachowania:

BehaviorSubject jest rodzajem podmiotu, podmiot jest szczególnym rodzajem obserwowalnego, który może działać jako obserwowalny, a obserwator można subskrybować wiadomości jak każdy inny obserwowalny, a po subskrypcji zwraca ostatnią wartość podmiotu emitowaną przez obserwowalne źródło:

Korzyść: brak relacji, takich jak relacja rodzic-dziecko, wymagana do przekazywania danych między komponentami.

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

KOMPONENT NAWIGACYJNY

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

ELEMENT OBSERWUJĄCY

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

Drugie podejście to Event Delegation in upward direction child -> parent

  1. Korzystanie z dekoratorów @Input i @Output nadrzędnych przekazujących dane do komponentu podrzędnego i komponentu nadrzędnego powiadamiającego podrzędny

np. Odpowiedź udzielona przez @Ashish Sharma.


0

Musisz użyć komponentu Nawigacja w szablonie ObservingComponent (nie zapomnij dodać selektora do komponentu Nawigacja .. np. Komponent nawigacyjny)

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

I zaimplementuj onNavGhange () w ObservingComponent

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

Ostatnia rzecz ... nie potrzebujesz atrybutu events w @Componennt

events : ['navchange'], 

To tylko łączy zdarzenie dla bazowego komponentu. Nie to próbuję zrobić. Mógłbym właśnie powiedzieć coś w stylu (^ navchange) (uwaga dotyczy bąbelkowania zdarzeń), nav-itemale chcę po prostu wyemitować zdarzenie, które inni mogą obserwować.
the_critic

możesz użyć navchange.toRx (). subscribe () .. ale musisz mieć odniesienie do navchange na ObservingComponent
Mourad Zouabi,

0

możesz użyć BehaviourSubject zgodnie z powyższym opisem lub istnieje jeszcze jeden sposób:

możesz obsługiwać EventEmitter w następujący sposób: najpierw dodaj selektor

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

}

Teraz możesz poradzić sobie z tym zdarzeniem, na przykład załóżmy, że observer.component.html jest widokiem komponentu Observer

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

następnie w 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

Znalazłem inne rozwiązanie dla tej sprawy bez korzystania z usług Reactivex ani. Naprawdę uwielbiam interfejs API rxjx, jednak myślę, że najlepiej działa przy rozwiązywaniu funkcji asynchronicznych i / lub złożonych. Używając go w ten sposób, to całkiem mnie przerosło.

Myślę, że szukasz audycji. Tylko to. I znalazłem to rozwiązanie:

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

To jest w pełni działający przykład: https://plnkr.co/edit/xGVuFBOpk2GP0pRBImsE


1
Spójrz na najlepszą odpowiedź, ona również używa metody subskrypcji. Właściwie w dzisiejszych czasach zaleciłbym użycie Redux lub innej kontroli stanu do rozwiązania tego problemu komunikacji między komponentami. Jest nieskończonością znacznie lepszą niż jakiekolwiek inne rozwiązanie, choć dodaje dodatkową złożoność. Zarówno przy użyciu sintax modułu obsługi zdarzeń Angular 2, jak i jawnie przy użyciu metody subscribe, koncepcja pozostaje taka sama. Moje ostatnie przemyślenia dotyczą tego, czy chcesz definitywnego rozwiązania tego problemu, użyj Redux, w przeciwnym razie skorzystaj z usług z emiterem zdarzeń.
Nicholas Marcaccini Augusto

subskrypcja jest ważna, o ile kątowa nie usuwa faktu, że jest obserwowalna. .subscribe () jest używana w najlepszej odpowiedzi, ale nie w tym konkretnym obiekcie.
Porschiey,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.