Globale Ereignisse in Angular


224

Gibt es kein Äquivalent zu $scope.emit()oder $scope.broadcast()in Angular?

Ich kenne die EventEmitterFunktionalität, aber soweit ich weiß , wird nur ein Ereignis an das übergeordnete HTML-Element ausgegeben.

Was ist, wenn ich zwischen fx kommunizieren muss? Geschwister oder zwischen einer Komponente in der Wurzel des DOM und einem Element, das mehrere Ebenen tief verschachtelt ist?


2
Ich hatte eine ähnliche Frage im Zusammenhang mit der Erstellung einer Dialogkomponente, auf die von jedem Punkt in dom aus zugegriffen werden kann: stackoverflow.com/questions/34572539/… Grundsätzlich besteht eine Lösung darin, einen Ereignisemitter in einen Dienst zu stellen
brando

1
Hier ist meine Implementierung eines solchen Dienstes mit RXJS, mit dem der n-te letzte Wert beim Abonnement abgerufen werden kann. stackoverflow.com/questions/46027693/…
Codewarrior

Antworten:


385

Es gibt kein Äquivalent zu $scope.emit()oder $scope.broadcast()von AngularJS. EventEmitter innerhalb einer Komponente kommt nahe, aber wie Sie bereits erwähnt haben, gibt es nur ein Ereignis an die unmittelbare übergeordnete Komponente aus.

In Angular gibt es andere Alternativen, die ich unten erklären möchte.

Mit @Input () -Bindungen kann das Anwendungsmodell in einem gerichteten Objektdiagramm (Wurzel zu Blättern) verbunden werden. Das Standardverhalten der Änderungsdetektorstrategie einer Komponente besteht darin, alle Änderungen an einem Anwendungsmodell für alle Bindungen einer verbundenen Komponente weiterzugeben.

Nebenbei: Es gibt zwei Arten von Modellen: Ansichtsmodelle und Anwendungsmodelle. Ein Anwendungsmodell ist über @ Input () -Bindungen verbunden. Ein Ansichtsmodell ist nur eine Komponenteneigenschaft (nicht mit @Input () dekoriert), die in der Vorlage der Komponente gebunden ist.

So beantworten Sie Ihre Fragen:

Was ist, wenn ich zwischen Geschwisterkomponenten kommunizieren muss?

  1. Gemeinsames Anwendungsmodell : Geschwister können über ein gemeinsames Anwendungsmodell kommunizieren (genau wie Winkel 1). Wenn beispielsweise ein Geschwister eine Änderung an einem Modell vornimmt, wird das andere Geschwister, das Bindungen an dasselbe Modell aufweist, automatisch aktualisiert.

  2. Komponentenereignisse : Untergeordnete Komponenten können mithilfe von @ Output () - Bindungen ein Ereignis an die übergeordnete Komponente senden. Die übergeordnete Komponente kann das Ereignis verarbeiten und das Anwendungsmodell oder das eigene Ansichtsmodell bearbeiten. Änderungen am Anwendungsmodell werden automatisch an alle Komponenten weitergegeben, die direkt oder indirekt an dasselbe Modell gebunden sind.

  3. Serviceereignisse : Komponenten können Serviceereignisse abonnieren. Beispielsweise können zwei Geschwisterkomponenten dasselbe Serviceereignis abonnieren und darauf reagieren, indem sie ihre jeweiligen Modelle ändern. Mehr dazu weiter unten.

Wie kann ich zwischen einer Root-Komponente und einer Komponente kommunizieren, die mehrere Ebenen tief verschachtelt ist?

  1. Gemeinsames Anwendungsmodell : Das Anwendungsmodell kann über @Input () -Bindungen von der Root-Komponente an tief verschachtelte Unterkomponenten übergeben werden. Änderungen an einem Modell von einer Komponente werden automatisch an alle Komponenten weitergegeben, die dasselbe Modell verwenden.
  2. Service Veranstaltungen : Sie können auch die EventEmitter zu einem gemeinsamen Dienst bewegen, die jede Komponente ermöglicht es, den Dienst zu injizieren und zu dem Ereignis abonnieren. Auf diese Weise kann eine Root-Komponente eine Servicemethode aufrufen (die normalerweise das Modell mutiert), die wiederum ein Ereignis ausgibt. Mehrere Ebenen weiter unten kann eine Enkelkomponente, die den Dienst ebenfalls injiziert und dasselbe Ereignis abonniert hat, damit umgehen. Jeder Ereignishandler, der ein freigegebenes Anwendungsmodell ändert, wird automatisch an alle davon abhängigen Komponenten weitergegeben. Dies ist wahrscheinlich das nächste Äquivalent zu $scope.broadcast()Angular 1. Im nächsten Abschnitt wird diese Idee ausführlicher beschrieben.

