Delegación: EventEmitter u Observable en Angular


Estoy tratando de implementar algo así 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 debería ser manejado por algún otro componente que escucha el evento.

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

}

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 que el componente de observación observe el evento en cuestión ?

Author: yurzui, 2015-12-20

6 answers

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

  • un tema de comportamiento, como recomendó @Abdulrahman en un comentario, o{[19]]}
  • un tema de repetición, como recomendó @ Jason Goemaat en un comentario

Un Sujeto es tanto un Observable (por lo que podemos subscribe() a él) y un Observador (por lo que podemos llamar next() en él para 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 del Tema. Tiene la noción de "el valor actual". Explotamos esto: cada vez que creamos un ObservingComponent, obtiene automáticamente el valor del elemento de navegación actual del BehaviorSubject.

El siguiente código y el émbolo usan BehaviorSubject.

ReplaySubject es otra variante del Tema. Si desea esperar hasta que se produzca un valor, utilizar ReplaySubject(1). Mientras que un BehaviorSubject requiere un valor inicial (que se proporcionará inmediatamente), ReplaySubject no lo hace. ReplaySubject siempre proporcionará el valor más reciente, pero como no tiene un valor inicial requerido, el servicio puede realizar alguna operación asincrónica antes de devolver su primer valor. Todavía se disparará inmediatamente en llamadas posteriores con el valor más reciente. Si solo desea un valor, use first() en la suscripción. No tiene que darse de baja si utiliza 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, así 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 navItem seleccionado actualmente en el servicio, de modo que cuando se crea un componente observing, puede recuperar el valor actual a través de la llamada API navItem(), y luego ser notificado de los cambios a través del Observable navChange$.

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


Vea también el ejemplo de Libro de cocina de Interacción de componentes , que usa un Subject además de observables. Aunque el ejemplo es "comunicación entre padres e hijos", la misma técnica es aplicable para componentes no relacionados.

 370
Author: Mark Rajcok,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:47:26

Últimas noticias: He agregado otra respuesta que usa un Observable en lugar de un EventEmitter. Recomiendo esa respuesta sobre ésta. 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 al ObservingComponent suscribirse directamente (y darse de baja) 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 intentas Émbolo, hay algunas cosas que no me gustan de este enfoque:

  • ObservingComponent necesita darse de baja cuando se destruye
  • tenemos que pasar el componente a subscribe() para que el this adecuado se establezca cuando se llame a la devolución de llamada

Actualización: Una alternativa que resuelve la segunda viñeta es que el ObservingComponent se suscriba directamente a la propiedad navchange EventEmitter:

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

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

Para hacer el NavService un poco más encapsulado, podría agregar un método getNavChangeEmitter() y usar eso:

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

constructor(private navService:NavService) {  // in ObservingComponent
   this.subscription = this.navService.getNavChangeEmitter().subscribe(data =>
     this.selectedNavItem(data));
}
 30
Author: Mark Rajcok,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:47:26

Si uno quiere seguir un estilo de programación orientado más reactivo, entonces definitivamente el concepto de "Todo es un flujo" entra en escena y, por lo tanto, use Observables para tratar con estos flujos tan a menudo como sea posible.

 1
Author: kg11,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-08-18 04:37:56

Puedes usar BehaviourSubject como se describió anteriormente o hay una forma más:

Puedes manejar EventEmitter así: 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 puedes manejar este evento como supongamos observador.componente.html es la vista del componente Observer

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

Luego en el componente de observación.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);
 }

 }
 1
Author: Ashish Sharma,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-10-05 09:43:27

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

Última cosa .. no necesita el atributo events en @Componennt

events : ['navchange'], 
 0
Author: Mourad Zouabi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-12-20 10:23:59

Encontré otra solución para este caso sin usar Reactivex ni servicios. Realmente me encanta la API rxjx, sin embargo, creo que funciona mejor cuando se resuelve una función asincrónica y/o compleja. Usarlo de esa manera, Es bastante superado para mí.

Lo que creo que estás buscando es una emisión. Sólo eso. Y encontré 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 completo de trabajo: https://plnkr.co/edit/xGVuFBOpk2GP0pRBImsE

 -2
Author: Nicholas Marcaccini Augusto,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-12-13 12:51:03