Globalne wydarzenia w Angular

224

Czy nie ma równoważne $scope.emit()lub $scope.broadcast()kątowej?

Znam EventEmitterfunkcjonalność, ale o ile rozumiem, po prostu wyśle ​​zdarzenie do nadrzędnego elementu HTML.

Co jeśli będę musiał się komunikować między fx. rodzeństwo lub między komponentem w katalogu głównym DOM a elementem zagnieżdżonym na kilku poziomach głęboko?

skovmand
źródło
2
Miałem podobne pytanie związane z tworzeniem komponentu okna dialogowego, do którego można uzyskać dostęp z dowolnego miejsca w domu: stackoverflow.com/questions/34572539/... Zasadniczo jednym rozwiązaniem jest umieszczenie emitera zdarzeń w usłudze
brando
1
Oto moja implementacja takiej usługi przy użyciu RXJS, która pozwala uzyskać n-te ostatnie wartości po subskrypcji. stackoverflow.com/questions/46027693/…
Codewarrior

Odpowiedzi:

385

Nie ma odpowiednika $scope.emit()ani $scope.broadcast()z AngularJS. EventEmitter wewnątrz komponentu jest blisko, ale jak wspomniałeś, będzie emitował zdarzenie tylko do bezpośredniego komponentu nadrzędnego.

W Angular istnieją inne alternatywy, które spróbuję wyjaśnić poniżej.

Powiązania @Input () pozwalają na podłączenie modelu aplikacji do grafu z ukierunkowanym obiektem (root do liści). Domyślnym zachowaniem strategii detektora zmian w komponencie jest propagowanie wszystkich zmian w modelu aplikacji dla wszystkich powiązań z dowolnego podłączonego komponentu.

Poza tym: Istnieją dwa typy modeli: Wyświetl modele i Modele aplikacji. Model aplikacji jest połączony za pomocą powiązań @Input (). Model widoku jest tylko właściwością komponentu (nie ozdobioną @Input ()), która jest związana w szablonie komponentu.

Aby odpowiedzieć na twoje pytania:

Co się stanie, jeśli będę musiał komunikować się między elementami rodzeństwa?

  1. Model współdzielonej aplikacji : Rodzeństwo może komunikować się za pośrednictwem modelu współdzielonej aplikacji (podobnie jak kątowy 1). Na przykład, gdy jedno rodzeństwo wprowadza zmiany w modelu, drugie rodzeństwo, które ma powiązania z tym samym modelem, jest automatycznie aktualizowane.

  2. Zdarzenia komponentu : komponenty potomne mogą emitować zdarzenie do komponentu nadrzędnego za pomocą powiązań @Output (). Komponent nadrzędny może obsługiwać zdarzenie i manipulować modelem aplikacji lub własnym modelem widoku. Zmiany w modelu aplikacji są automatycznie propagowane do wszystkich komponentów, które bezpośrednio lub pośrednio wiążą się z tym samym modelem.

  3. Zdarzenia serwisowe : składniki mogą subskrybować zdarzenia serwisowe. Na przykład dwa komponenty rodzeństwa mogą zasubskrybować to samo zdarzenie serwisowe i odpowiedzieć, modyfikując odpowiednie modele. Więcej na ten temat poniżej.

Jak mogę komunikować się między komponentem głównym a komponentem zagnieżdżonym na kilku poziomach głębokości?

  1. Współużytkowany model aplikacji : model aplikacji można przekazać z komponentu głównego do głęboko zagnieżdżonych podskładników za pomocą powiązań @Input (). Zmiany w modelu z dowolnego komponentu będą automatycznie propagowane do wszystkich komponentów, które współużytkują ten sam model.
  2. Zdarzenia serwisowe : możesz także przenieść EventEmitter do usługi współdzielonej, która pozwala dowolnemu komponentowi wstrzyknąć usługę i subskrybować zdarzenie. W ten sposób składnik główny może wywołać metodę usługi (zwykle mutującą model), która z kolei emituje zdarzenie. Kilka warstw w dół, składnik typu grand-child, który również wstrzyknął usługę i zasubskrybował to samo zdarzenie, może sobie z tym poradzić. Każda procedura obsługi zdarzeń, która zmienia wspólny model aplikacji, automatycznie propaguje się do wszystkich komponentów, które są od niego zależne. Jest to prawdopodobnie najbliższy odpowiednik $scope.broadcast()z Angulara 1. Kolejna sekcja opisuje ten pomysł bardziej szczegółowo.