Beispiel eines beobachtbaren Dienstes, der Dienstereignisse verwendet, um Änderungen zu verbreiten

Hier ist ein Beispiel für einen beobachtbaren Dienst, der Dienstereignisse verwendet, um Änderungen weiterzugeben. Wenn ein TodoItem hinzugefügt wird, gibt der Dienst ein Ereignis aus, das seine Komponententeilnehmer benachrichtigt.

export class TodoItem {
    constructor(public name: string, public done: boolean) {
    }
}
export class TodoService {
    public itemAdded$: EventEmitter<TodoItem>;
    private todoList: TodoItem[] = [];

    constructor() {
        this.itemAdded$ = new EventEmitter();
    }

    public list(): TodoItem[] {
        return this.todoList;
    }

    public add(item: TodoItem): void {
        this.todoList.push(item);
        this.itemAdded$.emit(item);
    }
}

So würde eine Root-Komponente das Ereignis abonnieren:

export class RootComponent {
    private addedItem: TodoItem;
    constructor(todoService: TodoService) {
        todoService.itemAdded$.subscribe(item => this.onItemAdded(item));
    }

    private onItemAdded(item: TodoItem): void {
        // do something with added item
        this.addedItem = item;
    }
}

Eine untergeordnete Komponente, die mehrere Ebenen tief verschachtelt ist, würde das Ereignis auf dieselbe Weise abonnieren:

export class GrandChildComponent {
    private addedItem: TodoItem;
    constructor(todoService: TodoService) {
        todoService.itemAdded$.subscribe(item => this.onItemAdded(item));
    }

    private onItemAdded(item: TodoItem): void {
        // do something with added item
        this.addedItem = item;
    }
}

Hier ist die Komponente, die den Dienst aufruft, um das Ereignis auszulösen (sie kann sich an einer beliebigen Stelle im Komponentenbaum befinden):

@Component({
    selector: 'todo-list',
    template: `
         <ul>
            <li *ngFor="#item of model"> {{ item.name }}
            </li>
         </ul>
        <br />
        Add Item <input type="text" #txt /> <button (click)="add(txt.value); txt.value='';">Add</button>
    `
})
export class TriggeringComponent{
    private model: TodoItem[];

    constructor(private todoService: TodoService) {
        this.model = todoService.list();
    }

    add(value: string) {
        this.todoService.add(new TodoItem(value, false));
    }
}

Referenz: Änderungserkennung im Winkel


27
Ich habe das nachfolgende $ in einigen Posts für einen Observable oder EventEmitter gesehen - z itemAdded$. Ist das eine RxJS-Konvention oder so? Woher kommt das?
Mark Rajcok

1
Gute Antwort. Sie haben angegeben: "Änderungen am App-Modell werden automatisch an alle Komponenten weitergegeben, die direkt oder indirekt an dasselbe Modell gebunden sind." Ich habe die Vermutung, dass es so nicht ganz funktioniert (aber ich bin mir nicht sicher). Savkins anderer Blog-Beitrag enthält ein Beispiel für eine Komponente, die die streetEigenschaft des App-Modells ändert. Da Angular 2 jedoch die Änderungserkennung durch Identität / Referenz implementiert, werden keine Änderungen weitergegeben ( onChangeswird nicht aufgerufen), da sich die Referenz des App-Modells nicht geändert hat ( Fortsetzung ...)
Mark Rajcok

9
Möglicherweise möchten Sie Ihre Antwort aktualisieren, um ein Observable anstelle eines EventEmitter im Dienst zu verwenden. Siehe stackoverflow.com/a/35568924/215945 und stackoverflow.com/questions/36076700
Mark Rajcok

2
Ja, das Suffix $ ist eine von Cycle.js populäre RxJS-Konvention. cycle.js.org/…
jody tate

4
Sie sollten einen Eventemitter nicht manuell abonnieren. In der endgültigen Version ist dies möglicherweise nicht zu beobachten! Siehe dies: bennadel.com/blog/…
NetProvoke

49

