Делегація: EventEmitter або спостерігається у кутовій


244

Я намагаюся реалізувати щось подібне до моделі делегування в Angular. Коли користувач натискає на a 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!');
  }

}

Ключове питання полягає в тому, як змусити компонент спостереження спостерігати за подією, про яку йдеться?


Відповідно до документів:> Клас EventEmitter розширює спостерігається.
зал

Відповіді:


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


Оригінальна відповідь, що використовує спостережуване: (для цього потрібно більше коду та логіки, ніж використання 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додаткові дані для спостереження. Хоча приклад "спілкування батьків та дітей", однакова техніка застосовується і для непов'язаних компонентів.


3
У коментарях до питань Angular2 про це неодноразово згадувалося, що його не рекомендують використовувати в EventEmitterбудь-якому місці, крім виходів. Зараз вони переписують підручники (серверне спілкування AFAIR), щоб не заохочувати цю практику.
Günter Zöchbauer

2
Чи є спосіб ініціалізувати службу та відключити першу подію з компонента Навігація у наведеному вище прикладі коду? Проблема полягає в _observerтому, що об'єкт служби принаймні не ініціалізується під час ngOnInit()виклику компонента Навігації.
ComFreek

4
Можна запропонувати використовувати BehaviorSubject замість Observable. Це ближче до того, EventEmitterщо це "гаряче", це означає, що воно вже "загальнодоступне", воно створене для збереження поточного значення і, нарешті, воно реалізує як "Спостережуване", так і "Спостережувальне", що дозволить заощадити принаймні п'ять рядків коду та два властивості
Абдулрахман Олдгайер

2
@PankajParkar, щодо "як двигун виявлення змін знає, що зміни відбулися на спостережуваному об'єкті" - я видалив попередню відповідь на коментар. Нещодавно я дізнався, що Angular не мавпочки subscribe(), тому він не може виявити, коли спостерігається зміна. Зазвичай, якась подія асинхронізації запускається (у моєму прикладі коду це події натискання кнопки), і пов'язаний зворотний виклик викликає наступний () на спостережуваному. Але виявлення змін запускається через подію асинхронізації, а не через спостережувані зміни. Дивіться також коментарі Gunter: stackoverflow.com/a/36846501/215945
Марк Rajcok

9
Якщо ви хочете зачекати, поки значення фактично не буде створено, ви можете використовувати ReplaySubject(1). Потрібно BehaviorSubjectпочаткове значення, яке буде надано негайно. ReplaySubject(1)Завжди буде забезпечувати саме останнє значення, але не має початкове значення , необхідне , щоб обслуговування може зробити яку - то операцію асинхронної перед його поверненням в перше значення, але все - таки спрацьовує відразу при наступних викликах з останнім значенням. Якщо вас просто цікавить одне значення, ви можете використовувати first()підписку, і не потрібно скасовувати підписку в кінці, тому що це буде завершено.
Джейсон Гімаат

33

Актуальні новини: Я додав ще одну відповідь, яка використовує спостережуваний, а не 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));
}

Я вважаю за краще це рішення, ніж відповідь, яку надав містер Зуабі, але я не є прихильником цього рішення, якщо чесно. Мені не байдуже скасовувати підписку на знищення, але я ненавиджу те, що ми повинні передавати компонент, щоб підписатися на подію ...
the_critic

Я насправді задумався над цим і вирішив піти з цим рішенням. Я хотів би мати трохи більш чисте рішення, але я не впевнений, що це можливо (або я, мабуть, не в змозі придумати щось більш елегантне, що слід сказати).
the_critic

насправді проблема 2-ї кулі полягає в тому, що замість цього передається посилання на Функцію. виправити: this.subscription = this.navService.subscribe(() => this.selectedNavItem());і підписатися:return this.navchange.subscribe(callback);
Андре Верланг

1

Якщо ви хочете керуватися більш реактивно орієнтованим стилем програмування, то, безумовно, поняття "Все - це потік" входить в картину, отже, використовуйте "Оглядові", щоб якомога частіше працювати з цими потоками.


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-component (navchange)='onNavGhange($event)'></navigation-component>

І реалізуйте onNavGhange () в ObservingComponent

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

Останнє .. вам не потрібен атрибут події в @Componennt

events : ['navchange'], 

Це підключає лише подію для основного компонента. Це не те, що я намагаюся зробити. Я міг би просто сказати щось на кшталт (^ navchange) (карета - це барботаж подій), nav-itemале я просто хочу винести подію, яку можуть спостерігати інші.
the_critic

ви можете використовувати navchange.toRx (). підписатися () .. але вам потрібно буде мати посилання на 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)
}

}

Тепер ви можете поводитися з цією подією, як , припустимо, 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. Я насправді люблю 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 () використовується в кращій відповіді, але не на цьому конкретному об'єкті.
Порщій
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.