J'essaie d'implémenter quelque chose comme un modèle de délégation dans Angular. Lorsque l'utilisateur clique sur un nav-item
, je souhaite 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 composant Navigation
:
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 le composant 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.
Mise à jour 2016-06-27: au lieu d'utiliser Observables, utilisez soit
Un Subject est à la fois un observable (nous pouvons donc subscribe()
) et un observateur (nous pouvons donc appeler next()
pour émettre une nouvelle valeur). Nous exploitons cette fonctionnalité. Un sujet permet aux valeurs d'être multicast à de nombreux observateurs. Nous n'exploitons pas cette fonctionnalité (nous n'avons qu'un observateur).
BehaviorSubject est une variante de Subject. Il a la notion de "la valeur actuelle". Nous exploitons ceci: chaque fois que nous créons un ObservingComponent, il récupère automatiquement la valeur de l’élément de navigation actuel du BehaviorSubject.
Le code ci-dessous et le plunker 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 besoin. 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 sera toujours déclenché immédiatement lors des appels suivants avec la valeur la plus récente. Si vous ne voulez qu'une valeur, utilisez first()
sur l'abonnement. Vous n'êtes pas obligé de vous désabonner 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);
}
}
Réponse originale utilisant un observable: (cela nécessite plus de code et de logique que d'utiliser un BehaviorSubject, je ne le recommande donc pas, mais cela peut être instructif )
Donc, voici une implémentation qui utilise un observable au lieu d'un EventEmitter . Contrairement à mon implémentation EventEmitter, cette implémentation stocke également le navItem
actuellement sélectionné dans le service. Ainsi, lorsqu'un composant d'observation est créé, il peut extraire la valeur actuelle via l'appel API navItem()
, puis être averti des modifications via le 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);
}
}
Voir aussi le exemple de Component Interaction Cookbook , qui utilise un Subject
en plus des observables. Bien que l'exemple soit "communication parents-enfants", la même technique s'applique aux composants non liés.
Dernières nouvelles: J'ai ajouté ne autre réponse qui utilise un observable plutôt qu'un EventEmitter. Je recommande cette réponse sur celle-ci. Et en fait, utiliser un EventEmitter dans un service est mauvaise pratique .
Réponse originale: (ne fais pas ça)
Placez EventEmitter dans un service permettant à ObservingComponent de s'abonner (et de se désabonner) 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 points que je n'aime pas dans cette approche:
subscribe()
pour que le this
correct soit défini lors de l'appel du rappelMise à jour: une alternative permettant de résoudre le problème suivant consiste à faire en sorte que le composant ObservingComponent souscrive directement à la propriété navchange
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 méthode subscribe()
sur le service de navigation.
Pour rendre NavService légèrement plus encapsulé, vous pouvez ajouter une méthode getNavChangeEmitter()
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));
}
Si on veut suivre un style de programmation plus réactif, alors le concept de "Tout est un flux" entre en scène et utilise donc Observables pour traiter ces flux aussi souvent que possible.
vous pouvez utiliser BehaviourSubject comme décrit ci-dessus ou il existe un autre moyen:
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 commesupposons que observer.component.html soit la vue du composant Observer
<app-nav-component (navchange)="recieveIdFromNav($event)"></app-nav-component>
puis dans le 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);
}
}
Vous pouvez utiliser soit:
BehaviorSubject est un type de sujet, un sujet est un type spécial d'observable qui peut agir en tant qu'observable et en tant qu'observateur, vous pouvez vous abonner à des messages comme n'importe quel autre observable et lors de votre inscription, il renvoie la dernière valeur du sujet émis par la source observable:
Avantage: Aucune relation telle qu'une relation parent-enfant n'est requise pour transmettre des données entre les composants.
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();
}
}
COMPOSANT 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);
}
}
OBSERVANT LE COMPOSANT
@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
p.ex. Réponse donnée par @Ashish Sharma.
Vous devez utiliser le composant Navigation dans le modèle de ObservingComponent (n'oubliez pas d'ajouter un sélecteur au composant Navigation .. composant de navigation par exemple)
<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'],