Der folgende Code als Beispiel für einen Ersatz für $ scope.emit () oder $ scope.broadcast () in Angular 2 unter Verwendung eines gemeinsam genutzten Dienstes zur Behandlung von Ereignissen.

import {Injectable} from 'angular2/core';
import * as Rx from 'rxjs/Rx';

@Injectable()
export class EventsService {
    constructor() {
        this.listeners = {};
        this.eventsSubject = new Rx.Subject();

        this.events = Rx.Observable.from(this.eventsSubject);

        this.events.subscribe(
            ({name, args}) => {
                if (this.listeners[name]) {
                    for (let listener of this.listeners[name]) {
                        listener(...args);
                    }
                }
            });
    }

    on(name, listener) {
        if (!this.listeners[name]) {
            this.listeners[name] = [];
        }

        this.listeners[name].push(listener);
    }

    off(name, listener) {
        this.listeners[name] = this.listeners[name].filter(x => x != listener);
    }

    broadcast(name, ...args) {
        this.eventsSubject.next({
            name,
            args
        });
    }
}

Anwendungsbeispiel:

Übertragung:

function handleHttpError(error) {
    this.eventsService.broadcast('http-error', error);
    return ( Rx.Observable.throw(error) );
}

Hörer:

import {Inject, Injectable} from "angular2/core";
import {EventsService}      from './events.service';

@Injectable()
export class HttpErrorHandler {
    constructor(eventsService) {
        this.eventsService = eventsService;
    }

    static get parameters() {
        return [new Inject(EventsService)];
    }

    init() {
        this.eventsService.on('http-error', function(error) {
            console.group("HttpErrorHandler");
            console.log(error.status, "status code detected.");
            console.dir(error);
            console.groupEnd();
        });
    }
}

Es kann mehrere Argumente unterstützen:

this.eventsService.broadcast('something', "Am I a?", "Should be b", "C?");

this.eventsService.on('something', function (a, b, c) {
   console.log(a, b, c);
});

Was macht das? statische get-Parameter () {return [new Inject (EventsService)]; }
Beanwah

In diesem Beispiel verwende ich Ionic 2 Framework. Die statische Parametermethode wird aufgerufen, wenn die Konstruktormethode aufgerufen wird, und wird verwendet, um die Abhängigkeiten an den Konstruktor zu liefern. Erklärung hier stackoverflow.com/questions/35919593/…
jim.taylor.1974

1
Schön gemacht. Einfach und bietet ein leicht anpassbares Benachrichtigungssystem für die gesamte App, nicht nur ein einmaliges.
Mike M

Ich habe gerade einen ähnlichen Dienst mit Wildcards-Unterstützung erstellt. Ich hoffe es hilft. github.com/govorov/ng-radio
Stanislav E. Govorov

2
Genial, habe es benutzt, aber eine Off-Funktion hinzugefügt, wenn mehr interessiert ist: off(name, listener) { this.listeners[name] = this.listeners[name].filter(x => x != listener); }
LVDM

16

Ich verwende einen Nachrichtendienst, der ein rxjs Subject(TypeScript) umschließt.

Plunker-Beispiel: Nachrichtendienst

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { Subscription } from 'rxjs/Subscription';
import 'rxjs/add/operator/filter'
import 'rxjs/add/operator/map'

interface Message {
  type: string;
  payload: any;
}

type MessageCallback = (payload: any) => void;

@Injectable()
export class MessageService {
  private handler = new Subject<Message>();

  broadcast(type: string, payload: any) {
    this.handler.next({ type, payload });
  }

  subscribe(type: string, callback: MessageCallback): Subscription {
    return this.handler
      .filter(message => message.type === type)
      .map(message => message.payload)
      .subscribe(callback);
  }
}

Komponenten können Ereignisse abonnieren und senden (Absender):

import { Component, OnDestroy } from '@angular/core'
import { MessageService } from './message.service'
import { Subscription } from 'rxjs/Subscription'

@Component({
  selector: 'sender',
  template: ...
})
export class SenderComponent implements OnDestroy {
  private subscription: Subscription;
  private messages = [];
  private messageNum = 0;
  private name = 'sender'

  constructor(private messageService: MessageService) {
    this.subscription = messageService.subscribe(this.name, (payload) => {
      this.messages.push(payload);
    });
  }

  send() {
    let payload = {
      text: `Message ${++this.messageNum}`,
      respondEvent: this.name
    }
    this.messageService.broadcast('receiver', payload);
  }

