委任:AngularでのEventEmitterまたはObservable


243

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

}

重要な問題は、監視コンポーネントに問題のイベントをどのように監視させるかです。


回答:


449

アップデート2016-06-27: Observablesを使用する代わりに、どちらかを使用

  • @Abdulrahmanがコメントで推奨するBehaviorSubject、または
  • コメントで@Jason Goemaatが推奨するReplaySubject

件名は、観察可能(私達ができるので、両方であるsubscribe()こと)とオブザーバー(私たちが呼び出すことができるように、next()新しい価値を放出することで)。この機能を利用します。サブジェクトを使用すると、値を多数のオブザーバーにマルチキャストできます。この機能は利用していません(オブザーバーは1つだけです)。

BehaviorSubjectは、Subjectのバリアントです。「現在の価値」という概念があります。これを利用します。ObservingComponentを作成すると、BehaviorSubjectから現在のナビゲーションアイテムの値が自動的に取得されます。

以下のコードとプランカーはBehaviorSubjectを使用します。

ReplaySubjectは、Subjectの別のバリアントです。値が実際に生成されるまで待機する場合は、を使用しますReplaySubject(1)。BehaviorSubjectには初期値が必要です(これはすぐに提供されます)が、ReplaySubjectでは必要ありません。ReplaySubjectは常に最新の値を提供しますが、必要な初期値がないため、サービスは最初の値を返す前に非同期操作を実行できます。それでも、最新の値を持つ後続の呼び出しですぐに起動します。1つの値だけが必要な場合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を使用するよりも多くのコードとロジックが必要ため、お勧めしませんが、参考になるかもしれません)

したがって、EventEmitterの代わりに Observableを使用する実装があります。私のEventEmitter実装とは異なり、この実装は現在選択さnavItemれているサービスも格納するため、監視コンポーネントが作成さnavItem()れると、API呼び出しを介して現在の値を取得し、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


オブザーバブルに加えてを使用するComponent Interaction Cookbookの例も参照してくださいSubject。例は「親と子のコミュニケーション」ですが、同じ手法が無関係なコンポーネントにも適用できます。


3
Angular2の問題に対するコメントで繰り返し言及されておりEventEmitter、出力以外の場所での使用は推奨されていません。彼らは現在、この実践を奨励しないようにチュートリアル(サーバー通信AFAIR)を書き直しています。
ギュンターZöchbauer

2
上記のサンプルコードのNavigationコンポーネント内からサービスを初期化して最初のイベントを発生させる方法はありますか?問題は、Navigationコンポーネントが呼び出さ_observerれたときに、サービスオブジェクトのが少なくとも初期化されていないngOnInit()ことです。
ComFreek 2016年

4
Observableの代わりにBehaviorSubjectを使用することをお勧めします。それは近いですEventEmitter、それはそれはすでに、「共有」だ、現在の値を保存するために設計されていますし、最終的にそれはあなたのコードの少なくとも5つの行と2つのプロパティを保存しますどの観測可能とオブザーバーの両方を実装意味する「ホット」だから
Abdulrahman Alsoghayer

2
@PankajParkarは、「変更検出エンジンは、監視可能なオブジェクトで変更が発生したことをどのようにして知るのか」について-以前のコメント応答を削除しました。Angularはサルパッチを適用しないsubscribe()ため、観測可能な変化を検出できないことを最近知りました。通常、発生する非同期イベントがいくつかあり(サンプルコードでは、ボタンクリックイベントです)、関連するコールバックがオブザーバブルでnext()を呼び出します。ただし、変更の検出は、監視可能な変更のためではなく、非同期イベントのために実行されます。ギュンターのコメントもご覧ください:stackoverflow.com/a/36846501/215945
Mark Rajcok

9
値が実際に生成されるまで待機する場合は、を使用できますReplaySubject(1)。AにBehaviorSubjectは、すぐに提供される初期値が必要です。ReplaySubject(1)常に最新の値を提供しますが、サービスはそれの最初の値を返す前に、いくつかの非同期操作を行うことができますので、初期値は必要ありませんが、それでも最後の値を持つ後続の呼び出しにすぐに発射します。1つの値だけに関心がある場合first()は、サブスクリプションで使用でき、最後にサブスクリプションを解除する必要はありません。
Jason Goemaat

33

速報: EventEmitterではなくObservableを使用する別の回答を追加しました。これよりもお答えすることをお勧めします。そして実際には、サービスで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に直接navchangeEventEmitterプロパティをサブスクライブさせることです。

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

私はこのソリューションをZouabi氏の回答よりも好んでいますが、正直なところ、このソリューションのファンではありません。私は、破棄の
サブスクリプションを

私は実際にこれについて考え、この解決策を採用することにしました。少しすっきりとした解決策が欲しいのですが、それが可能であるかどうかはわかりません(または、もっとエレガントなものを思いつくことができないかもしれません)。
the_critic

実際、2番目の箇条書きの問題は、関数への参照が代わりに渡されていることです。修正に:this.subscription = this.navService.subscribe(() => this.selectedNavItem());との購読:return this.navchange.subscribe(callback);
アンドレWerlang

1

よりリアクティブ指向のプログラミングスタイルを採用したい場合は、「すべてがストリームである」という概念が明確になるため、Observableを使用してこれらのストリームをできるだけ頻繁に処理します。


1

次のいずれかを使用できます。

  1. 行動主体:

BehaviorSubjectはサブジェクトの一種であり、サブジェクトはオブザーバブルとして機能できる特別なタイプのオブザーバブルであり、オブザーバーは他のオブザーバブルと同様にメッセージをサブスクライブでき、サブスクリプション時に、ソースオブザーバブルによって発行されたサブジェクトの最後の値を返します。

利点:コンポーネント間でデータを渡すために必要な親子関係などの関係はありません。

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

ナビゲーションコンポーネント

@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

ObservingComponentのテンプレートでNavigationコンポーネントを使用する必要があります(Navigationコンポーネントにセレクターを追加することを忘れないでください。exのナビゲーションコンポーネント)

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

そして、ObservingComponentにonNavGhange()を実装します。

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

最後に.. @Componenntの events属性は必要ありません

events : ['navchange'], 

これは、基になるコンポーネントのイベントのみをフックします。それは私がやろうとしていることではありません。(^ navchange)(キャレットはイベントのバブリング用です)のようなことを言ったかもしれませんがnav-item、他の人が観察できるイベントを発行したいだけです。
the_critic 2015

navchange.toRx()。subscribe()..を使用できますが、ObservingComponent
Mourad Zouabi

0

上記のようにBehaviourSubjectを使用するか、もう1つの方法があります。

次のように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);
 }

 }

-2

Reactivexもどちらのサービスも使用せずに、このケースの別の解決策を見つけました。私は実際にはrxjx APIが大好きですが、非同期関数や複雑な関数を解決するときに最適だと思います。そのようにそれを使用して、それは私にはかなり超えました。

あなたが探しているのは放送用だと思います。それだけ。そして私はこの解決策を見つけました:

<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を使用するかどうか、そうでなければイベントエミッターを使用するサービスを使用するかどうかです。
Nicholas Marcaccini Augusto 2017

subscribeは、angularが観察可能であるという事実を削除しない限り有効です。.subscribe()はベストアンサーで使用されますが、その特定のオブジェクトでは使用されません。
Porschiey 2017
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.