Delegación: EventEmitter u Observable en Angular


243

Estoy tratando de implementar algo como un patrón de delegación en Angular. Cuando el usuario hace clic en un nav-item, me gustaría llamar a una función que luego emite un evento que a su vez debe ser manejado por algún otro componente que escuche el evento.

Aquí está el escenario: tengo 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)
    }

}

Aquí está el componente de observación:

export class ObservingComponent {

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

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

}

La pregunta clave es, ¿cómo hago para que el componente de observación observe el evento en cuestión?


Respuestas:


449

Actualización 2016-06-27: en lugar de usar Observables, use cualquiera

  • un BehaviorSubject, según lo recomendado por @Abdulrahman en un comentario, o
  • un ReplaySubject, según lo recomendado por @Jason Goemaat en un comentario

Un Sujeto es tanto un Observable (para que podamos subscribe()hacerlo) como un Observador (para que podamos invocarlo next()y emitir un nuevo valor). Explotamos esta característica. Un sujeto permite que los valores sean multidifusión para muchos observadores. No explotamos esta característica (solo tenemos un observador).

BehaviorSubject es una variante de Subject. Tiene la noción de "el valor actual". Explotamos esto: cada vez que creamos un componente Observador, obtiene el valor del elemento de navegación actual del BehaviorSubject automáticamente.

El código a continuación y el plunker usan BehaviorSubject.

ReplaySubject es otra variante de Subject. Si desea esperar hasta que se produzca un valor, use ReplaySubject(1). Mientras que BehaviorSubject requiere un valor inicial (que se proporcionará de inmediato), ReplaySubject no. ReplaySubject siempre proporcionará el valor más reciente, pero dado que no tiene un valor inicial requerido, el servicio puede realizar algunas operaciones asíncronas antes de devolver su primer valor. Todavía se disparará inmediatamente en llamadas posteriores con el valor más reciente. Si solo quieres un valor, úsalo first()en la suscripción. No tiene que darse de baja si lo usa 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


Respuesta original que usa un Observable: (requiere más código y lógica que usar un BehaviorSubject, por lo que no lo recomiendo, pero puede ser instructivo)

Entonces, aquí hay una implementación que usa un Observable en lugar de un EventEmitter . A diferencia de mi implementación EventEmitter, esta implementación también almacena el seleccionado actualmente navItemen el servicio, de modo que cuando se crea un componente de observación, puede recuperar el valor actual a través de una llamada API navItem()y luego ser notificado de los cambios a través del 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


Consulte también el ejemplo del Libro de cocina de interacción de componentes , que utiliza Subjectademás de observables. Aunque el ejemplo es "comunicación entre padres e hijos", la misma técnica es aplicable para componentes no relacionados.


3
Se mencionó repetidamente en los comentarios a los problemas de Angular2 que se desaconseja usar en EventEmittercualquier lugar, excepto en las salidas. Actualmente están reescribiendo los tutoriales (comunicación del servidor AFAIR) para no fomentar esta práctica.
Günter Zöchbauer

2
¿Hay alguna manera de inicializar el servicio y activar un primer evento desde el componente de Navegación en el código de muestra anterior? El problema es que _observeral menos el objeto de servicio no se inicializa en el momento en ngOnInit()que se llama al componente de navegación.
ComFreek

44
¿Puedo sugerir usar BehaviorSubject en lugar de Observable? Está más cerca EventEmitterporque está "caliente", lo que significa que ya está "compartido", está diseñado para guardar el valor actual y finalmente implementa tanto Observable como Observer, lo que le ahorrará al menos cinco líneas de código y dos propiedades
Abdulrahman Alsoghayer,

2
@PankajParkar, con respecto a "¿cómo sabe el motor de detección de cambios que se ha producido un cambio en un objeto observable"? Eliminé mi respuesta de comentario anterior. Aprendí recientemente que Angular no parchea el mono subscribe(), por lo que no puede detectar cuándo cambia un observable. Normalmente, hay algún evento asíncrono que se dispara (en mi código de muestra, son los eventos de clic del botón), y la devolución de llamada asociada llamará a next () en un observable. Pero la detección de cambios se ejecuta debido al evento asíncrono, no debido al cambio observable. Ver también los comentarios de Günter: stackoverflow.com/a/36846501/215945
Mark Rajcok

99
Si desea esperar hasta que se produzca un valor, puede usarlo ReplaySubject(1). A BehaviorSubjectrequiere un valor inicial que se proporcionará inmediatamente. El ReplaySubject(1)siempre va a proporcionar el valor más reciente, pero no tiene un valor inicial requiere que el servicio pueda hacer alguna operación asíncrona antes de regresar a su primer valor, pero aún así el fuego inmediato en llamadas posteriores con el último valor. Si solo está interesado en un valor que puede usar first()en la suscripción y no tiene que darse de baja al final porque se completará.
Jason Goemaat

33

