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

244

Я пытаюсь реализовать что-то вроде шаблона делегирования в 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!');
  }

}

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

the_critic
источник
Согласно документации:> Класс EventEmitter расширяет Observable.
зал

Ответы:

450

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

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

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

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

Код ниже и плункер используют 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$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


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

Марк Райкок
источник
3
В комментариях к проблемам Angular2 неоднократно упоминалось, что его не рекомендуется использовать EventEmitterгде-либо, кроме выходов. В настоящее время они переписывают учебные пособия (сервер связи AFAIR), чтобы не поощрять эту практику.
Гюнтер Цохбауэр
2
Есть ли способ инициализировать службу и запустить первое событие из компонента Navigation в приведенном выше примере кода? Проблема заключается в том, _observerчто объект службы по меньшей мере не инициализируется во время ngOnInit()вызова компонента навигации.
ComFreek
4
Могу ли я предложить использовать BehaviorSubject вместо Observable. Он ближе к тому, EventEmitterпотому что он «горячий», то есть он уже «общий», он предназначен для сохранения текущего значения и, наконец, реализует как Observable, так и Observer, которые сохранят вам как минимум пять строк кода и два свойства
Абдулрахман Аутхайер
2
@PankajParkar, относительно того, «как механизм обнаружения изменений узнает, что изменение произошло на наблюдаемом объекте» - я удалил свой предыдущий ответ на комментарий. Недавно я узнал, что Angular не делает обезьяньих патчей subscribe(), поэтому не может обнаружить, когда наблюдаемые изменения изменяются. Обычно, возникает некоторое асинхронное событие (в моем примере кода это события нажатия кнопки), и связанный с ним обратный вызов будет вызывать next () для наблюдаемой. Но обнаружение изменений происходит из-за асинхронного события, а не из-за наблюдаемого изменения. См. Также комментарии Гюнтера: stackoverflow.com/a/36846501/215945
Марк Райкок
9
Если вы хотите подождать, пока значение не будет получено, вы можете использовать ReplaySubject(1). A BehaviorSubjectтребует начального значения, которое будет предоставлено немедленно. ReplaySubject(1)Всегда будет обеспечивать самое последнее значение, но не имеет начальное значение , необходимое , чтобы обслуживание может сделать какую - то операцию асинхронной перед его возвращением в первое значение, но все - таки срабатывает сразу при последующих вызовах с последним значением. Если вас интересует только одно значение, которое вы можете использовать first()в подписке, и вам не нужно отписываться в конце, потому что оно будет выполнено.
Джейсон Гомаат
33

Последние новости: я добавил еще один ответ, который использует 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при вызове вызывался правильный

Обновление: альтернатива, которая решает 2-ую пулю, состоит в том, чтобы 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));
}
Марк Райкок
источник
Я предпочитаю это решение ответу, предоставленному г-ном Зуаби, но я, честно говоря, тоже не фанат этого решения. Меня не волнует отказ от подписки на уничтожение, но я ненавижу тот факт, что мы должны передать компонент, чтобы подписаться на событие ...
the_critic
Я действительно думал об этом и решил пойти с этим решением. Я хотел бы иметь немного более чистое решение, но я не уверен, что это возможно (или я, вероятно, не в состоянии придумать что-то более элегантное, я должен сказать).
the_critic
на самом деле проблема второго пункта заключается в том, что вместо этого передается ссылка на функцию. исправить: this.subscription = this.navService.subscribe(() => this.selectedNavItem());и на подписку:return this.navchange.subscribe(callback);
Андре Верланг
1

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

Кришна Ганеривал
источник
1

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

  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
  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.

Хизер
источник
0

Вам необходимо использовать компонент Navigation в шаблоне ObservingComponent (не забудьте добавить селектор в компонент Navigation. Компонент для навигации, например)

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

И реализовать onNavGhange () в ObservingComponent

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

И последнее ... вам не нужен атрибут events в @Componennt

events : ['navchange'], 
Мурад Зуаби
источник
Это только подключает событие для базового компонента. Это не то, что я пытаюсь сделать. Я мог бы просто сказать что-то вроде (^ navchange) (каретка для всплытия событий), nav-itemно я просто хочу выпустить событие, которое могут наблюдать другие.
the_critic
Вы можете использовать navchange.toRx (). subscribe () .. но вам нужно будет иметь ссылку на navchange на ObservingComponent
Mourad Zouabi
0

Вы можете использовать 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)
}

}

Теперь вы можете обработать это событие, например , предположим, что наблюдающий.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);
 }

 }
Ашиш Шарма
источник
-2

Я нашел другое решение для этого случая без использования 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

Николас Маркаччини Аугусто
источник
1
Посмотрите на лучший ответ, он тоже использует метод подписки. На самом деле, сегодня я бы порекомендовал использовать Redux или какой-то другой контроль состояния для решения этой проблемы связи между компонентами. Это бесконечность намного лучше, чем любое другое решение, хотя и добавляет дополнительную сложность. При использовании синтаксиса обработчика событий компонентов Angular 2 или явном использовании метода подписки концепция остается неизменной. Мои последние мысли: если вы хотите получить окончательное решение для этой проблемы, используйте Redux, в противном случае используйте сервисы с источником событий.
Николас Маркаччини Аугусто
подписка действительна до тех пор, пока угловая не устраняет тот факт, что она наблюдаема. .subscribe () используется в лучшем ответе, но не для этого конкретного объекта.
Porschiey