Delegasi: EventEmitter atau Observable in Angular

244

Saya mencoba menerapkan sesuatu seperti pola delegasi di Angular. Ketika pengguna mengklik a nav-item, saya ingin memanggil fungsi yang kemudian memancarkan suatu peristiwa yang pada gilirannya harus ditangani oleh beberapa komponen lain yang mendengarkan acara tersebut.

Berikut ini skenarionya: Saya punya Navigationkomponen:

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

}

Berikut adalah komponen yang mengamati:

export class ObservingComponent {

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

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

}

Pertanyaan kuncinya adalah, bagaimana cara membuat komponen mengamati mengamati peristiwa yang dimaksud?

the_critic
sumber

Jawaban:

450

Pembaruan 2016-06-27: alih-alih menggunakan Observables, gunakan keduanya

  • sebuah BehaviorSubject, seperti yang direkomendasikan oleh @Abdulrahman dalam komentar, atau
  • sebuah ReplaySubject, seperti yang direkomendasikan oleh @Jason Goemaat dalam komentar

Sebuah Subyek adalah baik diamati (sehingga kita bisa subscribe()untuk itu) dan Pengamat (sehingga kita bisa menyebutnya next()di atasnya untuk memancarkan nilai baru). Kami memanfaatkan fitur ini. Subjek memungkinkan nilai menjadi multicast bagi banyak Pengamat. Kami tidak mengeksploitasi fitur ini (kami hanya memiliki satu Pengamat).

BehaviorSubject adalah varian dari Subjek. Ini memiliki gagasan "nilai saat ini". Kami mengeksploitasi ini: setiap kali kami membuat ObservingComponent, ia mendapatkan nilai item navigasi saat ini dari BehaviorSubject secara otomatis.

Kode di bawah ini dan plunker menggunakan BehaviorSubject.

ReplaySubject adalah varian lain dari Subjek. Jika Anda ingin menunggu hingga nilai benar-benar dihasilkan, gunakan ReplaySubject(1). Sedangkan BehaviorSubject membutuhkan nilai awal (yang akan segera disediakan), ReplaySubject tidak. ReplaySubject akan selalu memberikan nilai terbaru, tetapi karena tidak memiliki nilai awal yang diperlukan, layanan dapat melakukan beberapa operasi async sebelum mengembalikan nilai pertama. Itu akan tetap aktif segera pada panggilan berikutnya dengan nilai terbaru. Jika Anda hanya ingin satu nilai, gunakan first()pada berlangganan. Anda tidak harus berhenti berlangganan jika menggunakan 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


Jawaban asli yang menggunakan Observable: (memerlukan lebih banyak kode dan logika daripada menggunakan BehaviorSubject, jadi saya tidak merekomendasikannya, tetapi mungkin instruktif)

Jadi, inilah implementasi yang menggunakan Observable alih-alih EventEmitter . Tidak seperti implementasi EventEmitter saya, implementasi ini juga menyimpan yang saat ini dipilih navItemdalam layanan, sehingga ketika komponen mengamati dibuat, itu dapat mengambil nilai saat ini melalui panggilan API navItem(), dan kemudian diberitahu tentang perubahan melalui 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


Lihat juga contoh Cookbook Interaksi Komponen , yang menggunakan Subjectselain yang bisa diamati. Meskipun contohnya adalah "komunikasi orang tua dan anak-anak," teknik yang sama berlaku untuk komponen yang tidak terkait.