Noticias de última hora: he agregado otra respuesta que usa un Observable en lugar de un EventEmitter. Recomiendo esa respuesta sobre esta. Y en realidad, usar un EventEmitter en un servicio es una mala práctica .


Respuesta original: (no hagas esto)

Ponga el EventEmitter en un servicio, que permite que ObservingComponent se suscriba directamente (y cancele la suscripción) al 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);
  }
}

Si prueba el Plunker, hay algunas cosas que no me gustan de este enfoque:

  • ObservingComponent necesita darse de baja cuando se destruye
  • tenemos que pasar el componente para subscribe()que thisse establezca el correcto cuando se llama la devolución de llamada

Actualización: una alternativa que resuelve la segunda viñeta es hacer que el componente Observador se suscriba directamente al navchange propiedad EventEmitter:

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

Si nos suscribimos directamente, entonces no necesitaríamos el subscribe() método en NavService.

Para hacer que NavService esté un poco más encapsulado, puede agregar un getNavChangeEmitter()método y usarlo:

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

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

Prefiero esta solución a la respuesta proporcionada por el Sr. Zouabi, pero tampoco soy un fanático de esta solución, para ser honesto. No me importa darme de baja en la destrucción, pero odio el hecho de que tengamos que pasar el componente para suscribirnos al evento ...
the_critic

De hecho, pensé en esto y decidí ir con esta solución. Me encantaría tener una solución un poco más limpia, pero no estoy seguro de que sea posible (o probablemente no pueda encontrar algo más elegante, debería decir).
the_critic

En realidad, el segundo problema de la viñeta es que se pasa una referencia a la función. para arreglar: this.subscription = this.navService.subscribe(() => this.selectedNavItem());y al suscribirse:return this.navchange.subscribe(callback);
André Werlang

1

Si se quiere seguir un estilo de programación más reactivo, entonces definitivamente aparece el concepto de "Todo es un flujo" y, por lo tanto, se utilizan los Observables para tratar estos flujos con la mayor frecuencia posible.


1

Puedes usar:

  1. Sujeto de comportamiento:

BehaviorSubject es un tipo de sujeto, un sujeto es un tipo especial de observable que puede actuar como observable y observador puede suscribirse a mensajes como cualquier otro observable y al suscribirse, devuelve el último valor del sujeto emitido por la fuente observable:

Ventaja: No se requiere una relación como la relación padre-hijo para pasar datos entre componentes.

SERVICIO 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 DE NAVEGACIÓN

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

OBSERVANDO COMPONENTE

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

El segundo enfoque es Event Delegation in upward direction child -> parent

  1. Usando los decoradores @Input y @Output los datos de paso primario al componente secundario y el componente primario de notificación secundaria

Por ejemplo, Respondido por @Ashish Sharma.


0

Debe usar el componente de navegación en la plantilla de ObservingComponent (no olvide agregar un selector al componente de navegación ... componente de navegación para ex)

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

E implementar onNavGhange () en ObservingComponent

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

Lo último ... no necesitas el atributo de eventos en @Componennt

events : ['navchange'], 

Esto solo conecta un evento para el componente subyacente. Eso no es lo que estoy tratando de hacer. Podría haber dicho algo como (^ navchange) (el cursor es para eventos burbujeantes) nav-itempero solo quiero emitir un evento que otros puedan observar.
the_critic

puede usar navchange.toRx (). subscribe () .. pero necesitará tener una referencia sobre navchange en ObservingComponent
Mourad Zouabi

0

puede usar BehaviourSubject como se describe anteriormente o hay una forma más:

puede manejar EventEmitter de esta manera: primero agregue un selector

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

}

Ahora puede manejar este evento como supongamos que observer.component.html es la vista del componente Observador

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

luego en 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

Encontré otra solución para este caso sin usar Reactivex ni servicios. De hecho, me encanta la API rxjx, sin embargo, creo que funciona mejor al resolver una función asincrónica y / o compleja. Utilizándolo de esa manera, es bastante excedido para mí.

Lo que creo que estás buscando es una transmisión. Solo eso. Y descubrí esta solución:

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

Este es un ejemplo de trabajo completo: https://plnkr.co/edit/xGVuFBOpk2GP0pRBImsE


1
Mire la mejor respuesta, también utiliza el método de suscripción. En realidad, hoy en día recomendaría usar Redux o algún otro control de estado para resolver este problema de comunicación entre componentes. Es infinito mucho mejor que cualquier otra solución, aunque agrega complejidad adicional. Ya sea usando sintax de controlador de eventos de componentes de Angular 2 o explícitamente usando el método de suscripción, el concepto sigue siendo el mismo. Mis pensamientos finales son si desea una solución definitiva para ese problema, use Redux, de lo contrario use servicios con emisor de eventos.
Nicholas Marcaccini Augusto

la suscripción es válida siempre que angular no elimine el hecho de que es observable. .subscribe () se usa en la mejor respuesta, pero no en ese objeto en particular.
Porschiey
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.