Przykład obserwowalnej usługi, która wykorzystuje zdarzenia serwisowe do propagowania zmian

Oto przykład obserwowalnej usługi, która wykorzystuje zdarzenia usługi do propagowania zmian. Po dodaniu TodoItem usługa emituje zdarzenie powiadamiające swoich subskrybentów.

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

Oto, w jaki sposób składnik główny zasubskrybuje to wydarzenie:

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

Komponent potomny zagnieżdżony na kilku poziomach w głębi zasubskrybowałby to wydarzenie w ten sam sposób:

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

Oto komponent, który wywołuje usługę w celu uruchomienia zdarzenia (może znajdować się w dowolnym miejscu w drzewie komponentów):

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

Odniesienie: Wykrywanie zmiany w kącie

piksele
źródło
27
Widziałem końcowe $ w kilku postach dla obserwowalnego lub EventEmitter - np itemAdded$. Czy to konwencja RxJS czy coś takiego? Skąd to pochodzi?
Mark Rajcok
1
Niezła odpowiedź. Stwierdziłeś: „Zmiany w modelu aplikacji są automatycznie propagowane do wszystkich komponentów, które bezpośrednio lub pośrednio wiążą się z tym samym modelem”. Mam przeczucie, że to nie działa w ten sposób (ale nie jestem pewien). Inny post Savkina na blogu podaje przykład komponentu zmieniającego streetwłaściwość modelu aplikacji, ale ponieważ Angular 2 implementuje wykrywanie zmian przez tożsamość / referencję, żadne zmiany nie są propagowane ( onChangesnie są wywoływane), ponieważ referencja modelu aplikacji nie uległa zmianie ( cd ...)
Mark Rajcok
10
Możesz zaktualizować swoją odpowiedź, aby w usłudze można było obserwować zamiast EventEmitter. Zobacz stackoverflow.com/a/35568924/215945 i stackoverflow.com/questions/36076700
Mark Rajcok
2
Tak, przyrostek $ to konwencja RxJS spopularyzowana przez Cycle.js. cycle.js.org/…
jody tate
4
Nie należy ręcznie subskrybować emitenta zdarzenia. To może nie być zauważalne w ostatecznej wersji! Zobacz: bennadel.com/blog/…
NetProvoke
49

Poniższy kod jako przykład zamiany $ scope.emit () lub $ scope.broadcast () w Angular 2 przy użyciu usługi współdzielonej do obsługi zdarzeń.

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

Przykładowe użycie:

Nadawanie:

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

Słuchacz:

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

Może obsługiwać wiele argumentów:

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

this.eventsService.on('something', function (a, b, c) {
   console.log(a, b, c);
});
jim.taylor.1974
źródło
Co to robi? static get parameters () {return [new Inject (EventsService)]; }
Beanwah,
W tym przykładzie używam Ionic 2 Framework. Metoda parametrów statycznych jest wywoływana, gdy wywoływana jest metoda konstruktora i służy do dostarczania konstruktorów zależności. Wyjaśnienie tutaj stackoverflow.com/questions/35919593/…
jim.taylor.1974
1
Ładnie wykonane. Prosty i zapewnia łatwy do dostosowania system powiadomień dla całej aplikacji, a nie tylko jednorazowy.
Mike M
Właśnie stworzyłem podobną usługę z obsługą symboli wieloznacznych. Mam nadzieję, że to pomoże. github.com/govorov/ng-radio
Stanislav E. Govorov
2
Wspaniale, użyłem go, ale dodałem funkcję wyłączenia, jeśli jest już zainteresowany: off(name, listener) { this.listeners[name] = this.listeners[name].filter(x => x != listener); }
LVDM
16

Korzystam z usługi wiadomości, która otacza rxjs Subject(TypeScript)

Przykład plunkera: usługa wiadomości

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

Komponenty mogą subskrybować i transmitować wydarzenia (nadawca):

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

(odbiorca)

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

subscribeMetody MessageServicezwracającej rxjs Subscriptionobiekt, który może być subskrypcję z tak:

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

Zobacz także tę odpowiedź: https://stackoverflow.com/a/36782616/1861779

Przykład plunkera: usługa wiadomości