  clear() {
    this.messages = [];
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

(Empfänger)

import { Component, OnDestroy } from '@angular/core'
import { MessageService } from './message.service'
import { Subscription } from 'rxjs/Subscription'

@Component({
  selector: 'receiver',
  template: ...
})
export class ReceiverComponent implements OnDestroy {
  private subscription: Subscription;
  private messages = [];

  constructor(private messageService: MessageService) {
    this.subscription = messageService.subscribe('receiver', (payload) => {
      this.messages.push(payload);
    });
  }

  send(message: {text: string, respondEvent: string}) {
    this.messageService.broadcast(message.respondEvent, message.text);
  }

  clear() {
    this.messages = [];
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

Die subscribeMethode MessageServicegibt ein rxjs- SubscriptionObjekt zurück, das wie folgt abgemeldet werden kann:

import { Subscription } from 'rxjs/Subscription';
...
export class SomeListener {
  subscription: Subscription;

  constructor(private messageService: MessageService) {
    this.subscription = messageService.subscribe('someMessage', (payload) => {
      console.log(payload);
      this.subscription.unsubscribe();
    });
  }
}

Siehe auch diese Antwort: https://stackoverflow.com/a/36782616/1861779

Plunker-Beispiel: Nachrichtendienst


2
sehr wertvoll. Danke für die Antwort. Ich habe gerade herausgefunden, dass Sie auf diese Weise nicht mit zwei Komponenten in zwei verschiedenen Modulen kommunizieren können. Um das Ziel zu erreichen, musste ich MessageService in der Ebene app.module registrieren, indem ich dort Anbieter hinzufügte. Auf jeden Fall ist das ein wirklich cooler Weg.
Rukshan Dangalla

das ist alles leider veraltet. besonders der Plunker, der keine Ressourcen erfolgreich lädt. Sie sind alle 500 Fehlercodes.
Tatsu

Ich bekommeProperty 'filter' does not exist on type 'Subject<EventMessage>'.
Drew

@ Draw, auf neueren Versionen von RxJS verwenden this.handler.pipe(filter(...)). Siehe Vermietbare Betreiber .
t.888

1
@ t.888 danke, ich habe es herausgefunden. Die aktualisierte Abonnementfunktion sieht aus wiereturn this.handler.pipe( filter(message => message.type === type), map(message => message.payload) ).subscribe(callback);
Drew

12

Verwenden Sie EventEmitter nicht für Ihre Servicekommunikation.

Sie sollten einen der Observable-Typen verwenden. Ich persönlich mag BehaviorSubject.

Einfaches Beispiel:

Sie können den Anfangszustand übergeben, hier übergebe ich null

let subject = new BehaviorSubject (null);

Wenn Sie den Betreff aktualisieren möchten

subject.next (myObject)

Beobachten Sie alle Dienste oder Komponenten und handeln Sie, wenn neue Updates eingehen.

subject.subscribe (this.YOURMETHOD);

Hier finden Sie weitere Informationen. .


1
Können Sie die Gründe für diese Entwurfsentscheidung erläutern?
mtraut

@mtraut dieser Link hat auch eine umfassende Erklärung.
Danial Kalbasi

Für eine detailliertere Erklärung zur Verwendung von BehaviourSubject lesen Sie bitte diesen Artikel blog.cloudboost.io/…
rafalkasa

Genau das, was ich brauchte. Schön und einfach :)
Niedrig


2

Meine Lieblingsmethode ist die Verwendung von Verhaltensthemen oder Ereignisemittern (fast gleich) in meinem Dienst, um alle meine Unterkomponenten zu steuern.

Führen Sie mithilfe von Angular Cli ng gs aus, um einen neuen Dienst zu erstellen, und verwenden Sie dann ein BehaviorSubject oder EventEmitter

export Class myService {
#all the stuff that must exist

myString: string[] = [];
contactChange : BehaviorSubject<string[]> = new BehaviorSubject(this.myString);

   getContacts(newContacts) {
     // get your data from a webservices & when you done simply next the value 
    this.contactChange.next(newContacts);
   }
}

Wenn Sie dies tun, wird jede Komponente, die Ihren Dienst als Anbieter verwendet, über die Änderung informiert. Abonnieren Sie das Ergebnis einfach wie bei eventEmitter;)

export Class myComp {
#all the stuff that exists like @Component + constructor using (private myService: myService)

this.myService.contactChange.subscribe((contacts) => {
     this.contactList += contacts; //run everytime next is called
  }
}

1

Ich habe hier ein Pub-Sub-Beispiel erstellt:

http://www.syntaxsuccess.com/viewarticle/pub-sub-in-angular-2.0

Die Idee ist, RxJs Subjects zu verwenden, um einen Observer und und Observables als generische Lösung für das Ausgeben und Abonnieren von benutzerdefinierten Ereignissen zu verkabeln. In meinem Beispiel verwende ich ein Kundenobjekt für Demozwecke

this.pubSubService.Stream.emit(customer);

this.pubSubService.Stream.subscribe(customer => this.processCustomer(customer));

Hier ist auch eine Live-Demo: http://www.syntaxsuccess.com/angular-2-samples/#/demo/pub-sub


1

Dies ist meine Version:

export interface IEventListenr extends OnDestroy{
    ngOnDestroy(): void
}

@Injectable()
export class EventManagerService {


    private listeners = {};
    private subject = new EventEmitter();
    private eventObserver = this.subject.asObservable();


    constructor() {

        this.eventObserver.subscribe(({name,args})=>{



             if(this.listeners[name])
             {
                 for(let listener of this.listeners[name])
                 {
                     listener.callback(args);
                 }
             }
        })

    }

    public registerEvent(eventName:string,eventListener:IEventListenr,callback:any)
    {

        if(!this.listeners[eventName])
             this.listeners[eventName] = [];

         let eventExist = false;
         for(let listener of this.listeners[eventName])
         {

             if(listener.eventListener.constructor.name==eventListener.constructor.name)
             {
                 eventExist = true;
                 break;
             }
         }

        if(!eventExist)
        {
             this.listeners[eventName].push({eventListener,callback});
        }
    }

    public unregisterEvent(eventName:string,eventListener:IEventListenr)
    {

        if(this.listeners[eventName])
        {
            for(let i = 0; i<this.listeners[eventName].length;i++)
            {

                if(this.listeners[eventName][i].eventListener.constructor.name==eventListener.constructor.name)
                {
                    this.listeners[eventName].splice(i, 1);
                    break;
                }
            }
        }


    }


    emit(name:string,...args:any[])
    {
        this.subject.next({name,args});
    }
}

verwenden:

export class <YOURCOMPONENT> implements IEventListener{

  constructor(private eventManager: EventManagerService) {


    this.eventManager.registerEvent('EVENT_NAME',this,(args:any)=>{
       ....
    })


  }

  ngOnDestroy(): void {
    this.eventManager.unregisterEvent('closeModal',this)
  }

}}

emittieren:

 this.eventManager.emit("EVENT_NAME");

0

Wir haben eine beobachtbare ngModelChange-Direktive implementiert, die alle Modelländerungen über einen Ereignisemitter sendet, den Sie in Ihrer eigenen Komponente instanziieren. Sie müssen lediglich Ihren Event-Emitter an die Direktive binden.

Siehe: https://github.com/atomicbits/angular2-modelchangeobservable

Binden Sie in HTML Ihren Ereignisemitter (in diesem Beispiel countryChanged):

<input [(ngModel)]="country.name"
       [modelChangeObservable]="countryChanged" 
       placeholder="Country"
       name="country" id="country"></input>

Führen Sie in Ihrer Typoskriptkomponente einige asynchrone Vorgänge für den EventEmitter aus:

import ...
import {ModelChangeObservable} from './model-change-observable.directive'


@Component({
    selector: 'my-component',
    directives: [ModelChangeObservable],
    providers: [],
    templateUrl: 'my-component.html'
})

export class MyComponent {

    @Input()
    country: Country

    selectedCountries:Country[]
    countries:Country[] = <Country[]>[]
    countryChanged:EventEmitter<string> = new EventEmitter<string>()


    constructor() {

        this.countryChanged
            .filter((text:string) => text.length > 2)
            .debounceTime(300)
            .subscribe((countryName:string) => {
                let query = new RegExp(countryName, 'ig')
                this.selectedCountries = this.countries.filter((country:Country) => {
                    return query.test(country.name)
                })
            })
    }
}

0

Serviceereignisse: Komponenten können Serviceereignisse abonnieren. Beispielsweise können zwei Geschwisterkomponenten dasselbe Serviceereignis abonnieren und darauf reagieren, indem sie ihre jeweiligen Modelle ändern. Mehr dazu weiter unten.

Stellen Sie jedoch sicher, dass Sie sich bei der Zerstörung der übergeordneten Komponente abmelden.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.