Mark Rajcok
sumber
3
Disebutkan berulang kali dalam komentar untuk masalah Angular2 yang tidak disarankan untuk digunakan di EventEmittermana pun kecuali untuk output. Mereka saat ini sedang menulis ulang tutorial (AFAIR komunikasi server) untuk tidak mendorong praktik ini.
Günter Zöchbauer
2
Apakah ada cara menginisialisasi layanan dan mematikan acara pertama dari dalam komponen Navigasi dalam kode contoh di atas? Masalahnya adalah bahwa _observerobjek layanan setidaknya tidak diinisialisasi pada saat ngOnInit()komponen Navigasi dipanggil.
ComFreek
4
Bolehkah saya menyarankan menggunakan BehaviorSubject daripada diobservasi. Lebih dekat dengan EventEmitterkarena "panas" yang berarti sudah "dibagikan", dirancang untuk menyimpan nilai saat ini dan akhirnya menerapkan Observable dan Observer yang akan menyelamatkan Anda setidaknya lima baris kode dan dua properti
Abdulrahman Alsoghayer
2
@PankajParkar, mengenai "bagaimana mesin deteksi perubahan tahu bahwa perubahan telah terjadi pada objek yang dapat diamati" - Saya menghapus tanggapan komentar saya sebelumnya. Saya belajar baru-baru ini bahwa Angular tidak monyet-patch subscribe(), sehingga tidak dapat mendeteksi kapan perubahan yang diamati. Biasanya, ada beberapa peristiwa async yang menyala (dalam kode sampel saya, ini adalah peristiwa klik tombol), dan panggilan balik yang terkait akan memanggil next () pada yang bisa diamati. Tetapi deteksi perubahan berjalan karena peristiwa async, bukan karena perubahan yang dapat diamati. Lihat juga komentar Günter: stackoverflow.com/a/36846501/215945
Mark Rajcok
9
Jika Anda ingin menunggu hingga nilai benar-benar dihasilkan, Anda dapat menggunakannya ReplaySubject(1). A BehaviorSubjectmembutuhkan nilai awal yang akan segera disediakan. Itu ReplaySubject(1)akan selalu memberikan nilai terbaru, tetapi tidak memiliki nilai awal yang diperlukan sehingga layanan dapat melakukan beberapa operasi async sebelum mengembalikan nilai pertama itu, tetapi masih langsung aktif pada panggilan berikutnya dengan nilai terakhir. Jika Anda hanya tertarik pada satu nilai yang dapat Anda gunakan first()pada langganan dan tidak harus berhenti berlangganan pada akhirnya karena itu akan selesai.
Jason Goemaat
33

Berita terkini: Saya telah menambahkan jawaban lain yang menggunakan Observable daripada EventEmitter. Saya merekomendasikan jawaban itu untuk yang satu ini. Dan sebenarnya, menggunakan EventEmitter dalam layanan adalah praktik yang buruk .


Jawaban asli: (jangan lakukan ini)

Masukkan EventEmitter ke dalam layanan, yang memungkinkan PengamatComponent untuk langsung berlangganan (dan berhenti berlangganan) ke acara :

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

Jika Anda mencoba Plunker, ada beberapa hal yang saya tidak suka tentang pendekatan ini:

  • MengamatiKomponen perlu berhenti berlangganan ketika dihancurkan
  • kita harus meneruskan komponen subscribe()agar tepat thisdiatur ketika panggilan balik dipanggil

Pembaruan: Alternatif yang memecahkan peluru ke-2 adalah memiliki ObservingComponent secara langsung berlangganan navchangeproperti EventEmitter:

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

Jika kami berlangganan secara langsung, maka kami tidak akan memerlukan subscribe()metode di NavService.

Untuk membuat NavService sedikit lebih dienkapsulasi, Anda bisa menambahkan getNavChangeEmitter()metode dan menggunakannya:

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

constructor(private navService:NavService) {  // in ObservingComponent
   this.subscription = this.navService.getNavChangeEmitter().subscribe(data =>
     this.selectedNavItem(data));
}
Mark Rajcok
sumber
Saya lebih suka solusi ini daripada jawaban yang diberikan oleh Mr Zouabi, tapi saya juga bukan penggemar solusi ini, jujur ​​saja. Saya tidak peduli berhenti berlangganan kehancuran, tapi saya benci kenyataan bahwa kita harus melewati komponen untuk berlangganan ke acara ...
the_critic
Saya benar-benar memikirkan hal ini dan memutuskan untuk pergi dengan solusi ini. Saya ingin memiliki solusi yang sedikit lebih bersih, tetapi saya tidak yakin itu mungkin (atau saya mungkin tidak dapat menemukan sesuatu yang lebih elegan yang harus saya katakan).
the_critic
sebenarnya masalah peluru ke-2 adalah bahwa referensi ke Fungsi sedang disahkan. untuk memperbaiki: this.subscription = this.navService.subscribe(() => this.selectedNavItem());dan berlangganan:return this.navchange.subscribe(callback);
André Werlang
1