t.888
źródło
2
bardzo cenny. Dziękuję za odpowiedź. Właśnie odkryłem, że nie można w ten sposób komunikować się z dwoma komponentami w dwóch różnych modułach . Aby osiągnąć cel, musiałem zarejestrować MessageService na poziomie app.module, dodając tam dostawców. W każdym razie jest to naprawdę fajny sposób.
Rukshan Dangalla
to wszystko jest bardzo nieaktualne. zwłaszcza plunker, który nie ładuje żadnych zasobów pomyślnie. wszystkie są 500 kodami błędów.
tatsu
RozumiemProperty 'filter' does not exist on type 'Subject<EventMessage>'.
Drew
@Drew, w nowszych wersjach RxJS this.handler.pipe(filter(...)). Zobacz operatorów do wynajęcia .
t.888,
1
@ t.888 dzięki, wymyśliłem to. Zaktualizowana funkcja subskrypcji wygląda następującoreturn this.handler.pipe( filter(message => message.type === type), map(message => message.payload) ).subscribe(callback);
Drew
12

NIE używaj EventEmitter do komunikacji z usługą.

Powinieneś użyć jednego z obserwowalnych typów. Osobiście lubię BehaviorSubject.

Prosty przykład:

Możesz przejść do stanu początkowego, tutaj I null

let subject = new BehaviorSubject (null);

Kiedy chcesz zaktualizować temat

subject.next (myObject)

Obserwuj z dowolnej usługi lub komponentu i działaj, gdy otrzyma nowe aktualizacje.

subject.subscribe (this.YOURMETHOD);

Oto więcej informacji. .

Danial Kalbasi
źródło
1
czy mógłbyś wyjaśnić przyczyny tej decyzji projektowej?
mtraut,
@mtraut ten link również zawiera wyczerpujące wyjaśnienie.
Danial Kalbasi,
Bardziej szczegółowe wyjaśnienia dotyczące korzystania z BehaviourSubject znajdują się w tym artykule blog.cloudboost.io/…
rafalkasa,
Dokładnie to, czego potrzebowałem. Ładnie i prosto :)
Low
2

Moim ulubionym sposobem jest użycie tematu lub emitera zdarzeń (prawie taki sam) w mojej usłudze do kontrolowania całego mojego podskładnika.

Używając kątowego cli, uruchom ng gs, aby utworzyć nową usługę, a następnie użyj BehaviorSubject lub 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);
   }
}

Gdy to zrobisz, każdy komponent korzystający z usługi jako dostawca będzie wiedział o zmianie. Po prostu zasubskrybuj wynik, tak jak robisz to z 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
  }
}
Andrea Martines
źródło
1

Utworzyłem tutaj próbkę sub-publikacji:

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

Chodzi o to, aby użyć obiektów RxJs w celu podłączenia obserwatora i obserwatorów jako ogólnego rozwiązania do wysyłania i subskrybowania niestandardowych zdarzeń. W mojej próbce używam obiektu klienta do celów demonstracyjnych

this.pubSubService.Stream.emit(customer);

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

Oto także demo na żywo: http://www.syntaxsuccess.com/angular-2-samples/#/demo/pub-sub

TGH
źródło
1

To jest moja wersja:

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

posługiwać się:

export class <YOURCOMPONENT> implements IEventListener{

  constructor(private eventManager: EventManagerService) {


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


  }

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

}

wydzielać:

 this.eventManager.emit("EVENT_NAME");
zakrzu
źródło
0

Wdrożyliśmy obserwowalną dyrektywę ngModelChange, która wysyła wszystkie zmiany modelu przez emiter zdarzeń, który tworzysz we własnym komponencie. Musisz po prostu przypisać emiter zdarzeń do dyrektywy.

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

W html powiąż emiter zdarzeń (countryChanged w tym przykładzie):

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

W komponencie maszynopis wykonaj pewne operacje asynchroniczne na EventEmitter:

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)
                })
            })
    }
}
Peter Rigole
źródło
0

Zdarzenia serwisowe: składniki mogą subskrybować zdarzenia serwisowe. Na przykład dwa komponenty rodzeństwa mogą zasubskrybować to samo zdarzenie serwisowe i odpowiedzieć, modyfikując odpowiednie modele. Więcej na ten temat poniżej.

Pamiętaj jednak, aby zrezygnować z subskrypcji po zniszczeniu komponentu nadrzędnego.

BVS Bharat Kumar
źródło