Délégation: EventEmitter ou observable en angulaire


244

J'essaie de mettre en œuvre quelque chose comme un modèle de délégation dans Angular. Lorsque l'utilisateur clique sur un nav-item, je voudrais appeler une fonction qui émet ensuite un événement qui devrait à son tour être géré par un autre composant à l'écoute de l'événement.

Voici le scénario: j'ai un Navigationcomposant:

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

}

Voici la composante d'observation:

export class ObservingComponent {

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

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

}

La question clé est de savoir comment faire en sorte que la composante d'observation observe l'événement en question?


Réponses:


450

Mise à jour 2016-06-27: au lieu d'utiliser Observables, utilisez soit

  • un BehaviorSubject, comme recommandé par @Abdulrahman dans un commentaire, ou
  • un ReplaySubject, comme recommandé par @Jason Goemaat dans un commentaire

Un sujet est à la fois un observable (pour que nous y puissions subscribe()) et un observateur (pour que nous puissions l'appeler next()pour émettre une nouvelle valeur). Nous exploitons cette fonctionnalité. Un sujet permet aux valeurs d'être multidiffusées à de nombreux observateurs. Nous n'exploitons pas cette fonctionnalité (nous n'avons qu'un seul Observateur).

BehaviorSubject est une variante de Subject. Il a la notion de "valeur actuelle". Nous exploitons ceci: chaque fois que nous créons un ObservingComponent, il obtient automatiquement la valeur actuelle de l'élément de navigation du BehaviorSubject.

Le code ci-dessous et le plongeur utilisent BehaviorSubject.

ReplaySubject est une autre variante de Subject. Si vous voulez attendre qu'une valeur soit réellement produite, utilisez ReplaySubject(1). Alors qu'un BehaviorSubject nécessite une valeur initiale (qui sera fournie immédiatement), ReplaySubject n'en a pas. ReplaySubject fournira toujours la valeur la plus récente, mais comme il n'a pas de valeur initiale requise, le service peut effectuer une opération asynchrone avant de renvoyer sa première valeur. Il se déclenchera toujours immédiatement lors des appels suivants avec la valeur la plus récente. Si vous ne voulez qu'une seule valeur, utilisez-la first()sur l'abonnement. Vous n'avez pas à vous désinscrire si vous utilisez 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


Réponse originale qui utilise un Observable: (elle nécessite plus de code et de logique que d'utiliser un BehaviorSubject, donc je ne le recommande pas, mais cela peut être instructif)

Voici donc une implémentation qui utilise un Observable au lieu d'un EventEmitter . Contrairement à mon implémentation EventEmitter, cette implémentation stocke également l' navItemélément actuellement sélectionné dans le service, de sorte que lorsqu'un composant d'observation est créé, il peut récupérer la valeur actuelle via un appel API navItem(), puis être informé des modifications via l' 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


Voir aussi l' exemple du livre de recettes sur l'interaction des composants , qui utilise un Subjecten plus des observables. Bien que l'exemple soit la «communication parents-enfants», la même technique s'applique aux composants non liés.


3
Il a été mentionné à plusieurs reprises dans les commentaires sur les problèmes Angular2 qu'il est déconseillé d'utiliser EventEmittern'importe où, sauf pour les sorties. Ils sont en train de réécrire les tutoriels (communication serveur AFAIR) pour ne pas encourager cette pratique.
Günter Zöchbauer

2
Existe-t-il un moyen d'initialiser le service et de déclencher un premier événement à partir du composant Navigation dans l'exemple de code ci-dessus? Le problème est que l' _observerobjet de service n'est au moins pas initialisé au moment ngOnInit()de l'appel du composant Navigation.
ComFreek

4
Puis-je suggérer d'utiliser BehaviorSubject au lieu d'Observable. Il est plus proche EventEmittercar il est "chaud" ce qui signifie qu'il est déjà "partagé", il est conçu pour enregistrer la valeur actuelle et enfin il implémente à la fois Observable et Observer qui vous fera économiser au moins cinq lignes de code et deux propriétés
Abdulrahman Alsoghayer

2
@PankajParkar, concernant "comment le moteur de détection de changement sait-il que le changement s'est produit sur un objet observable" - J'ai supprimé ma réponse de commentaire précédente. J'ai appris récemment qu'Angular ne fait pas de patch de singe subscribe(), donc il ne peut pas détecter quand un changement observable. Normalement, il y a un événement asynchrone qui se déclenche (dans mon exemple de code, ce sont les événements de clic de bouton), et le rappel associé appellera next () sur un observable. Mais la détection des modifications s'exécute en raison de l'événement asynchrone et non en raison des modifications observables. Voir aussi les commentaires de Günter: stackoverflow.com/a/36846501/215945
Mark Rajcok

9
Si vous voulez attendre qu'une valeur soit réellement produite, vous pouvez l'utiliser ReplaySubject(1). A BehaviorSubjectrequiert une valeur initiale qui sera fournie immédiatement. Le ReplaySubject(1)fournira toujours la valeur la plus récente, mais n'a pas de valeur initiale requise pour que le service puisse effectuer une opération asynchrone avant de renvoyer sa première valeur, mais se déclenche toujours immédiatement lors des appels suivants avec la dernière valeur. Si vous êtes simplement intéressé par une valeur que vous pouvez utiliser first()sur l'abonnement et que vous n'avez pas à vous désinscrire à la fin, car cela se terminera.
Jason Goemaat

33

Dernières nouvelles: J'ai ajouté une autre réponse qui utilise un Observable plutôt qu'un EventEmitter. Je recommande cette réponse par rapport à celle-ci. Et en fait, l'utilisation d'un EventEmitter dans un service est une mauvaise pratique .


Réponse originale: (ne faites pas cela)

Mettez l'EventEmitter dans un service, ce qui permet à l'ObservingComponent de s'abonner (et de se désinscrire) directement à l'événement :

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

Si vous essayez le Plunker, il y a quelques choses que je n'aime pas dans cette approche:

  • ObservingComponent doit se désinscrire lorsqu'il est détruit
  • nous devons passer le composant pour subscribe()que le bon thissoit défini lorsque le rappel est appelé

Mise à jour: Une alternative qui résout la 2ème puce est d'avoir le ObservingComponent directement souscrire à la navchangepropriété EventEmitter:

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

Si nous nous abonnons directement, nous n'aurions pas besoin de la subscribe()méthode sur NavService.

Pour rendre le NavService légèrement plus encapsulé, vous pouvez ajouter une getNavChangeEmitter()méthode et l'utiliser:

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

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

Je préfère cette solution à la réponse fournie par M. Zouabi, mais je ne suis pas non plus fan de cette solution, pour être honnête. Je ne me soucie pas de me désinscrire sur la destruction, mais je déteste le fait que nous devons passer le composant pour vous abonner à l'événement ...
the_critic

En fait, j'y ai pensé et j'ai décidé d'aller avec cette solution. J'adorerais avoir une solution légèrement plus propre, mais je ne suis pas sûr que ce soit possible (ou je ne suis probablement pas en mesure de proposer quelque chose de plus élégant, je devrais dire).
the_critic

en fait, le deuxième problème est qu'une référence à la fonction est passée à la place. réparer:this.subscription = this.navService.subscribe(() => this.selectedNavItem()); et sur abonnement:return this.navchange.subscribe(callback);
André Werlang

1

Si l'on veut suivre un style de programmation plus réactif, alors le concept de "Tout est un flux" entre en jeu et, par conséquent, utilise Observables pour traiter ces flux aussi souvent que possible.


1

Vous pouvez utiliser soit:

  1. Sujet du comportement:

BehaviorSubject est un type de sujet, un sujet est un type spécial d'observable qui peut agir comme observable et observateur vous pouvez vous abonner à des messages comme tout autre observable et lors de la souscription, il retourne la dernière valeur du sujet émise par la source observable:

Avantage: aucune relation telle qu'une relation parent-enfant requise pour transmettre des données entre les composants.

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

COMPOSANTE DE NAVIGATION

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

COMPOSANTE D'OBSERVATION

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

La deuxième approche est Event Delegation in upward direction child -> parent

  1. Utilisation du parent des décorateurs @Input et @Output en passant des données au composant enfant et au composant parent notifiant l'enfant

par exemple Répondu par @Ashish Sharma.


0

Vous devez utiliser le composant Navigation dans le modèle de ObservingComponent (n'oubliez pas d'ajouter un sélecteur au composant Navigation .. navigation-component par ex)

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

Et implémentez onNavGhange () dans ObservingComponent

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

Dernière chose .. vous n'avez pas besoin de l'attribut events dans @Componennt

events : ['navchange'], 

Cela ne connecte qu'un événement pour le composant sous-jacent. Ce n'est pas ce que j'essaie de faire. J'aurais pu dire quelque chose comme (^ navchange) (le curseur est pour le bouillonnement d'événement) sur le nav-itemmais je veux juste émettre un événement que les autres peuvent observer.
the_critic

vous pouvez utiliser navchange.toRx (). subscribe () .. mais vous aurez besoin d'avoir une référence sur navchange sur ObservingComponent
Mourad Zouabi

0

vous pouvez utiliser BehaviourSubject comme décrit ci-dessus ou il existe une autre façon:

vous pouvez gérer EventEmitter comme ceci: ajoutez d'abord un sélecteur

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

}

Vous pouvez maintenant gérer cet événement comme supposons que observer.component.html est la vue du composant Observer

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

puis dans 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

J'ai trouvé une autre solution pour ce cas sans utiliser ni Reactivex ni les services. J'adore l'API rxjx, mais je pense qu'elle fonctionne mieux lors de la résolution d'une fonction asynchrone et / ou complexe. En l'utilisant de cette façon, c'est assez dépassé pour moi.

Je pense que vous cherchez une émission. Juste ça. Et j'ai découvert cette solution:

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

Ceci est un exemple de travail complet: https://plnkr.co/edit/xGVuFBOpk2GP0pRBImsE


1
Regardez la meilleure réponse, il utilise également la méthode d'abonnement. En fait, aujourd'hui, je recommanderais d'utiliser Redux ou un autre contrôle d'état pour résoudre ce problème de communication entre les composants. Il est infiniment meilleur que toute autre solution, bien qu'il ajoute une complexité supplémentaire. Soit en utilisant la sintax du gestionnaire d'événements des composants Angular 2, soit en utilisant explicitement la méthode subscribe, le concept reste le même. Mes dernières réflexions sont si vous voulez une solution définitive pour ce problème, utilisez Redux, sinon utilisez des services avec émetteur d'événements.
Nicholas Marcaccini Augusto

abonnez-vous est valable tant que angulaire ne supprime pas le fait qu'il est observable. .subscribe () est utilisé dans la meilleure réponse, mais pas sur cet objet particulier.
Porschiey
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.