Jika seseorang ingin mengikuti gaya pemrograman yang lebih Reaktif, maka pasti konsep "Semuanya adalah aliran" muncul dan karenanya, gunakan Observable untuk menangani aliran ini sesering mungkin.

Krishna Ganeriwal
sumber
1

Anda dapat menggunakan:

  1. Subjek Perilaku:

BehaviorSubject adalah jenis subjek, subjek adalah jenis khusus yang dapat diamati yang dapat bertindak sebagai diamati dan pengamat Anda dapat berlangganan pesan seperti diamati lainnya dan setelah berlangganan, itu mengembalikan nilai terakhir dari subjek yang dipancarkan oleh sumber yang diamati:

Keuntungan: Tidak Ada Hubungan seperti hubungan orangtua-anak yang diperlukan untuk meneruskan data antar komponen.

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

KOMPONEN NAVIGASI

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

KOMPONEN PENGAMATAN

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

Pendekatan kedua adalah Event Delegation in upward direction child -> parent

  1. Menggunakan induk @Input dan @Output dekorator yang meneruskan data ke komponen anak dan komponen induk pemberitahuan anak

misalnya Dijawab diberikan oleh @Ashish Sharma.

khizer
sumber
0

Anda perlu menggunakan komponen Navigasi dalam templat ObservingComponent (jangan lupa menambahkan pemilih ke komponen Navigasi .. komponen navigasi untuk ex)

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

Dan mengimplementasikan onNavGhange () di ObservingComponent

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

Hal terakhir .. Anda tidak perlu atribut acara di @Componennt

events : ['navchange'], 
Mourad Zouabi
sumber
Ini hanya menghubungkan acara untuk komponen yang mendasarinya. Bukan itu yang saya coba lakukan. Saya bisa saja mengatakan sesuatu seperti (^ navchange) (tanda sisipan untuk acara menggelegak) pada nav-itemtapi saya hanya ingin memancarkan suatu peristiwa yang orang lain dapat amati.
the_critic
Anda dapat menggunakan navchange.toRx (). berlangganan () .. tetapi Anda harus memiliki referensi tentang navchange di ObservingComponent
Mourad Zouabi
0

Anda dapat menggunakan BehaviourSubject seperti dijelaskan di atas atau ada satu cara lagi:

Anda dapat menangani EventEmitter seperti ini: pertama-tama tambahkan pemilih

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

}

Sekarang Anda dapat menangani acara ini seperti izinkan kami mengira observer.component.html adalah tampilan komponen Observer

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

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

 }
Ashish Sharma
sumber
-2

Saya menemukan solusi lain untuk kasus ini tanpa menggunakan Reactivex maupun layanan. Saya benar-benar menyukai API rxjx namun saya pikir ini akan lebih baik ketika menyelesaikan fungsi async dan / atau kompleks. Menggunakannya dengan cara itu, itu sangat melebihi saya.

Apa yang saya pikir Anda cari adalah siaran. Hanya itu saja. Dan saya menemukan solusi ini:

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

Ini adalah contoh yang berfungsi penuh: https://plnkr.co/edit/xGVuFBOpk2GP0pRBImsE

Nicholas Marcaccini Augusto
sumber
1
Lihatlah jawaban terbaik, ia menggunakan metode berlangganan juga .. Sebenarnya hari ini saya akan merekomendasikan menggunakan Redux atau kontrol negara lain untuk menyelesaikan masalah komunikasi ini antara komponen. Ini tak terhingga jauh lebih baik daripada solusi lain meskipun itu menambah kompleksitas ekstra. Baik menggunakan komponen Angular 2 event handler sintax atau secara eksplisit menggunakan metode berlangganan konsepnya tetap sama. Pikiran terakhir saya adalah jika Anda menginginkan solusi pasti untuk masalah itu gunakan Redux, jika tidak gunakan layanan dengan event emitor.
Nicholas Marcaccini Augusto
berlangganan valid selama sudut tidak menghilangkan fakta bahwa itu dapat diamati. .subscribe () digunakan dalam jawaban terbaik, tetapi tidak pada objek tertentu.
Porschiey