Делегирование: EventEmitter или Observable in Angular

Я пытаюсь реализовать что-то вроде шаблона делегирования в Angular. Когда пользователь нажимает на элемент nav-item, я хотел бы вызвать функцию, которая затем генерирует событие, которое, в свою очередь, должно обрабатываться другим компонентом, прослушивающим событие.

Вот сценарий: у меня есть компонент 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)
    }

}

Вот компонент наблюдения:

export class ObservingComponent {

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

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

}

Ключевой вопрос заключается в том, как заставить компонент наблюдения наблюдать за рассматриваемым событием?

Ответ 1

Обновить 2016-06-27: вместо использования Observables используйте

  • a BehaviorSubject, как рекомендовано @Abdulrahman в комментарии, или
  • a ReplaySubject, как рекомендовано @Jason Goemaat в комментарии

A Subject является одновременно наблюдаемым (поэтому мы можем subscribe() к нему) и Observer (поэтому мы можем назвать next() на нем, чтобы испустить новое значение). Мы используем эту функцию. Объект позволяет значениям быть многоадресными для многих наблюдателей. Мы не используем эту функцию (у нас есть только один наблюдатель).

BehaviorSubject является вариантом Subject. Он имеет понятие "текущее значение". Мы используем это: всякий раз, когда мы создаем ObservingComponent, он автоматически получает текущее значение навигационного элемента из BehaviorSubject.

В приведенном ниже коде и plunker используется BehaviorSubject.

ReplaySubject - еще один вариант Subject. Если вы хотите подождать до фактического создания значения, используйте ReplaySubject(1). В то время как для BehaviorSubject требуется начальное значение (которое будет предоставлено немедленно), ReplaySubject этого не делает. ReplaySubject всегда будет предоставлять самое последнее значение, но поскольку он не имеет требуемого начального значения, служба может выполнить некоторую асинхронную операцию перед возвратом ее первого значения. Он будет немедленно срабатывать при последующих вызовах с самым последним значением. Если вам просто нужно одно значение, используйте first() в подписке. Вам не нужно отписываться, если вы используете 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


Оригинальный ответ, который использует Observable: (для этого требуется больше кода и логики, чем использование BehaviorSubject, поэтому я не рекомендую его, но это может быть поучительным)

Итак, вот реализация, которая использует Observable вместо EventEmitter. В отличие от моей реализации EventEmitter эта реализация также сохраняет выбранный в настоящий момент navItem в службе, так что, когда создается компонент наблюдения, он может получить текущее значение через вызов API navItem(), а затем получать уведомления об изменениях через 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


См. также Пример рекомендации по взаимодействию с компонентами, в котором в дополнение к наблюдаемым используется Subject. Хотя примером является "связь между родителями и детьми", тот же метод применим для несвязанных компонентов.

Ответ 2

Последние новости: Я добавил еще один ответ, в котором используется Observable, а не EventEmitter. Я рекомендую ответить на этот вопрос. И фактически, использование EventEmitter в сервисе - это плохая практика.


Оригинальный ответ: (не делайте этого)

Поместите EventEmitter в службу, которая позволяет ObservingComponent напрямую подписываться (и отписываться) на событие:

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

Если вы попробуете Plunker, мне не нравится этот подход:

  • ObservingComponent должен отказаться от подписки, когда он уничтожен.
  • мы должны передать компонент в subscribe(), чтобы правильный this был задан, когда обратный вызов называется

Обновление: альтернатива, которая решает вторую пулю, заключается в том, чтобы ObservingComponent напрямую подписался на свойство navchange EventEmitter:

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

Если мы подписываемся напрямую, нам не понадобится метод subscribe() в NavService.

Чтобы сделать NavService немного инкапсулированным, вы можете добавить метод getNavChangeEmitter() и использовать его:

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

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

Ответ 3

Если вы хотите следовать более реактивному ориентированному стилю программирования, то определенно понятие "все - поток" входит в картину и, следовательно, использует Observables для работы с этими потоками как можно чаще.

Ответ 4

вы можете использовать BehaviourSubject, как описано выше, или есть еще один способ:

вы можете обработать EventEmitter следующим образом: сначала добавьте селектор

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

}

Теперь вы можете обрабатывать это событие, например предположим, что observer.component.html - это представление компонента Observer

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

, затем в 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);
 }

 }

Ответ 5

Вам нужно использовать компонент "Навигация" в шаблоне ObservingComponent (не забывайте добавлять селектор в компонент "Навигация" ). "Навигация-компонент для ex"

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

И реализуем onNavGhange() в ObservingComponent

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

Последнее, что вам не нужен атрибут событий в @Componennt

events : ['navchange'], 

Ответ 6

Вы можете использовать либо:

  1. Поведение Тема:

BehaviorSubject - это тип субъекта, субъект - это особый тип наблюдаемого, который может выступать в качестве наблюдаемого, и наблюдатель, которого вы можете подписать на сообщения, как и любое другое наблюдаемое, и после подписки возвращает последнее значение субъекта, излученное наблюдаемым источником:

Преимущество: для передачи данных между компонентами не требуется таких отношений, как отношения родитель-потомок.

НАВ СЕРВИС

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 consumers
  getNavItemClicked$() {
   return this.navSubject$.asObservable();
  }
}

НАВИГАЦИОННЫЙ КОМПОНЕНТ

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

НАБЛЮДАЮЩИЙ КОМПОНЕНТ

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

Второй подход - Event Delegation in upward direction child → parent

  1. Использование декораторов @Input и @Output для передачи родительских данных дочернему компоненту и родительскому компоненту, уведомляющему о дочерних процессах

например, ответ от @Ashish Sharma.

Ответ 7

Я обнаружил другое решение для этого случая без использования Reactivex ни служб. Мне действительно нравится API-интерфейс rxjx, но я думаю, что он лучше всего подходит для решения асинхронной и/или сложной функции. Используя его таким образом, он довольно превзошел меня.

Я думаю, что вы ищете для трансляции. Только то. И я нашел это решение:

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

Это полный рабочий пример: https://plnkr.co/edit/xGVuFBOpk2GP0pRBImsE